# Push from CI

The [CI guides](/guides/ci/overview) build an image, push it to a registry and deploy it. If the registry is your own **Zaroz Registry**, two things get simpler: you push with a credential that can do only that, and the Workload pulls the image **without any registry login** in its source (Zaroz hands the cluster its own pull credential for your registry).

Before you start, finish the [Registry quick start](/guides/registry/quick-start) up to step 2.

## A credential for the pipeline

Create a credential named after the pipeline with scope **Pull and push**. Not "Pull, push and delete": a build that can delete tags can delete what runs in production. Store the username and the secret in your CI's secret store as `ZAROZ_REGISTRY_USERNAME` and `ZAROZ_REGISTRY_SECRET`. Store the login host as `ZAROZ_REGISTRY_HOST` and the image name as `ZAROZ_REGISTRY_IMAGE` (`<login host>/<namespace>/my-app`); these two are not secret.

Use one credential per pipeline and environment. Revoking it stops that pipeline and nothing else; a token already issued lapses within minutes.

## GitHub Actions

```yaml
# .github/workflows/deploy.yml (the build job; the deploy job is the one in the GitHub Actions guide)
jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image: ${{ steps.meta.outputs.image }}
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          registry: ${{ vars.ZAROZ_REGISTRY_HOST }}
          username: ${{ secrets.ZAROZ_REGISTRY_USERNAME }}
          password: ${{ secrets.ZAROZ_REGISTRY_SECRET }}
      - id: meta
        # One immutable tag per commit: Zaroz does not watch a moved tag.
        run: echo "image=${{ vars.ZAROZ_REGISTRY_IMAGE }}:${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT"
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.image }}
```

In the deploy job of the [GitHub Actions guide](/guides/ci/github-actions), leave out `REGISTRY_USERNAME` and `REGISTRY_PASSWORD`: the image is yours and the cluster already can pull it.

## GitLab CI

```yaml
# .gitlab-ci.yml
build:
  stage: build
  image: docker:27
  services: [docker:27-dind]
  variables:
    IMAGE: "$ZAROZ_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
  script:
    - echo "$ZAROZ_REGISTRY_SECRET" | docker login "${ZAROZ_REGISTRY_IMAGE%%/*}" --username "$ZAROZ_REGISTRY_USERNAME" --password-stdin
    - docker build -t "$IMAGE" .
    - docker push "$IMAGE"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
```

Put the variables in **Settings > CI/CD > Variables**, masked, and protected if the branch is protected. `${ZAROZ_REGISTRY_IMAGE%%/*}` keeps only the login host from the image name. The deploy job is the one in the [GitLab CI guide](/guides/ci/gitlab-ci), again without registry login variables.

## Forgejo Actions and Gitea Actions

```yaml
# .forgejo/workflows/deploy.yml
jobs:
  build:
    runs-on: docker
    steps:
      - uses: actions/checkout@v4
      - name: Log in
        env:
          SECRET: ${{ secrets.ZAROZ_REGISTRY_SECRET }}
        run: echo "$SECRET" | docker login "${IMAGE_REPO%%/*}" --username "${{ secrets.ZAROZ_REGISTRY_USERNAME }}" --password-stdin
      - name: Build and push
        run: |
          IMAGE="${IMAGE_REPO}:$(echo "$GITHUB_SHA" | cut -c1-12)"
          docker build -t "$IMAGE" .
          docker push "$IMAGE"
    env:
      IMAGE_REPO: ${{ vars.ZAROZ_REGISTRY_IMAGE }}
```

The runner needs access to a Docker daemon. The deploy step is the one in the [Forgejo and Gitea guide](/guides/ci/forgejo-actions).

## Keep the registry tidy

A pipeline that pushes one tag per commit fills the registry. Once a release is out, delete old build tags from the **Tags** tab, or with a credential of scope "Pull, push and delete" kept outside the build pipeline, for example in a scheduled job that calls `DELETE /subscriptions/{id}/registry/repositories/tags`. Stored size, and your bill, fall after the registry's next clean-up run.
