diff --git a/README.md b/README.md index 8280cb8..764c246 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,33 @@ Whenever the action updates a GitOps file, it stamps the following annotations o 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. +### Annotate-only mode (Flux image automation) + +Each `gitops-dev`/`gitops-stage`/`gitops-prod` line is normally ` `, where +`` is the yq path of the image reference to update. **Omit the field** (provide a +path only) and the action will write the `deploy.staffbase.com/*` annotations **without +touching the image tag**: + +```yaml +gitops-stage: |- + kubernetes/namespaces/my-service/stage/de1/my-service-helm.yaml +``` + +Use this for apps whose image tag is owned by +[Flux image automation](https://fluxcd.io/flux/components/image/) (ImageRepository / +ImagePolicy / ImageUpdateAutomation), which scans the registry and commits the `tag:` +line back to the GitOps repo itself. The action still needs to stamp the annotations, +because image automation only knows the tag — it never knows the source `commitSha` or +`repositoryFullName` that Swarmia needs for DORA metrics. This also yields a full commit +SHA in every environment, including prod, where image tags (e.g. `2025.50.14`) carry no SHA. + +> **Note:** the `version` annotation is set to the freshly built tag, so it may briefly +> lead the actual `tag:` until image automation selects the new build. It converges, and +> the reporter dedupes on `(commitSha, version)`, so it self-heals. + +The two-token ` ` form is unchanged: it updates the image **and** writes the +annotations, exactly as before. + ## Inputs | Name | Description | Default | @@ -137,15 +164,19 @@ These keys mirror the [Swarmia Deployment API](https://help.swarmia.com/settings | `docker-build-target` | Sets the target stage to build like: "runtime" | | | `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-build-outputs` | Custom output destinations (e.g. `type=registry,push=true,compression=zstd,force-compression=true`). When set, this replaces the default push behavior — include `push=true` if pushing is desired | | | `docker-disable-retagging` | Disables retagging of existing images and run a new build instead | `false` | | `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 | | +| `gitops-dev` | Files which should be updated by the GitHub Action for DEV, must be relative to the root of the GitOps repository. Each line is ` `; omit `` (path only) to write annotations without touching the image — see [Annotate-only mode](#annotate-only-mode-flux-image-automation) | | +| `gitops-stage` | Files which should be updated by the GitHub Action for STAGE, must be relative to the root of the GitOps repository. Each line is ` `; omit `` (path only) for [annotate-only mode](#annotate-only-mode-flux-image-automation) | | +| `gitops-prod` | Files which should be updated by the GitHub Action for PROD, must be relative to the root of the GitOps repository. Each line is ` `; omit `` (path only) for [annotate-only mode](#annotate-only-mode-flux-image-automation) | | +| `upwind-client-id` | Upwind Client ID | | +| `upwind-organization-id` | Upwind Organization ID | | +| `upwind-client-secret` | Upwind Client Secret | | | `working-directory` | The directory in which the GitOps action should be executed. The docker-file variable should be relative to working directory. | `.` | ## Outputs diff --git a/scripts/lib/gitops-functions.sh b/scripts/lib/gitops-functions.sh index 2b4f64b..839d3a1 100755 --- a/scripts/lib/gitops-functions.sh +++ b/scripts/lib/gitops-functions.sh @@ -33,19 +33,23 @@ update_file() { local field="$2" local image="$3" - echo "Check if path ${file} ${field} exists and get old current version" - yq -e ."${field}" "${file}" - echo "Run update ${file} ${field} ${image}" - if [[ "${field}" == *.tag ]]; then - yq -i ".${field}=\"${INPUT_TAG}\"" "${file}" - else - local field_type - field_type=$(yq "(.${field} | type)" "${file}" 2>/dev/null || echo "!!null") - if [[ "${field_type}" == "!!map" ]] && yq -e ".${field} | (has(\"tag\") or has(\"repository\"))" "${file}" > /dev/null 2>&1; then - yq -i ".${field}.tag=\"${INPUT_TAG}\"" "${file}" + if [[ -n "$field" ]]; then + echo "Check if path ${file} ${field} exists and get old current version" + yq -e ."${field}" "${file}" + echo "Run update ${file} ${field} ${image}" + if [[ "${field}" == *.tag ]]; then + yq -i ".${field}=\"${INPUT_TAG}\"" "${file}" else - yq -i ."${field}"=\""${image}"\" "${file}" + local field_type + field_type=$(yq "(.${field} | type)" "${file}" 2>/dev/null || echo "!!null") + if [[ "${field_type}" == "!!map" ]] && yq -e ".${field} | (has(\"tag\") or has(\"repository\"))" "${file}" > /dev/null 2>&1; then + yq -i ".${field}.tag=\"${INPUT_TAG}\"" "${file}" + else + yq -i ."${field}"=\""${image}"\" "${file}" + fi fi + else + echo "No field for ${file}; image tag owned by Flux image automation — writing annotations only" fi echo "Writing deployment annotations to ${file}" diff --git a/tests/lib-gitops-functions.bats b/tests/lib-gitops-functions.bats index 635cbba..ed97637 100644 --- a/tests/lib-gitops-functions.bats +++ b/tests/lib-gitops-functions.bats @@ -147,6 +147,63 @@ EOF ! grep -qE 'deploy\.staffbase\.com/(repo|sha)"' "${TEST_TEMP_DIR}/yq_calls.log" } +# --- update_file: annotate-only mode (empty field) --- + +@test "update_file with empty field skips image update" { + update_file "helmrelease.yaml" "" "" + # the field-existence check (yq -e .) only runs in field mode + ! grep -q 'yq -e .' "${TEST_TEMP_DIR}/yq_calls.log" + # no image/tag assignment written + ! grep -qE 'yq -i \.[^ ]*=' "${TEST_TEMP_DIR}/yq_calls.log" +} + +@test "update_file with empty field still writes all three annotations" { + update_file "helmrelease.yaml" "" "" + grep -q 'deploy.staffbase.com/repositoryFullName' "${TEST_TEMP_DIR}/yq_calls.log" + grep -q 'deploy.staffbase.com/commitSha' "${TEST_TEMP_DIR}/yq_calls.log" + grep -q "deploy.staffbase.com/version.*${INPUT_TAG}" "${TEST_TEMP_DIR}/yq_calls.log" +} + +@test "INTEGRATION: empty field annotates without touching the image tag" { + rm -rf "${TEST_TEMP_DIR}/mocks" + skip_if_no_yq + local test_file="${TEST_TEMP_DIR}/helmrelease.yaml" + cp "${BATS_TEST_DIRNAME}/fixtures/helmrelease.yaml" "$test_file" + + update_file "$test_file" "" "" + + # image tag is unchanged + run yq '.spec.values.workload.container.image.tag' "$test_file" + assert_output "placeholder" + + # annotations are stamped + run yq '.metadata.annotations["deploy.staffbase.com/repositoryFullName"]' "$test_file" + assert_output "$GITHUB_REPOSITORY" + run yq '.metadata.annotations["deploy.staffbase.com/commitSha"]' "$test_file" + assert_output "$GITHUB_SHA" + run yq '.metadata.annotations["deploy.staffbase.com/version"]' "$test_file" + assert_output "$INPUT_TAG" +} + +@test "INTEGRATION: provided field updates the tag AND annotates" { + rm -rf "${TEST_TEMP_DIR}/mocks" + skip_if_no_yq + local test_file="${TEST_TEMP_DIR}/helmrelease.yaml" + cp "${BATS_TEST_DIRNAME}/fixtures/helmrelease.yaml" "$test_file" + + update_file "$test_file" "spec.values.workload.container.image" "$IMAGE" + + # image tag updated + run yq '.spec.values.workload.container.image.tag' "$test_file" + assert_output "$INPUT_TAG" + + # annotations also stamped + run yq '.metadata.annotations["deploy.staffbase.com/commitSha"]' "$test_file" + assert_output "$GITHUB_SHA" + run yq '.metadata.annotations["deploy.staffbase.com/version"]' "$test_file" + assert_output "$INPUT_TAG" +} + # --- commit_changes --- @test "commit_changes commits and pushes when push is true" { @@ -196,6 +253,13 @@ file2.yaml spec.image" [[ "$yq_count" -eq 2 ]] } +@test "process_file_updates handles path-only line (annotate only)" { + process_file_updates "file1.yaml" "false" + # no field check performed, but annotations written + ! grep -q 'yq -e .' "${TEST_TEMP_DIR}/yq_calls.log" + grep -q 'deploy.staffbase.com/commitSha' "${TEST_TEMP_DIR}/yq_calls.log" +} + @test "process_file_updates commits when should_commit is true" { process_file_updates "file1.yaml spec.image" "true" grep -q 'git commit' "${TEST_TEMP_DIR}/git_calls.log"