diff --git a/.github/workflows/reusable--release--build.yaml b/.github/workflows/reusable--release--build.yaml index b06bfdb..6bbb814 100644 --- a/.github/workflows/reusable--release--build.yaml +++ b/.github/workflows/reusable--release--build.yaml @@ -55,7 +55,7 @@ jobs: TARGETS: ${{ steps.resolver.outputs.targets }} run: | set -euo pipefail - dir=$(jq -r --arg service "$SERVICE" 'first(.[] | select(.type == "container") | select(.service == $service)) | .working_directory' <<<"$TARGETS") + dir=$(jq -r 'first(.[] | select(.type == "container")) | .working_directory' <<<"$TARGETS") if [ -z "$dir" ]; then echo "::error::No directory found for service '$SERVICE' among the container targets of workflow-config.yaml" exit 1 diff --git a/.github/workflows/reusable--test-terragrunt-release.yaml b/.github/workflows/reusable--test-terragrunt-release.yaml deleted file mode 100644 index 17f2523..0000000 --- a/.github/workflows/reusable--test-terragrunt-release.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# Validates the release-please configs of the caller's production environments. -name: 'Reusable - Test Terragrunt Release' - -on: - workflow_call: - -jobs: - validate: - name: 'Validate terragrunt release config' - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Checkout - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Checkout shared workflows - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: ${{ job.workflow_repository }} - ref: ${{ job.workflow_sha }} - path: .shared-workflows - persist-credentials: false - - - name: Resolve release candidates - id: resolver - uses: panicboat/deploy-actions/label-resolver@9c5aea836b982c12d283c4bea8fac965d7242eda # v0.1.0 - with: - repository: ${{ github.repository }} - service: all - - - name: Check release-please configs - env: - CANDIDATES: ${{ steps.resolver.outputs.candidates }} - run: | - set -euo pipefail - printf '%s\n' "${CANDIDATES}" > "${RUNNER_TEMP}/candidates.json" - bash .shared-workflows/scripts/terragrunt-release/check-release-configs.sh "${RUNNER_TEMP}/candidates.json" diff --git a/scripts/terragrunt-release/check-release-configs.sh b/scripts/terragrunt-release/check-release-configs.sh deleted file mode 100755 index 7826cc5..0000000 --- a/scripts/terragrunt-release/check-release-configs.sh +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [ "$#" -ne 1 ]; then - echo "::error::usage: check-release-configs.sh " >&2 - exit 1 -fi -candidates_path="$1" - -failed=0 - -report() { - echo "::error::$*" >&2 - failed=1 -} - -if ! jq -e 'type == "array" and all(.[]; type == "object" and (.target | type == "object"))' "${candidates_path}" >/dev/null 2>&1; then - echo "::error::${candidates_path} must hold the label resolver candidates as a JSON array of objects with a target object" >&2 - exit 1 -fi - -# Excluded candidates still need a release-please config, so they stay in the release set. -release_candidates="$(jq -c '[.[] | select(.target.type == "terragrunt" and .target.environment == "production")]' "${candidates_path}")" - -incomplete="$(jq -r ' - .[] | select(.excluded | not) | .target as $target - | ("aws_region", "iam_role_plan", "iam_role_apply") as $field - | select(($target[$field] // "") == "") - | "::error::release target \($target.working_directory) has no \($field)" -' <<<"${release_candidates}")" -if [ -n "${incomplete}" ]; then - echo "${incomplete}" >&2 - failed=1 -fi - -release_packages="$(jq -r 'map([.target.service, .root]) | unique[] | @tsv' <<<"${release_candidates}")" - -while IFS=$'\t' read -r service package; do - [ -n "${service}" ] || continue - config_file="${package}/release-please-config.json" - manifest_file="${package}/release-please-manifest.json" - - if [ ! -f "${config_file}" ]; then - report "${config_file} is missing but ${service} has a release environment" - continue - fi - - component="$(jq -r --arg package "${package}" '.packages[$package].component // ""' "${config_file}")" - if [ "${component}" != "${service}" ]; then - report "${package}: component must be ${service} (got '${component}')" - fi - - if [ ! -f "${manifest_file}" ] || ! jq -e --arg package "${package}" 'has($package)' "${manifest_file}" >/dev/null; then - report "${manifest_file} must have the key ${package}" - fi -done <<<"${release_packages}" - -components="" -config_files="$(git ls-files '*/release-please-config.json')" -while IFS= read -r config_file; do - [ -n "${config_file}" ] || continue - packages="$(jq -r '.packages // {} | to_entries[] | [.key, (.value.component // "")] | @tsv' "${config_file}")" - while IFS=$'\t' read -r package component; do - [ -n "${package}" ] || continue - if [ -n "${component}" ]; then - components+="${component}"$'\t'"${package}"$'\n' - fi - done <<<"${packages}" -done <<<"${config_files}" - -# A release tag names only the component, so a component shared by two packages would release both. -duplicates="$(jq -R -s -r ' - split("\n") | map(select(. != "") | split("\t")) | group_by(.[0])[] - | select(length > 1) - | "::error::component \(.[0][0]) is used by more than one package: \(map(.[1]) | join(" "))" -' <<<"${components}")" -if [ -n "${duplicates}" ]; then - echo "${duplicates}" >&2 - failed=1 -fi - -exit "${failed}" diff --git a/scripts/terragrunt-release/test.sh b/scripts/terragrunt-release/test.sh index a631699..f8ff091 100755 --- a/scripts/terragrunt-release/test.sh +++ b/scripts/terragrunt-release/test.sh @@ -83,156 +83,6 @@ test_release_targets_rejects_empty_stdin() { assert_fails bash "${script_dir}/release-targets.sh" "${package}/release-please-config.json" < "${package}/release-please-manifest.json" -} - -terragrunt_candidate() { - local service="$1" root="$2" excluded="${3:-false}" environment="${4:-production}" - jq -nc --arg service "${service}" --arg root "${root}" --arg environment "${environment}" --argjson excluded "${excluded}" '{ - target: { - service: $service, - environment: $environment, - stack: "terragrunt", - working_directory: "\($root)/infrastructure/aws/\($environment)", - type: "terragrunt", - aws_region: "ap-northeast-1", - iam_role_plan: "arn:aws:iam::222222222222:role/\($environment)-plan", - iam_role_apply: "arn:aws:iam::222222222222:role/\($environment)-apply" - }, - excluded: $excluded, - root: $root - }' -} - -write_candidates() { - printf '%s\n' "$@" | jq -sc '.' > candidates.json -} - -check_configs() { - git init -q - git add -A - bash "${script_dir}/check-release-configs.sh" candidates.json -} - -test_check_configs_accepts_matching_set() { - write_candidates "$(terragrunt_candidate monolith dystopia/monolith)" - write_service_release_config dystopia/monolith - write_service_release_config dystopia/frontend - assert_eq "" "$(check_configs 2>&1)" -} - -test_check_configs_rejects_missing_config() { - write_candidates "$(terragrunt_candidate pennyworth system-components/pennyworth)" - assert_fails check_configs - assert_stderr_contains "system-components/pennyworth/release-please-config.json" -} - -test_check_configs_rejects_wrong_component() { - write_candidates "$(terragrunt_candidate monolith dystopia/monolith)" - write_service_release_config dystopia/monolith frontend - assert_fails check_configs - assert_stderr_contains "dystopia/monolith: component must be monolith (got 'frontend')" -} - -test_check_configs_rejects_manifest_without_package() { - write_candidates "$(terragrunt_candidate monolith dystopia/monolith)" - write_service_release_config dystopia/monolith - echo '{}' > dystopia/monolith/release-please-manifest.json - assert_fails check_configs - assert_stderr_contains "dystopia/monolith/release-please-manifest.json" - rm dystopia/monolith/release-please-manifest.json - assert_fails check_configs - assert_stderr_contains "dystopia/monolith/release-please-manifest.json" -} - -test_check_configs_rejects_incomplete_release_env() { - write_candidates "$(terragrunt_candidate monolith dystopia/monolith | jq -c '.target |= with_entries(select(.key | IN("aws_region", "iam_role_plan", "iam_role_apply") | not))')" - write_service_release_config dystopia/monolith - assert_fails check_configs - assert_stderr_contains "dystopia/monolith/infrastructure/aws/production" - assert_stderr_contains "aws_region" - assert_stderr_contains "iam_role_plan" - assert_stderr_contains "iam_role_apply" -} - -test_check_configs_skips_incomplete_excluded_env() { - write_candidates "$(terragrunt_candidate monolith dystopia/monolith true | jq -c '.target.aws_region = ""')" - write_service_release_config dystopia/monolith - assert_eq "" "$(check_configs 2>&1)" -} - -test_check_configs_requires_config_for_excluded_env() { - write_candidates "$(terragrunt_candidate monolith dystopia/monolith true)" - assert_fails check_configs - assert_stderr_contains "dystopia/monolith/release-please-config.json" - write_service_release_config dystopia/monolith - assert_eq "" "$(check_configs 2>&1)" -} - -test_check_configs_ignores_candidates_that_are_not_terragrunt_production() { - write_candidates \ - "$(terragrunt_candidate monolith dystopia/monolith false develop)" \ - "$(jq -nc '{target: {service: "frontend", stack: "container", working_directory: "dystopia/frontend", type: "container"}, excluded: false, root: "dystopia/frontend"}')" \ - "$(terragrunt_candidate pennyworth system-components/pennyworth | jq -c '.target.environment = "staging"')" - assert_eq "" "$(check_configs 2>&1)" -} - -test_check_configs_rejects_candidates_that_are_not_an_array() { - echo '' > candidates.json - assert_fails check_configs - assert_stderr_contains "::error::" - echo '{}' > candidates.json - assert_fails check_configs - assert_stderr_contains "::error::" -} - -test_check_configs_rejects_missing_candidates_path() { - assert_fails bash "${script_dir}/check-release-configs.sh" - assert_stderr_contains "::error::usage: check-release-configs.sh " -} - -test_check_configs_rejects_candidates_that_are_not_objects_with_a_target() { - local candidates - for candidates in '[1]' '[null]' '[{"target":1}]' '[{"root":"dystopia/monolith"}]'; do - echo "${candidates}" > candidates.json - assert_fails check_configs - assert_stderr_contains "::error::" - assert_eq "1" "$(printf '%s\n' "${last_stderr}" | wc -l | tr -d ' ')" - done -} - -test_check_configs_ignores_exclude_paths() { - write_candidates "$(terragrunt_candidate monolith dystopia/monolith)" - write_service_release_config dystopia/monolith monolith '["unrelated/path"]' - assert_eq "" "$(check_configs 2>&1)" - jq -c 'del(.packages["dystopia/monolith"]["exclude-paths"])' dystopia/monolith/release-please-config.json > config.tmp - mv config.tmp dystopia/monolith/release-please-config.json - assert_eq "" "$(check_configs 2>&1)" -} - -test_check_configs_ignores_untracked_configs() { - write_candidates - write_service_release_config dystopia/frontend - git init -q - git add -A - write_service_release_config node_modules/pkg pkg '[]' - assert_eq "" "$(bash "${script_dir}/check-release-configs.sh" candidates.json 2>&1)" -} - -test_check_configs_rejects_duplicate_service() { - write_candidates - write_service_release_config dystopia/monolith - write_service_release_config system-components/monolith - assert_fails check_configs - assert_stderr_contains "component monolith is used by more than one package: dystopia/monolith system-components/monolith" -} - failed=0 for test_name in $(compgen -A function test_); do fixture="$(mktemp -d "${work_root}/fixture.XXXXXX")" diff --git a/scripts/workflows/test.sh b/scripts/workflows/test.sh index 0bde772..20ddf11 100755 --- a/scripts/workflows/test.sh +++ b/scripts/workflows/test.sh @@ -158,10 +158,10 @@ test_release_build_resolves_the_container_directory_with_the_label_resolver() { work="$(mktemp -d)" trap "rm -rf '${work}'" EXIT - targets='[{"service":"api","environment":"develop","stack":"terragrunt","working_directory":"terragrunt/api/develop","type":"terragrunt"},{"service":"web","stack":"container","working_directory":"services/web","type":"container"},{"service":"api","stack":"container","working_directory":"services/api","type":"container"}]' + targets='[{"service":"api","environment":"develop","stack":"terragrunt","working_directory":"terragrunt/api/develop","type":"terragrunt"},{"service":"api","stack":"container","working_directory":"services/api","type":"container"}]' GITHUB_OUTPUT="${work}/output" SERVICE=api TARGETS="${targets}" bash -c "${run}" assert_eq "working-directory=services/api" "$(cat "${work}/output")" - for targets in "${targets}" '[]'; do + for targets in '[{"service":"billing","environment":"develop","stack":"terragrunt","working_directory":"terragrunt/billing/develop","type":"terragrunt"}]' '[]'; do status=0 out="$(GITHUB_OUTPUT="${work}/none" SERVICE=billing TARGETS="${targets}" bash -c "${run}")" || status=$? assert_eq "1" "${status}" @@ -170,44 +170,6 @@ test_release_build_resolves_the_container_directory_with_the_label_resolver() { done } -test_release_config_check_uses_the_label_resolver() { - local workflow="${workflows_dir}/reusable--test-terragrunt-release.yaml" - local pin_pattern='^panicboat/deploy-actions/label-resolver@[0-9a-f]{40} # v[0-9]+\.[0-9]+\.[0-9]+$' - local steps='.jobs.validate.steps[]' - local pin step run work fixture status stderr - pin="$(grep -o 'panicboat/deploy-actions/label-resolver@.*' "${workflow}")" - [[ "${pin}" =~ ${pin_pattern} ]] || { echo " unpinned: ${pin}" >&2; exit 1; } - assert_eq "$(grep -o 'panicboat/deploy-actions/label-resolver@.*' "${workflows_dir}/reusable--label-base-deploy--resolver.yaml")" "${pin}" - assert_eq '{"contents":"read"}' "$(yq -o=json -I=0 '.jobs.validate.permissions' "${workflow}")" - assert_eq "1" "$(yq "[${steps} | select(.uses == \"actions/checkout@*\" and .with.path == null)] | length" "${workflow}")" - - step="$(yq "${steps} | select(.id == \"resolver\")" "${workflow}")" - assert_eq '${{ github.repository }}' "$(yq '.with.repository' <<<"${step}")" - assert_eq 'all' "$(yq '.with.service' <<<"${step}")" - assert_eq 'null' "$(yq '.with.environments' <<<"${step}")" - - step="$(yq "${steps} | select(.name == \"Check release-please configs\")" "${workflow}")" - run="$(yq '.run' <<<"${step}")" - assert_eq '${{ steps.resolver.outputs.candidates }}' "$(yq '.env.CANDIDATES' <<<"${step}")" - assert_eq "0" "$(grep -c '\${{' <<<"${run}" || true)" - grep -qF 'check-release-configs.sh' <<<"${run}" - - work="$(mktemp -d)" - trap "rm -rf '${work}'" EXIT - fixture="${work}/caller" - mkdir -p "${fixture}/dystopia/monolith" "${work}/temp" - ln -s "${repo_root}" "${fixture}/.shared-workflows" - echo '{"packages":{"dystopia/monolith":{"component":"monolith"}}}' > "${fixture}/dystopia/monolith/release-please-config.json" - echo '{"dystopia/monolith":"0.1.0"}' > "${fixture}/dystopia/monolith/release-please-manifest.json" - git -C "${fixture}" init -q - git -C "${fixture}" add dystopia - (cd "${fixture}" && RUNNER_TEMP="${work}/temp" CANDIDATES='[{"target":{"service":"monolith","environment":"production","stack":"terragrunt","working_directory":"dystopia/monolith/production","type":"terragrunt","aws_region":"ap-northeast-1","iam_role_plan":"p","iam_role_apply":"a"},"excluded":false,"root":"dystopia/monolith"},{"target":{"service":"monolith","environment":"develop","stack":"terragrunt","working_directory":"dystopia/monolith/develop","type":"terragrunt","aws_region":"ap-northeast-1","iam_role_plan":"p","iam_role_apply":"a"},"excluded":false,"root":"dystopia/develop-only"}]' bash -c "${run}") - status=0 - stderr="$(cd "${fixture}" && RUNNER_TEMP="${work}/temp" CANDIDATES='[{"target":{"service":"pennyworth","environment":"production","stack":"terragrunt","working_directory":"p/production","type":"terragrunt","aws_region":"r","iam_role_plan":"p","iam_role_apply":"a"},"excluded":true,"root":"system-components/pennyworth"}]' bash -c "${run}" 2>&1 >/dev/null)" || status=$? - assert_eq "1" "${status}" - grep -qF 'system-components/pennyworth/release-please-config.json' <<<"${stderr}" || { echo " stderr does not name the missing config: ${stderr}" >&2; exit 1; } -} - test_kustomize_builder_checks_out_head_ref_only_for_helmfile() { local resolver="${workflows_dir}/reusable--label-base-deploy--resolver.yaml" local kustomize_builder="${workflows_dir}/internal--kustomize-builder.yaml"