From f80c00ac0f705b908309ba962b78c1ecfdad7f6a Mon Sep 17 00:00:00 2001 From: Asad Khan Date: Thu, 28 May 2026 18:27:01 +0200 Subject: [PATCH 1/2] fix: auto-detect image field type to support staffbase-application chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The staffbase-application Helm chart splits the image into a mapping: image: repository: registry.staffbase.com/sb-images/app tag: dev-abc123 When gitops-github-action writes the full URI as a scalar to spec.values.workload.container.image, the kustomize patch replaces the mapping with a string. The chart then renders 'image: :' — both .image.repository and .image.tag resolve to empty. Fix: before writing, read the type of the target field using yq. - If the field is a mapping (!!map), write only INPUT_TAG to .field.tag - Otherwise, write the full image URI as before (Apperator pattern) Callers specifying spec.values.workload.container.image get the right behaviour automatically based on what is already in their overlay file. No changes to cicd.yml required. Backwards compatible: all existing Apperator-style callers have scalar image fields (!!str) and continue to receive the full URI unchanged. Assisted-by: pi:claude-sonnet-4-5 --- scripts/lib/gitops-functions.sh | 9 ++++++++- tests/fixtures/helmrelease.yaml | 13 +++++++++++++ tests/lib-gitops-functions.bats | 25 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/helmrelease.yaml diff --git a/scripts/lib/gitops-functions.sh b/scripts/lib/gitops-functions.sh index 9f88ce3..73ee110 100755 --- a/scripts/lib/gitops-functions.sh +++ b/scripts/lib/gitops-functions.sh @@ -36,7 +36,14 @@ update_file() { echo "Check if path ${file} ${field} exists and get old current version" yq -e ."${field}" "${file}" echo "Run update ${file} ${field} ${image}" - yq -i ."${field}"=\""${image}"\" "${file}" + local field_type + field_type=$(yq "(.${field} | type)" "${file}" 2>/dev/null || echo "!!null") + if [[ "${field_type}" == "!!map" ]]; then + echo "Field ${field} is a mapping — writing tag only to ${field}.tag" + yq -i ".${field}.tag=\"${INPUT_TAG}\"" "${file}" + else + yq -i ."${field}"=\""${image}"\" "${file}" + fi echo "Writing deployment annotations to ${file}" yq -i '.metadata.annotations["deploy.staffbase.com/repositoryFullName"] = "'"${GITHUB_REPOSITORY}"'"' "${file}" diff --git a/tests/fixtures/helmrelease.yaml b/tests/fixtures/helmrelease.yaml new file mode 100644 index 0000000..39de195 --- /dev/null +++ b/tests/fixtures/helmrelease.yaml @@ -0,0 +1,13 @@ +apiVersion: helm.toolkit.fluxcd.io/v2 +kind: HelmRelease +metadata: + name: my-service + namespace: my-service + annotations: {} +spec: + values: + workload: + container: + image: + repository: registry.staffbase.com/sb-images/my-service + tag: placeholder diff --git a/tests/lib-gitops-functions.bats b/tests/lib-gitops-functions.bats index aafa1bd..d96163f 100644 --- a/tests/lib-gitops-functions.bats +++ b/tests/lib-gitops-functions.bats @@ -51,6 +51,31 @@ teardown() { # --- update_file --- +@test "update_file writes tag only when field value is a mapping" { + cat > "${TEST_TEMP_DIR}/mocks/yq" << 'YQ_MOCK' +#!/usr/bin/env bash +echo "yq $*" >> "${MOCK_CALLS_DIR}/yq_calls.log" +if [[ "$*" == *"| type"* ]]; then echo "!!map"; fi +exit 0 +YQ_MOCK + chmod +x "${TEST_TEMP_DIR}/mocks/yq" + update_file "helmrelease.yaml" "spec.values.workload.container.image" "$IMAGE" + grep -q ".spec.values.workload.container.image.tag=\"${INPUT_TAG}\"" "${TEST_TEMP_DIR}/yq_calls.log" + ! grep -q "${IMAGE}" "${TEST_TEMP_DIR}/yq_calls.log" +} + +@test "update_file writes full URI when field value is a scalar" { + 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" + update_file "deployment.yaml" "spec.template.spec.containers.app.image" "$IMAGE" + grep -q "${IMAGE}" "${TEST_TEMP_DIR}/yq_calls.log" +} + @test "update_file calls yq to check and update field" { update_file "deployment.yaml" "spec.image" "$IMAGE" assert [ -f "${TEST_TEMP_DIR}/yq_calls.log" ] From fbfda1be8e843f2e86906b04db6cb99e871c3799 Mon Sep 17 00:00:00 2001 From: Asad Khan Date: Thu, 28 May 2026 20:18:47 +0200 Subject: [PATCH 2/2] refactor: handle all image field shapes via explicit and auto-detection Assisted-by: pi:claude-sonnet-4-5 --- scripts/lib/gitops-functions.sh | 15 +++++++++------ tests/lib-gitops-functions.bats | 25 ++++++++++++++++--------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/scripts/lib/gitops-functions.sh b/scripts/lib/gitops-functions.sh index 73ee110..5c4cf71 100755 --- a/scripts/lib/gitops-functions.sh +++ b/scripts/lib/gitops-functions.sh @@ -36,13 +36,16 @@ update_file() { echo "Check if path ${file} ${field} exists and get old current version" yq -e ."${field}" "${file}" echo "Run update ${file} ${field} ${image}" - local field_type - field_type=$(yq "(.${field} | type)" "${file}" 2>/dev/null || echo "!!null") - if [[ "${field_type}" == "!!map" ]]; then - echo "Field ${field} is a mapping — writing tag only to ${field}.tag" - yq -i ".${field}.tag=\"${INPUT_TAG}\"" "${file}" + 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}.tag" "${file}" > /dev/null 2>&1; then + yq -i ".${field}.tag=\"${INPUT_TAG}\"" "${file}" + else + yq -i ."${field}"=\""${image}"\" "${file}" + fi fi echo "Writing deployment annotations to ${file}" diff --git a/tests/lib-gitops-functions.bats b/tests/lib-gitops-functions.bats index d96163f..81fa67e 100644 --- a/tests/lib-gitops-functions.bats +++ b/tests/lib-gitops-functions.bats @@ -51,29 +51,36 @@ teardown() { # --- update_file --- -@test "update_file writes tag only when field value is a mapping" { +@test "update_file writes full URI when field value is a scalar" { cat > "${TEST_TEMP_DIR}/mocks/yq" << 'YQ_MOCK' #!/usr/bin/env bash echo "yq $*" >> "${MOCK_CALLS_DIR}/yq_calls.log" -if [[ "$*" == *"| type"* ]]; then echo "!!map"; fi +if [[ "$*" == *"| type"* ]]; then echo "!!str"; fi exit 0 YQ_MOCK chmod +x "${TEST_TEMP_DIR}/mocks/yq" - update_file "helmrelease.yaml" "spec.values.workload.container.image" "$IMAGE" - grep -q ".spec.values.workload.container.image.tag=\"${INPUT_TAG}\"" "${TEST_TEMP_DIR}/yq_calls.log" - ! grep -q "${IMAGE}" "${TEST_TEMP_DIR}/yq_calls.log" + update_file "deployment.yaml" "spec.template.spec.containers.app.image" "$IMAGE" + grep -q "${IMAGE}" "${TEST_TEMP_DIR}/yq_calls.log" + ! grep -q ".tag=\"${INPUT_TAG}\"" "${TEST_TEMP_DIR}/yq_calls.log" } -@test "update_file writes full URI when field value is a scalar" { +@test "update_file writes tag to .tag subfield when field is a map with tag property" { 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 +if [[ "$*" == *"| type"* ]]; then echo "!!map"; fi exit 0 YQ_MOCK chmod +x "${TEST_TEMP_DIR}/mocks/yq" - update_file "deployment.yaml" "spec.template.spec.containers.app.image" "$IMAGE" - grep -q "${IMAGE}" "${TEST_TEMP_DIR}/yq_calls.log" + update_file "helmrelease.yaml" "spec.values.workload.container.image" "$IMAGE" + grep -q ".spec.values.workload.container.image.tag=\"${INPUT_TAG}\"" "${TEST_TEMP_DIR}/yq_calls.log" + ! grep -q "${IMAGE}" "${TEST_TEMP_DIR}/yq_calls.log" +} + +@test "update_file writes tag only when field path ends with .tag" { + update_file "helmrelease.yaml" "spec.values.workload.container.image.tag" "$IMAGE" + grep -q ".spec.values.workload.container.image.tag=\"${INPUT_TAG}\"" "${TEST_TEMP_DIR}/yq_calls.log" + ! grep -q "${IMAGE}" "${TEST_TEMP_DIR}/yq_calls.log" } @test "update_file calls yq to check and update field" {