# Deploys on GitHub Actions

This guide sets up a repository that deploys from `main` through hushrun's reusable workflow. A push of files that already passed deploys the image built for them, by digest, with no build and no check to repeat. For what a deploy pipeline is, start at [Deploys](https://hushrun.com/docs/deploy.md).

## Before you begin

- Admin rights on the repository.
- An org on hushrun ([sign up](https://app.hushrun.com/sign-up)), with the repository added: an org admin's `npx hushrun init` adds it.
- The container registry your image already uses.
- hushrun installed: see [Quickstart](https://hushrun.com/docs/quickstart.md).

## Write hushrun.yaml

```bash
hushrun init --pipeline --image us-docker.pkg.dev/acme/api/server
```

Edit the file: the checks the deploy requires and their commands, the branch, and in a monorepo the scopes and `digest.exclude` ([What a deploy requires](https://hushrun.com/docs/deploy.md#deploy)). Then check it:

```console
$ hushrun policy validate
hushrun.yaml is valid: 1 scope(s), 3 check(s), 1 artifact(s), and it deploys
```

In a monorepo, include `.github/workflows/hushrun.yml` and `hushrun.yaml` in each scope, so that a change to the deploy changes the digest.

## Connect the repository

Run `npx hushrun init`. It adds `repository_url` and `transparency_log` to `hushrun.yaml`. Commit the file: hushrun reads it as committed.

With the GitHub CLI (`gh`) signed in, init also tells hushrun the repository's GitHub id, which is how hushrun's server recognizes the repository's CI. Without `gh`, the server learns it from the repository's first CI run.

## Write the workflow

```bash
hushrun adopt
```

It writes `.github/workflows/hushrun.yml`, one short workflow that calls hushrun's reusable workflow. The name leaves a `deploy.yml` of your own alone: remove that one once this one deploys. Set `build`, `deploy` and your cloud credentials, and pin the reusable workflow to a release commit: see [The reusable workflow](#reusable-workflow).

`adopt` keeps a file that differs from what it would write. `--dry-run` prints what it would write, and `--force` replaces. `--branch` names the branch it deploys from, and `--image` the image's repository.

## Check the setup

```bash
hushrun doctor
```

It exits 2 when a check fails, and says what to fix. [Commands](https://hushrun.com/docs/reference/cli.md#hushrun-doctor) lists everything it checks.

## Push to main

Push to `main`. The first run plans `full`: it runs the checks, builds, deploys, and records each step. Re-run it, or later revert to these files, and it plans `promote`: it deploys the image it already built and checked, by digest, with no build and no check to repeat.

Each plan job's step summary says what the run does: see [Day to day](#day-to-day).

## Day to day

Each plan job's step summary starts with what the run does, then gives the mode and the reason. For scopes other than `default`, it's a table with a row for each scope instead.

| Mode | The step summary says | What you do |
|---|---|---|
| `promote` | **Deploying** the image an earlier run built and checked, by digest. Nothing is rebuilt or checked again. | Nothing |
| `refresh` | **Deploying** the image an earlier run built, after running the checks below first. | Nothing. Only checks without valid evidence run ([Which checks run](https://hushrun.com/docs/deploy.md#checks-to-run)) |
| `full` | **Building**, running the checks below, then deploying. | Nothing |
| `current` | **Nothing to deploy.** This repository already runs this code. | Nothing. To deploy it again, re-run the workflow, or run it by hand |
| `blocked` | **Not deployed.** The job fails on purpose; the reason says what to do. | What the reason says. See below |

- **A blocked plan.** The reason says what to do: the run is on another branch than `deploy.branch`, or it was forced where `allow_force: false` forbids it.
- **Force a full run.** In the Actions tab, run the workflow by hand with `force_full` checked, which the workflow passes to the reusable workflow's `force-full` input. A forced run builds and runs every required check.
- **A newer commit landed while deploying.** `guard` records `superseded`, and the deploy steps skip. The newer run deploys instead.
- **Re-run failed jobs.** A re-run is a new attempt of the same run, and hushrun records it as one.
- **Clear old events.** An org admin runs `hushrun repository gc --older-than 180d --dry-run` to see what would go, then runs it without `--dry-run`. It keeps the newest deployment of each scope.
- **The org's plan is used up.** New check results aren't recorded, and `run` and `attest` say so once, with where to upgrade. Everything else is recorded, and deploys go on: see [Plans and limits](https://hushrun.com/docs/plans.md#at-the-limit).

## Keyless signing

Give every job that runs hushrun `permissions: id-token: write`. With GitHub's ID token, the job authenticates to hushrun's server and signs its events keylessly, as the repository's CI. It needs no secret and no key.

- Sigstore's certificate authority (Fulcio) certifies a key that lives only in the job, and a timestamp authority timestamps each signature.
- The certificate names the repository's numeric id, which hushrun's server knows as the repository's CI. The events count as `ci`.
- Any workflow of the repository signs as its CI, except a run that `pull_request_target` started: it runs the base branch's workflow for a change that hasn't merged.
- In CI, hushrun checks that the server's CI is this job's repository, and exits 2 otherwise. A `repository_url` that points at another repository fails loudly.
- When Fulcio or the timestamp authority can't be reached, a signing step exits 9 and records nothing: re-run the job. Verifying needs neither. It's offline, against the Sigstore trust root built into hushrun, or a newer one `hushrun sigstore refresh` fetches.

`HUSHRUN_SIGNING=sigstore` is the default in a job with an ID token and no key. The reusable workflow sets it with `signing: sigstore`.

Public-good Sigstore logs every certificate it issues in a public log, with the repository's and the workflow's names. To keep a private repository's names out of it, name a private Sigstore instance with `HUSHRUN_FULCIO_URL`, `HUSHRUN_TSA` and `HUSHRUN_SIGSTORE_TRUSTED_ROOT`. Or sign with an agent's machine key ([Evidence from agents](https://hushrun.com/docs/deploy/evidence.md#agents)), whose events don't count while the repository [takes CI's evidence only](https://hushrun.com/docs/deploy.md#ci-only).

### The repository's URL in CI

- hushrun reads the URL from the committed `hushrun.yaml`'s `repository_url`, and sends it only the job's ID token, whose audience is that URL. No variable is needed.
- To set it outside the repository too, add a repository variable `HUSHRUN_REPOSITORY_URL` (Settings, Secrets and variables, Actions, Variables). `hushrun adopt`'s workflow passes it, empty when unset. When it's set, it must match `hushrun.yaml`'s, or every command that reads or writes events exits 2.
- `HUSHRUN_REPOSITORY_TOKEN` goes only to a URL that `HUSHRUN_REPOSITORY_URL` or `--repository-url` names, so a change to `hushrun.yaml` can't send a secret elsewhere.

## The plan and the jobs after it

The plan job decides once, and every later job carries out its plan:

- **plan:** `hushrun plan --start` decides, and records `started` for the scope it deploys. It writes its outputs to `$GITHUB_OUTPUT` (`mode`, `reason`, `digest`, `revision`, `attempt`, `build`, `ref`, `artifacts`, `checks`, and the whole plan as `json`), and the step summary. It exits 3 when the plan is `blocked`. With several scopes, `actionable_scopes` lists the ones that deploy, in place of the per-scope outputs.
- **test:** `hushrun run` runs exactly the plan's checks, and records each result. It exits 8 when a check fails.
- **deploy:** runs when the mode is `promote`, `refresh` or `full`.
  1. `hushrun guard` stops a deploy of a revision that is no longer the branch's head ([Guard](#guard)).
  2. `hushrun artifact resolve` outputs `ref`, the image to deploy, and `build`.
  3. When `build` is `true`, your build pushes the image to `$REF`, and `hushrun attest --artifact image=$REF` records it, pinned by digest.
  4. Your deploy deploys `$REF`, then `hushrun mark deployed --artifact image=$REF`.
  5. `hushrun mark passed --artifact image=$REF`.
- **report:** when the run is cancelled, `hushrun mark superseded --reason cancelled`. When a job failed or timed out, `hushrun mark failed --reason "…"`.

Each later job gets the plan as `HUSHRUN_PLAN_JSON`, from the plan job's `json` output, or as a file with `--plan plan.json`.

To write these jobs by hand:

- Give every job `permissions: { contents: read, id-token: write }`. hushrun asks GitHub for nothing but the ID token.
- Install hushrun with `hushrun/hushrun/action/setup`, pinned to a release commit. It installs the newest release, checked against the SHA-256 published with it, or the one `with: { version: 0.9.0, sha256: … }` pins. Pass the plan job's `version` output to the later jobs, so a run never mixes two releases.
- Check out the revision the plan was made for in every job: never check out `ref: main` or pass `--rev`.

## Guard

A newer commit can land on the branch while a deploy runs. `hushrun guard` checks that the revision being deployed is still the head of `deploy.branch` on `origin`:

- If it is, `guard` outputs `stale=false` and exits 0.
- If the branch moved on, `guard` records `superseded`, outputs `stale=true`, and exits 0. The deploy steps after it skip on `steps.guard.outputs.stale == 'true'`, and the newer run deploys instead.

`--on-stale fail` exits 4 instead. `--branch` and `--remote` name another branch or remote.

## Pull requests

A job on `pull_request` reads `hushrun.yaml` from the branch the pull request merges into, which was reviewed, never from the change. Fetch that branch, and name it with `--config-ref`:

```bash
git fetch --depth=1 origin "$GITHUB_BASE_REF"
hushrun run --all --config-ref "origin/$GITHUB_BASE_REF"
```

Without `--config-ref`, or with another branch, every command that reads or writes events exits 2.

- A pull request's jobs sign as the repository's CI too. Run its checks there, and deploy only from `main`.
- The checks a pull request passes count for the same digest. When `main` hasn't moved, the merged commit has the same files, and the deploy doesn't run those checks again.

## The reusable workflow

`hushrun adopt` writes one short workflow that calls hushrun's reusable workflow, which holds the plan, test, deploy and report jobs above. With its values set:

```yaml title=".github/workflows/hushrun.yml"
name: Deploy
on:
  push:
    branches: [main]
  workflow_dispatch:
    inputs:
      force_full:
        description: Build and run every check, whatever the evidence
        type: boolean
        default: false

# One deploy at a time; runs started off main get a group of their own.
concurrency:
  group: ${{ github.ref == 'refs/heads/main' && 'deploy' || format('deploy-refused-{0}', github.run_id) }}
  cancel-in-progress: false

permissions:
  contents: read
  id-token: write          # workload identity federation; ID tokens for hushrun's server

jobs:
  deploy:
    uses: hushrun/hushrun/.github/workflows/hushrun-deploy.yml@main   # pin to a release commit
    with:
      # version: 0.9.0          # pins hushrun; without it, each run installs the newest release
      repository-base: ${{ vars.HUSHRUN_REPOSITORY_BASE }}
      repository-url: ${{ vars.HUSHRUN_REPOSITORY_URL }}
      force-full: ${{ inputs.force_full || false }}
      toolchain: node
      setup: gcloud auth configure-docker us-docker.pkg.dev --quiet
      build: docker build -t "$REF" . && docker push "$REF"
      deploy: gcloud run deploy api --image "$REF" --region us-central1
      gcp-workload-identity-provider: projects/123456789/locations/global/workloadIdentityPools/github/providers/github
      gcp-service-account: deployer@acme.iam.gserviceaccount.com
      signing: sigstore        # keylessly, as this repository's CI
      # deploy-environment: NAME   # a GitHub environment, whose rules guard the deploy job
```

Its inputs:

| Input | What it is |
|---|---|
| `version` | The hushrun release every job installs, such as `0.9.0`. Default: the newest release when the plan job starts |
| `deploy` | The deploy command. `$REF` is the image, pinned by digest. Required |
| `build` | The build command, run when the plan builds. It pushes the image to `$REF` |
| `setup` | Commands every job runs after its credentials, such as `gcloud auth configure-docker` |
| `image` | The artifact of `hushrun.yaml` that `build` and `deploy` work on. Default `image` |
| `scope` | The scope to deploy, in a repository with several. Default its only one |
| `force-full` | Build and run every check, whatever the evidence |
| `isolated` | Run the plan's checks in their pinned images with Docker, as agents run them ([pinned images](https://hushrun.com/docs/deploy/evidence.md#images)) |
| `toolchain`, `toolchain-version` | The toolchain the checks need, `go`, `node`, `python`, `java` or `none`, and its version |
| `runs-on` | The runner of every job. Default `ubuntu-latest` |
| `signing` | `sigstore` to sign keylessly, as the repository's CI |
| `deploy-environment` | The GitHub environment of the deploy job, whose protection rules guard the deploy |
| `signing-environment` | The GitHub environment of the jobs that sign |
| `repository-url`, `repository-base` | The repository's URL from your settings. When empty, hushrun reads `hushrun.yaml`'s |
| `gcp-workload-identity-provider`, `gcp-service-account` | Google Cloud credentials through workload identity federation |
| `aws-role-to-assume`, `aws-region` | AWS credentials through GitHub's OIDC provider |
| `azure-client-id`, `azure-tenant-id`, `azure-subscription-id` | Azure credentials through a federated credential |

Its outputs are `mode`, the plan's mode, and `ref`, the image deployed. No job needs a cloud secret: credentials come through your cloud's workload identity federation, with GitHub's ID token.

### Which hushrun CI runs

Each run installs the newest hushrun release when its plan job starts, and every later job of the run installs the same one. A new release reaches your pipeline at its next run: its fixes without an edit, and a bad release as fast.

To pin one release, set `version`, such as `version: 0.9.0`, or run `hushrun adopt --hushrun-version 0.9.0`. A pinned version changes only through a commit you review.

### A monorepo

Call the reusable workflow once per scope. Each plans its own digest, so a scope whose files didn't change plans `current` while the others deploy. `build` and `deploy` see the scope as `$SCOPE`:

```yaml title=".github/workflows/hushrun.yml"
jobs:
  deploy:
    strategy:
      fail-fast: false
      matrix:
        scope: [api, web]
    uses: hushrun/hushrun/.github/workflows/hushrun-deploy.yml@main   # pin to a release commit
    with:
      scope: ${{ matrix.scope }}
      image: ${{ matrix.scope }}-image
      build: docker build -t "$REF" "services/$SCOPE" && docker push "$REF"
      deploy: gcloud run deploy "$SCOPE" --image "$REF" --region us-central1
      signing: sigstore
```

## Exit codes

The ones a pipeline meets most, from [the full list](https://hushrun.com/docs/reference/cli.md#exit-codes):

| Code | Meaning |
|---|---|
| 2 | A usage or configuration error, such as a `repository_url` that doesn't match, a pull request without `--config-ref`, or a `hushrun.yaml` that `plan` refuses |
| 3 | `plan`: blocked |
| 4 | `guard --on-stale fail`: the branch moved on |
| 6 | hushrun's server is unavailable or refused access, or a transparency check failed |
| 7 | `artifact resolve --no-fallback`: the image can't be reused |
| 8 | `run`: a check failed, and its result was recorded |
| 9 | The signer is unavailable: GitHub's ID token, Fulcio, or the timestamp authority |

## Next

- [The planner](https://hushrun.com/docs/deploy.md#planner): what each mode means, and what happens in common situations.
- [Evidence from agents and laptops](https://hushrun.com/docs/deploy/evidence.md): let agents run the deploy's checks before a push.
- [Transparency log](https://hushrun.com/docs/deploy/transparency.md): monitor hushrun's server from your CI.
