Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 34 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<path> <field>`, where
`<field>` 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 `<path> <field>` form is unchanged: it updates the image **and** writes the
annotations, exactly as before.

## Inputs

| Name | Description | Default |
Expand All @@ -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 `<path> <field>`; omit `<field>` (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 `<path> <field>`; omit `<field>` (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 `<path> <field>`; omit `<field>` (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
Expand Down
26 changes: 15 additions & 11 deletions scripts/lib/gitops-functions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
64 changes: 64 additions & 0 deletions tests/lib-gitops-functions.bats
Original file line number Diff line number Diff line change
Expand Up @@ -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 .<field>) 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" {
Expand Down Expand Up @@ -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"
Expand Down
Loading