From 297c6eba041f5bf19fe41039fa5279919de25a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Bauer?= Date: Fri, 10 Jul 2026 10:47:42 +0200 Subject: [PATCH] CORE-4588 - add deployment labels to docker image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: André Bauer --- README.md | 19 +++++++++++++-- action.yml | 25 ++++++++++++++++++++ scripts/generate-label-prefix.sh | 14 +++++++++++ scripts/lib/common.sh | 17 +++++++++++++- scripts/lib/gitops-functions.sh | 15 ++++++++---- tests/generate-label-prefix.bats | 27 +++++++++++++++++++++ tests/lib-common.bats | 20 ++++++++++++++++ tests/lib-gitops-functions.bats | 40 ++++++++++++++++++++++++++++++++ 8 files changed, 170 insertions(+), 7 deletions(-) create mode 100755 scripts/generate-label-prefix.sh create mode 100644 tests/generate-label-prefix.bats diff --git a/README.md b/README.md index 5ffbe93..1ebff73 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ jobs: ### Deployment tracking annotations -Whenever the action updates a GitOps file, it stamps the following annotations onto the manifest's `metadata.annotations`: +By default (`deployment-annotations: 'true'`), whenever the action updates a GitOps file it stamps the following annotations onto the manifest's `metadata.annotations`: | Annotation | Value | |------------|-------| @@ -116,7 +116,19 @@ Whenever the action updates a GitOps file, it stamps the following annotations o | `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-` on `dev`, `main-` on `main`, `master-` on `master`, the version without the leading `v` on `v*` tag pushes, and the tag name on other tag pushes. See [GitOps tag](#gitops-tag) below | -These keys mirror the [Swarmia Deployment API](https://help.swarmia.com/settings/organization/configuring-deployments-in-swarmia) field names and are read by `flux-deployment-reporter` to report deployments to Swarmia once Flux finishes reconciling. +These keys mirror the [Swarmia Deployment API](https://help.swarmia.com/settings/organization/configuring-deployments-in-swarmia) 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`](#inputs). + +### Deployment tracking labels + +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`](#inputs) 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](#image-tags--flux-image-automation)) 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. ## Inputs @@ -138,6 +150,9 @@ These keys mirror the [Swarmia Deployment API](https://help.swarmia.com/settings | `docker-build-platforms` | Sets the target platforms for build | linux/amd64 | | `docker-build-provenance` | Generate [provenance](https://docs.docker.com/build/attestations/slsa-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](#deployment-tracking-annotations) | `true` | +| `deployment-domain` | Key namespace for deployment-tracking metadata. Used verbatim for annotation keys (`/...`) 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](#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` | diff --git a/action.yml b/action.yml index dbc7f62..6dae410 100644 --- a/action.yml +++ b/action.yml @@ -62,6 +62,18 @@ inputs: description: 'Disable retagging of existing images' required: false default: 'false' + deployment-annotations: + description: 'Stamp deployment-tracking annotations (deploy.staffbase.com/repositoryFullName, /commitSha, /version) onto the updated GitOps manifests. Enabled by default; set to ''false'' to skip.' + required: false + default: 'true' + deployment-domain: + description: 'Domain used as the key namespace for deployment-tracking metadata. Applied verbatim to GitOps annotation keys (/repositoryFullName) and reversed to reverse-DNS for image label keys (deploy.staffbase.com -> com.staffbase.deploy.repositoryFullName).' + required: false + default: 'deploy.staffbase.com' + deployment-labels: + description: 'Stamp deployment-tracking labels (repositoryFullName, commitSha, version) onto the built Docker image. Enabled by default; only applied on builds, not on release/custom retags. Set to ''false'' to skip.' + required: false + default: 'true' gitops-organization: description: 'GitHub Organization for GitOps' required: true @@ -127,6 +139,13 @@ runs: INPUT_DOCKER_IMAGE: ${{ inputs.docker-image }} run: ${{ github.action_path }}/scripts/generate-tags.sh + - name: Generate Deployment Label Prefix + id: deployment_labels + shell: bash + env: + INPUT_DEPLOYMENT_DOMAIN: ${{ inputs.deployment-domain }} + run: ${{ github.action_path }}/scripts/generate-label-prefix.sh + - name: Verify Architecture Match shell: bash if: steps.preparation.outputs.build == 'true' @@ -158,6 +177,10 @@ runs: target: ${{ inputs.docker-build-target }} build-args: ${{ inputs.docker-build-args }} tags: ${{ steps.preparation.outputs.tag_list }} + labels: | + ${{ inputs.deployment-labels == 'true' && format('{0}.repositoryFullName={1}', steps.deployment_labels.outputs.label_prefix, github.repository) || '' }} + ${{ inputs.deployment-labels == 'true' && format('{0}.commitSha={1}', steps.deployment_labels.outputs.label_prefix, github.sha) || '' }} + ${{ inputs.deployment-labels == 'true' && format('{0}.version={1}', steps.deployment_labels.outputs.label_prefix, steps.preparation.outputs.gitops_tag) || '' }} secrets: ${{ inputs.docker-build-secrets }} secret-files: ${{ inputs.docker-build-secret-files }} platforms: ${{ inputs.docker-build-platforms }} @@ -205,6 +228,8 @@ runs: INPUT_GITOPS_DEV: ${{ inputs.gitops-dev }} INPUT_GITOPS_STAGE: ${{ inputs.gitops-stage }} INPUT_GITOPS_PROD: ${{ inputs.gitops-prod }} + INPUT_DEPLOYMENT_ANNOTATIONS: ${{ inputs.deployment-annotations }} + INPUT_DEPLOYMENT_DOMAIN: ${{ inputs.deployment-domain }} run: ${{ github.action_path }}/scripts/update-gitops.sh - name: Emit Image Build Event to Upwind.io diff --git a/scripts/generate-label-prefix.sh b/scripts/generate-label-prefix.sh new file mode 100755 index 0000000..9af28be --- /dev/null +++ b/scripts/generate-label-prefix.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Computes the reverse-DNS key prefix for deployment-tracking image labels, +# derived from the deployment domain (deploy.staffbase.com -> com.staffbase.deploy). +# Emitted as the `label_prefix` output and consumed by the Build step's image +# `labels` input. The matching GitOps annotations use the forward "/..." +# form instead (see scripts/lib/gitops-functions.sh). +# +# Optional env vars: INPUT_DEPLOYMENT_DOMAIN (defaults to "deploy.staffbase.com") + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/common.sh +source "${SCRIPT_DIR}/lib/common.sh" + +set_output "label_prefix" "$(reverse_domain "${INPUT_DEPLOYMENT_DOMAIN:-deploy.staffbase.com}")" diff --git a/scripts/lib/common.sh b/scripts/lib/common.sh index f3ac081..ebfa422 100755 --- a/scripts/lib/common.sh +++ b/scripts/lib/common.sh @@ -30,8 +30,23 @@ set_output() { fi } -# --- Validation --- +# reverse_domain turns a dotted domain into reverse-DNS order +# (deploy.staffbase.com -> com.staffbase.deploy). Used to derive OCI image +# label keys from the same deployment domain used verbatim for GitOps annotations. +reverse_domain() { + local domain="$1" + local IFS='.' + local -a parts + read -ra parts <<< "$domain" + local out="" i + for (( i=${#parts[@]}-1; i>=0; i-- )); do + out+="${parts[i]}" + (( i > 0 )) && out+="." + done + printf '%s' "$out" +} +# --- Validation --- require_env() { local var_name="$1" if [[ -z "${!var_name:-}" ]]; then diff --git a/scripts/lib/gitops-functions.sh b/scripts/lib/gitops-functions.sh index 2b4f64b..d39a473 100755 --- a/scripts/lib/gitops-functions.sh +++ b/scripts/lib/gitops-functions.sh @@ -6,6 +6,8 @@ # INPUT_DOCKER_REGISTRY, INPUT_DOCKER_IMAGE, INPUT_TAG, INPUT_PUSH, # INPUT_GITOPS_USER, INPUT_GITOPS_TOKEN, # INPUT_GITOPS_ORGANIZATION, INPUT_GITOPS_REPOSITORY, +# INPUT_DEPLOYMENT_ANNOTATIONS (optional, defaults to "true"), +# INPUT_DEPLOYMENT_DOMAIN (optional, defaults to "deploy.staffbase.com"), # GITHUB_REPOSITORY, GITHUB_SHA, IMAGE push_to_gitops_repo() { @@ -48,10 +50,15 @@ update_file() { fi fi - echo "Writing deployment annotations to ${file}" - yq -i '.metadata.annotations["deploy.staffbase.com/repositoryFullName"] = "'"${GITHUB_REPOSITORY}"'"' "${file}" - yq -i '.metadata.annotations["deploy.staffbase.com/commitSha"] = "'"${GITHUB_SHA}"'"' "${file}" - yq -i '.metadata.annotations["deploy.staffbase.com/version"] = "'"${INPUT_TAG}"'"' "${file}" + if [[ "${INPUT_DEPLOYMENT_ANNOTATIONS:-true}" == "true" ]]; then + local domain="${INPUT_DEPLOYMENT_DOMAIN:-deploy.staffbase.com}" + echo "Writing deployment annotations to ${file}" + yq -i '.metadata.annotations["'"${domain}"'/repositoryFullName"] = "'"${GITHUB_REPOSITORY}"'"' "${file}" + yq -i '.metadata.annotations["'"${domain}"'/commitSha"] = "'"${GITHUB_SHA}"'"' "${file}" + yq -i '.metadata.annotations["'"${domain}"'/version"] = "'"${INPUT_TAG}"'"' "${file}" + else + echo "Deployment annotations disabled (INPUT_DEPLOYMENT_ANNOTATIONS != true); skipping" + fi } process_file_updates() { diff --git a/tests/generate-label-prefix.bats b/tests/generate-label-prefix.bats new file mode 100644 index 0000000..8e81441 --- /dev/null +++ b/tests/generate-label-prefix.bats @@ -0,0 +1,27 @@ +#!/usr/bin/env bats + +load 'test_helper/setup' + +SCRIPT="${BATS_TEST_DIRNAME}/../scripts/generate-label-prefix.sh" + +setup() { + setup_common +} + +teardown() { + teardown_common +} + +@test "label_prefix defaults to reversed deploy.staffbase.com" { + unset INPUT_DEPLOYMENT_DOMAIN + run "$SCRIPT" + assert_success + assert_output_value "label_prefix" "com.staffbase.deploy" +} + +@test "label_prefix reverses a custom deployment domain" { + export INPUT_DEPLOYMENT_DOMAIN="deploy.example.org" + run "$SCRIPT" + assert_success + assert_output_value "label_prefix" "org.example.deploy" +} diff --git a/tests/lib-common.bats b/tests/lib-common.bats index 3ec1d0a..47e0626 100644 --- a/tests/lib-common.bats +++ b/tests/lib-common.bats @@ -59,6 +59,26 @@ teardown() { assert_output "OUTPUT key=value" } +# --- reverse_domain --- + +@test "reverse_domain reverses a three-part domain" { + run reverse_domain "deploy.staffbase.com" + assert_success + assert_output "com.staffbase.deploy" +} + +@test "reverse_domain reverses an arbitrary domain" { + run reverse_domain "deploy.example.org" + assert_success + assert_output "org.example.deploy" +} + +@test "reverse_domain leaves a single segment unchanged" { + run reverse_domain "localhost" + assert_success + assert_output "localhost" +} + # --- require_env --- @test "require_env succeeds when variable is set" { diff --git a/tests/lib-gitops-functions.bats b/tests/lib-gitops-functions.bats index 635cbba..f402c1d 100644 --- a/tests/lib-gitops-functions.bats +++ b/tests/lib-gitops-functions.bats @@ -147,6 +147,46 @@ EOF ! grep -qE 'deploy\.staffbase\.com/(repo|sha)"' "${TEST_TEMP_DIR}/yq_calls.log" } +@test "update_file writes annotations when INPUT_DEPLOYMENT_ANNOTATIONS is unset (default on)" { + unset INPUT_DEPLOYMENT_ANNOTATIONS + update_file "deployment.yaml" "spec.image" "$IMAGE" + grep -q 'deploy.staffbase.com/repositoryFullName' "${TEST_TEMP_DIR}/yq_calls.log" +} + +@test "update_file skips annotations when INPUT_DEPLOYMENT_ANNOTATIONS is false" { + export INPUT_DEPLOYMENT_ANNOTATIONS="false" + update_file "deployment.yaml" "spec.image" "$IMAGE" + ! grep -q 'deploy.staffbase.com/' "${TEST_TEMP_DIR}/yq_calls.log" +} + +@test "update_file still updates the image field when annotations are disabled" { + cat > "${TEST_TEMP_DIR}/mocks/yq" << 'YQ_MOCK' +#!/usr/bin/env bash +echo "yq $*" >> "${MOCK_CALLS_DIR}/yq_calls.log" +if [[ "$*" == *"| type"* ]]; then echo "!!str"; fi +exit 0 +YQ_MOCK + chmod +x "${TEST_TEMP_DIR}/mocks/yq" + export INPUT_DEPLOYMENT_ANNOTATIONS="false" + update_file "deployment.yaml" "spec.image" "$IMAGE" + grep -q "${IMAGE}" "${TEST_TEMP_DIR}/yq_calls.log" +} + +@test "update_file uses default deploy.staffbase.com domain when unset" { + unset INPUT_DEPLOYMENT_DOMAIN + update_file "deployment.yaml" "spec.image" "$IMAGE" + grep -q 'deploy.staffbase.com/repositoryFullName' "${TEST_TEMP_DIR}/yq_calls.log" +} + +@test "update_file uses a custom INPUT_DEPLOYMENT_DOMAIN for annotation keys" { + export INPUT_DEPLOYMENT_DOMAIN="deploy.example.org" + update_file "deployment.yaml" "spec.image" "$IMAGE" + grep -q 'deploy.example.org/repositoryFullName' "${TEST_TEMP_DIR}/yq_calls.log" + grep -q 'deploy.example.org/commitSha' "${TEST_TEMP_DIR}/yq_calls.log" + grep -q 'deploy.example.org/version' "${TEST_TEMP_DIR}/yq_calls.log" + ! grep -q 'deploy.staffbase.com/' "${TEST_TEMP_DIR}/yq_calls.log" +} + # --- commit_changes --- @test "commit_changes commits and pushes when push is true" {