# Deploy from GitHub Actions

Prerequisites: a Workload that already runs (created in the console), its service instance id, and the [deploy token](/guides/ci/overview#create-a-token-that-can-only-deploy). Add [`zaroz-deploy.sh`](/guides/ci/shell) to your repository as `deploy/zaroz-deploy.sh`.

## Secrets and variables

In **Settings > Secrets and variables > Actions**, create these on the `production` environment (Settings > Environments):

| Name | Kind | Value |
|---|---|---|
| `ZAROZ_TOKEN` | secret | the `zaroz_...` deploy token |
| `ZAROZ_SERVICE` | variable | the service instance UUID |
| `GHCR_PULL_TOKEN` | secret | only for a private package: a classic token with `read:packages`, **not** the one that pushes |

Environment secrets are only handed to jobs that target that environment, so add **required reviewers** to `production` if a human should approve each deploy.

## The workflow

```yaml
# .github/workflows/deploy.yml
name: deploy

on:
  push:
    branches: [main]

# Two pushes in a row must not deploy at the same time: Zaroz refuses the second
# with 409 anyway, and this queues it instead of failing it.
concurrency:
  group: deploy-production
  cancel-in-progress: false

permissions:
  contents: read
  packages: write

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image: ${{ steps.meta.outputs.image }}
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - id: meta
        # One immutable tag per commit: Zaroz does not watch a moved tag.
        run: echo "image=ghcr.io/${GITHUB_REPOSITORY,,}:${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT"
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.image }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    # Put the approval rule (reviewers) on this environment, not on the token.
    environment: production
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Zaroz
        run: sh deploy/zaroz-deploy.sh
        env:
          IMAGE: ${{ needs.build.outputs.image }}
          ZAROZ_TOKEN: ${{ secrets.ZAROZ_TOKEN }}
          ZAROZ_SERVICE: ${{ vars.ZAROZ_SERVICE }}
          # Only for a private image: a read-only token that can pull the package.
          REGISTRY_USERNAME: ${{ github.actor }}
          REGISTRY_PASSWORD: ${{ secrets.GHCR_PULL_TOKEN }}
```


How it behaves:

- `build` pushes `ghcr.io/<owner>/<repo>:<first 12 chars of the commit>`. Each commit gets its own tag.
- `deploy` runs the script, which fails the job when the rollout fails and has already put the previous image back.
- `concurrency` queues a second push instead of racing the first. Zaroz would otherwise refuse it with `409` while the first deployment holds the service.
- The job log prints `deployment #N started`, then `... is serving` or the `failure_reason`.

## Roll back by hand

Re-run the `deploy` job of an older successful run: it deploys that run's image again. Or from a terminal: `IMAGE=ghcr.io/acme/api:<old tag> sh deploy/zaroz-deploy.sh`.

## Pull requests

Do not give a pull request from a fork access to `ZAROZ_TOKEN`. The workflow above runs only on pushes to `main`, where secrets are available. For a preview environment, mint a second token scoped to a separate preview service.
