This GitHub Action can be used for our GitOps workflow. The GitHub Action will build and push the Docker image for your service and deploys the new version at your Kubernetes clusters.
When you want to use this GitHub Action your GitHub repository should have a dev and master / main branch and it
should use tags for
releases.
- For the
devbranch we will change the files specified undergitops-dev. - For the
master/mainbranch we will change the files specified undergitops-stage. - For a new tag the files under
gitops-prodwill be used.
This GitOps setup should be the default for all your repositories. However, if you have a special case, you can
leave gitops-dev, gitops-stage and gitops-prod undefined, then those steps will be skipped.
The docker-registry input was renamed to docker-registries to support pushing to multiple registries.
Rename it in your workflow; the value stays the same:
-- uses: Staffbase/gitops-github-action@v8
+- uses: Staffbase/gitops-github-action@v9
with:
- docker-registry: registry.example.com
+ docker-registries: registry.example.comWorkflows that never set docker-registry (and so used the default registry.staffbase.com) need no input changes;
only bump the action reference to @v9.
docker-registry-api no longer defaults to https://registry.staffbase.com/v2/. It is now derived from the first
docker-registries entry, so a custom registry no longer needs a matching docker-registry-api. An explicitly set
docker-registry-api keeps working as before.
name: CD
on: [ push ]
jobs:
ci-cd:
name: Build, Push and Deploy
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
- name: GitOps (build, push and deploy a new Docker image)
uses: Staffbase/gitops-github-action@v9
with:
docker-username: ${{ vars.HARBOR_USERNAME }}
docker-password: ${{ secrets.HARBOR_PASSWORD }}
docker-image: private/my-service
gitops-token: ${{ secrets.GITOPS_TOKEN }}
gitops-dev: |-
clusters/customization/dev/mothership/my-service/my-service-helm.yaml spec.template.spec.containers.redbook.image
gitops-stage: |-
clusters/customization/stage/mothership/my-service/my-service-helm.yaml spec.template.spec.containers.redbook.image
gitops-prod: |-
clusters/customization/prod/mothership/my-service/my-service-helm.yaml spec.template.spec.containers.redbook.imagename: CD
on: [ push ]
jobs:
ci-cd:
name: Build and Push
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
- name: GitOps (build and push a new Docker image)
uses: Staffbase/gitops-github-action@v9
with:
docker-username: ${{ vars.HARBOR_USERNAME }}
docker-password: ${{ secrets.HARBOR_PASSWORD }}
docker-image: private/my-servicename: CD
on: [ push ]
jobs:
ci-cd:
name: Deploy
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v6
- name: GitOps (deploy a new Docker image)
uses: Staffbase/gitops-github-action@v9
with:
docker-image: private/my-service
gitops-token: ${{ secrets.GITOPS_TOKEN }}
gitops-dev: |-
clusters/customization/dev/mothership/my-service/my-service-helm.yaml spec.template.spec.containers.redbook.image
gitops-stage: |-
clusters/customization/stage/mothership/my-service/my-service-helm.yaml spec.template.spec.containers.redbook.image
gitops-prod: |-
clusters/customization/prod/mothership/my-service/my-service-helm.yaml spec.template.spec.containers.redbook.imageBy default (deployment-annotations: 'true'), whenever the action updates a GitOps file it stamps the following annotations onto the manifest's metadata.annotations:
| Annotation | Value |
|---|---|
deploy.staffbase.com/repositoryFullName |
The source repository in owner/repo form ($GITHUB_REPOSITORY) |
deploy.staffbase.com/commitSha |
The commit SHA being deployed ($GITHUB_SHA) |
deploy.staffbase.com/version |
The image tag written to the GitOps repo — always the non-timestamped tag: dev-<short-sha> on dev, main-<short-sha> on main, master-<short-sha> on master, the version without the leading v on v* tag pushes, and the tag name on other tag pushes. See GitOps tag below |
These keys mirror the Swarmia Deployment API field names and are read by flux-deployment-reporter to report deployments to Swarmia once Flux finishes reconciling. Set deployment-annotations: 'false' to skip them. The deploy.staffbase.com namespace is configurable via deployment-domain.
Enabled by default (deployment-labels: 'true'). The same three values are stamped onto the built Docker image as OCI labels, using reverse-DNS keys — the deployment-domain reversed (deploy.staffbase.com → com.staffbase.deploy):
| Label | Value |
|---|---|
com.staffbase.deploy.repositoryFullName |
The source repository in owner/repo form ($GITHUB_REPOSITORY) |
com.staffbase.deploy.commitSha |
The commit SHA being deployed ($GITHUB_SHA) |
com.staffbase.deploy.version |
The non-timestamped GitOps tag (same value as the deploy.staffbase.com/version annotation) |
Note: labels are baked in at build time, so they are only applied on builds. Release (
v*) and custom-tag runs that retag an existing image instead of rebuilding (see Image tags) do not get fresh labels — the retagged image keeps the labels from the branch build it was promoted from. This feature is independent of the annotations above; enable either, both, or neither.
Our recovery/failover regions have no ARM capacity, so images deployed there must ship both linux/amd64 and linux/arm64. The action never cross-compiles or emulates: each architecture is built natively on its own runner, and the results are combined into one manifest list afterwards.
This needs a job matrix, which a composite action cannot create itself — so the fan-out lives in your workflow, and the action provides the two halves via multiarch-mode:
multiarch-mode: build— builds the runner's native platform (docker-build-platformsis ignored), pushes it by digest only (no tags), and uploads the digest as an artifact. GitOps, retagging and Upwind are skipped.multiarch-mode: merge— downloads all digest artifacts, combines them into one multi-arch image, applies the real tags, and then runs the GitOps and Upwind steps exactly as a normal run would.
name: CD
on: [ push ]
jobs:
build:
name: Build (${{ matrix.arch }})
runs-on: ${{ matrix.runs-on }}
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runs-on: ubuntu-24.04
- arch: arm64
runs-on: ubuntu-24.04-arm
steps:
- name: Checkout
uses: actions/checkout@v6
- name: GitOps (build and push by digest)
uses: Staffbase/gitops-github-action@v9
with:
multiarch-mode: build
docker-username: ${{ vars.HARBOR_USERNAME }}
docker-password: ${{ secrets.HARBOR_PASSWORD }}
docker-image: private/my-service
deploy:
name: Merge and Deploy
runs-on: ubuntu-24.04
needs: build
steps:
- name: Checkout
uses: actions/checkout@v6
- name: GitOps (merge manifests and deploy)
uses: Staffbase/gitops-github-action@v9
with:
multiarch-mode: merge
docker-username: ${{ vars.HARBOR_USERNAME }}
docker-password: ${{ secrets.HARBOR_PASSWORD }}
docker-image: private/my-service
gitops-token: ${{ secrets.GITOPS_TOKEN }}
gitops-dev: |-
clusters/customization/dev/mothership/my-service/my-service-helm.yaml spec.template.spec.containers.redbook.imagePass the same docker-* inputs to both jobs — the merge job recomputes the tags, so docker-custom-tag, docker-tag-timestamp and friends must match. Notes:
- On branches that do not push (e.g. feature branches), the build jobs just build and validate; the merge job finds nothing to merge and skips straight to the GitOps steps.
- Release (
v*) and custom-tag runs that retag an existing image do not rebuild, so they promote the existing multi-arch image untouched. Only the merge job performs the retag. docker-build-outputscannot be combined withmultiarch-mode: build; the build already pushes by digest.- Building more than one image in a single workflow? Give each one a distinct
multiarch-artifact-name, or the digests get mixed up.
docker-registries takes either a single registry (the default, registry.staffbase.com) or a newline-separated list of registries to migrate between without losing the ability to roll back. Every build, merge and retag pushes to all of them; GitOps manifests and release-retag lookups always use the first entry (the primary registry).
- name: GitOps
uses: Staffbase/gitops-github-action@v9
with:
docker-registries: |-
registry.staffbase.com|${{ vars.HARBOR_USERNAME }}|${{ secrets.HARBOR_PASSWORD }}
europe-docker.pkg.dev|oauth2accesstoken|${{ steps.gar.outputs.access-token }}
docker-image: private/my-service
gitops-token: ${{ secrets.GITOPS_TOKEN }}Notes:
- Each line is
registry[|username[|password]]. Omitting the username/password on a line falls back to the top-leveldocker-username/docker-password. - The registry part can carry a path prefix after the host (e.g.
europe-docker.pkg.dev/staffbase-artifacts/images-publish) when a registry addresses a project/repository as part of the push path. Login always uses just the host; the full value is used to build the pushed image ref. - Two entries sharing a host must use identical credentials — Docker's own credential store is keyed by host alone, so conflicting credentials on the same host fail fast at login instead of silently overwriting each other.
- A username of
oauth2accesstoken(Google Artifact/Container Registry's convention for "this password is an OAuth2 access token") authenticates release-retag's manifest lookups with a Bearer token instead of HTTP Basic, since that's what GAR's registry API requires. - To roll back, drop the extra registry from the list (or reorder to make a different one primary) — no rebuild needed, since the primary registry's images are untouched.
| Name | Description | Default |
|---|---|---|
docker-registries |
Docker Registry, or a newline-separated list of registry[|username[|password]] entries to push to more than one, for migrating between registries. See Multiple Registries |
registry.staffbase.com |
docker-registry-api |
Docker Registry API used for release-retag manifest lookups. Defaults to the standard v2 API form of the primary docker-registries entry's host, preserving its path prefix if it has one (https://<host>/v2/ or https://<host>/v2/<path>/); set explicitly only for a non-standard endpoint. |
|
docker-image |
Docker Image | |
docker-custom-tag |
Docker Custom Tag to be set on the image | |
docker-tag-timestamp |
Insert a UTC timestamp into dev/main/master branch tags (dev-<timestamp>-<short-sha>) to make them sortable for Flux image automation. Enabled by default; set to 'false' for the legacy <prefix>-<short-sha> format |
true |
docker-tag-keep-v-prefix |
Keep the leading v on release (v*) tags (v1.2.3 → v1.2.3). Default strips it (v1.2.3 → 1.2.3) |
false |
docker-username |
Username for the Docker Registry | |
docker-password |
Password for the Docker Registry | |
docker-file |
Dockerfile | ./Dockerfile |
docker-build-args |
List of build-time variables | |
docker-build-secrets |
List of secrets to expose to the build (e.g., key=string, GIT_AUTH_TOKEN=mytoken) | |
docker-build-secret-files |
List of secret files to expose to the build (e.g., key=filename, MY_SECRET=./secret.txt) | |
docker-build-target |
Sets the target stage to build like: "runtime" | |
docker-build-platforms |
Sets the target platforms for build. Ignored when multiarch-mode: build (the runner's own architecture wins) |
linux/amd64 |
multiarch-mode |
build or merge to build a multi-arch image from a job matrix, empty for a normal single-arch build. See Multi-Arch Images |
|
multiarch-artifact-name |
Base name of the artifact carrying the per-architecture digests between the build and merge jobs (the architecture is appended) | docker-digests |
multiarch-digests-path |
Directory holding the per-architecture digest files | /tmp/gitops-action-digests |
docker-build-provenance |
Generate provenance attestation for the build | false |
docker-disable-retagging |
Disables retagging of existing images and run a new build instead | false |
deployment-annotations |
Stamp deployment-tracking annotations (deploy.staffbase.com/*) onto updated GitOps manifests. See Deployment tracking annotations |
true |
deployment-domain |
Key namespace for deployment-tracking metadata. Used verbatim for annotation keys (<domain>/...) and reversed to reverse-DNS for label keys (com.staffbase.deploy.*) |
deploy.staffbase.com |
deployment-labels |
Stamp deployment-tracking labels (com.staffbase.deploy.*) onto the built image. Only applied on builds, not on release/custom retags. See Deployment tracking labels |
true |
gitops-organization |
GitHub Organization for GitOps | Staffbase |
gitops-repository |
GitHub Repository for GitOps | mops |
gitops-user |
GitHub User for GitOps | Staffbot |
gitops-email |
GitHub Email for GitOps | staffbot@staffbase.com |
gitops-token |
GitHub Token for GitOps | |
gitops-dev |
Files which should be updated by the GitHub Action for DEV, must be relative to the root of the GitOps repository | |
gitops-stage |
Files which should be updated by the GitHub Action for STAGE, must be relative to the root of the GitOps repository | |
gitops-prod |
Files which should be updated by the GitHub Action for PROD, must be relative to the root of the GitOps repository | |
working-directory |
The directory in which the GitOps action should be executed. The docker-file variable should be relative to working directory. | . |
| Name | Description |
|---|---|
docker-digest |
Digest of the image |
docker-tag |
Tag of the image |
The generated image tag depends on the Git ref:
| Ref | Tag (default) | Tag (docker-tag-timestamp: 'false') |
Floating tag |
|---|---|---|---|
dev branch |
dev-<utc-timestamp>-<short-sha> |
dev-<short-sha> |
dev |
main branch |
main-<utc-timestamp>-<short-sha> |
main-<short-sha> |
main |
master branch |
master-<utc-timestamp>-<short-sha> |
master-<short-sha> |
master |
v* tag (prod) |
the version with the v stripped, e.g. v2025.50.14 → 2025.50.14 (or kept with docker-tag-keep-v-prefix: 'true') |
(unchanged) | latest |
| other branch | <short-sha> (not pushed) |
(unchanged) | — |
By default branch tags carry a YYYYMMDDHHMMSS (UTC) timestamp inserted before the
SHA. This makes branch tags sortable so
Flux image automation can pick the
newest build — the Git SHA alone is not orderable. The short SHA is kept for
traceability and Flux sorts on the timestamp only. Set docker-tag-timestamp: 'false'
to fall back to the legacy <prefix>-<short-sha> shape.
Note: with the timestamp enabled (the default) the build also pushes the plain
<prefix>-<short-sha>tag alongside the timestamped one. That stable per-commit tag is what the release step retags into the version tag and what the action writes to the GitOps repo (see GitOps tag), so it must continue to exist. It does not match the^<prefix>-[0-9]+-[0-9a-f]+$filter below, so Flux ignores it.
The tag the action builds and pushes is the timestamped one (so Flux image
automation can sort it). The tag the action writes to the external GitOps repo
(gitops-dev/gitops-stage/gitops-prod files and the deployment annotations) is
always the non-timestamped tag — the stable <prefix>-<short-sha> alias for
branch builds, and the plain tag for v*/custom builds (which never carry a
timestamp).
This is deliberate and not configurable. When the action runs across separate
invocations — e.g. one step builds the image and a later step pushes and updates
GitOps — each invocation recomputes a fresh timestamp, so the timestamped tag
differs between them. The <prefix>-<short-sha> alias is deterministic, so the
GitOps reference stays consistent and always points at an image that was actually
pushed.
With the timestamp enabled, use one ImagePolicy per environment, filtering by prefix:
# dev (and likewise main-/master- for stage)
spec:
imageRepositoryRef: { name: my-service }
filterTags:
pattern: '^dev-(?P<ts>[0-9]+)-[0-9a-f]+$'
extract: '$ts'
policy:
numerical: { order: asc }
---
# prod — CalVer tags parse as SemVer (no zero-padding!)
spec:
imageRepositoryRef: { name: my-service }
policy:
semver: { range: '>=0.0.0' }Note: the prod
semverpolicy only works if CalVer parts are never zero-padded (2025.5.3, not2025.05.03) — SemVer forbids leading zeros. Track the immutable*-<timestamp>-<sha>tags, not the floatingdev/maintags, so deployments keep their provenance.
Please read CONTRIBUTING.md for details on our code of conduct, and the process for submitting pull requests to us.
This project is licensed under the Apache-2.0 License - see the LICENSE.md file for details.
|
Staffbase GmbH
Staffbase is an internal communications platform built to revolutionize the way you work and unite your company. Staffbase is hiring: jobs.staffbase.com GitHub | Website | Jobs |
Go to the release overview page and publish the draft release with a new version number. Make sure to update the floating version commit.
PRs that break existing callers get the major label and an entry under Upgrading. Before publishing
a major release, add a link to that entry at the top of the release notes.
