hack/promote-packages-contract.bats
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bats
# Structural contract for publishing and verifying stable-candidate packages.
# The registry workflow cannot run in a PR lane, so ordering around the first
# write-once stable tag is pinned here. Behaviour lives in the two *_test suites.
load test_helper
REPO_ROOT="$(cd "$(dirname "${BATS_TEST_FILENAME:-$0}")/.." && pwd)"
PROMOTE="$REPO_ROOT/.github/workflows/promote-rc.yaml"
FINALIZE="$REPO_ROOT/.github/workflows/pull-requests-release.yaml"
PULL_REQUESTS="$REPO_ROOT/.github/workflows/pull-requests.yaml"
code_lines() {
rc=0
grep -v '^[[:space:]]*#' || rc=$?
[ "$rc" -le 1 ]
}
job_block() {
awk -v job=" $1:" '
$0 == job { inside = 1; next }
/^ [a-z0-9_-]+:$/ { inside = 0 }
inside' "$2"
}
step_block() {
awk -v want=" - name: $1" '
$0 == want { inside = 1; next }
/^ - name: / { inside = 0 }
inside' "$2"
}
step_line() {
code_lines < "$2" | grep -nF " - name: $1" | awk -F: 'NR == 1 { print $1 }'
}
@test "candidate publish and verification helpers exist and are executable" {
[ -x "$REPO_ROOT/hack/promote-packages-artifact.sh" ]
[ -x "$REPO_ROOT/hack/verify-promoted-packages.sh" ]
}
@test "promote commits the rewritten tree then publishes and commits its pin" {
block="$(step_block 'Prepare stable branch' "$PROMOTE")"
[ -n "$block" ]
rewrite="$(printf '%s\n' "$block" | code_lines | grep -nF '.release-tooling/hack/promote-rewrite-tags.sh' | awk -F: 'NR == 1 { print $1 }')"
publish="$(printf '%s\n' "$block" | code_lines | grep -nF '.release-tooling/hack/promote-packages-artifact.sh' | awk -F: 'NR == 1 { print $1 }')"
content_commit="$(printf '%s\n' "$block" | code_lines | grep -nF 'git commit -s -m "Prepare release' | awk -F: 'NR == 1 { print $1 }')"
pin_commit="$(printf '%s\n' "$block" | code_lines | grep -nF 'git commit -s -m "Pin promoted packages artifact' | awk -F: 'NR == 1 { print $1 }')"
[ -n "$rewrite" ] && [ -n "$content_commit" ] && [ -n "$publish" ] && [ -n "$pin_commit" ]
[ "$rewrite" -lt "$content_commit" ]
[ "$content_commit" -lt "$publish" ]
[ "$publish" -lt "$pin_commit" ]
# The helper comes from the dispatch ref's sparse tooling checkout, so the
# first release after this fix does not depend on the older rc tree carrying it.
printf '%s\n' "$block" | code_lines | grep -qF '.release-tooling/hack/promote-packages-artifact.sh'
printf '%s\n' "$block" | code_lines | grep -qF 'SOURCE_SHA="$(git rev-parse HEAD)"'
printf '%s\n' "$block" | code_lines | grep -qF 'PROMOTION_ID: ${{ github.run_id }}-${{ github.run_attempt }}'
}
@test "promote authenticates Flux with the same Docker config used by publish" {
job="$(job_block promote "$PROMOTE")"
login="$(step_block 'Login to registry (GHCR)' "$PROMOTE")"
prepare="$(step_block 'Prepare stable branch' "$PROMOTE")"
printf '%s\n' "$job" | code_lines | grep -qF ' packages: write'
printf '%s\n' "$job" | code_lines | grep -qF 'uses: fluxcd/flux2/action@'
printf '%s\n' "$login" | code_lines | grep -qF 'DOCKER_CONFIG: ${{ runner.temp }}/.docker'
printf '%s\n' "$prepare" | code_lines | grep -qF 'DOCKER_CONFIG: ${{ runner.temp }}/.docker'
}
@test "every promotion toolchain install pins its tools" {
# A release gate whose tooling comes from `releases/latest/download` is not a
# fixed input: the bytes that decide whether vX.Y.Z may be created, and the
# yq that rewrites the installer pin, change whenever upstream publishes.
# Pinned by version, and pinned to STAY pinned — an unpinned one-liner comes
# back by copy-paste rather than by anyone deciding to unpin a release step.
# flux is pinned by hack/ci-tool-installs-contract.bats across every workflow.
for step_and_file in \
"Set up toolchain (yq)|$PROMOTE" \
"Set up promotion toolchain (yq)|$PULL_REQUESTS" \
"Set up promotion toolchain (skopeo, yq, helm)|$FINALIZE"; do
block="$(step_block "${step_and_file%%|*}" "${step_and_file#*|}")"
[ -n "$block" ]
printf '%s\n' "$block" | code_lines | grep -qF 'YQ_VERSION: "'
count="$(printf '%s\n' "$block" | code_lines | grep -c 'releases/latest/download' || true)"
[ "${count:-0}" -eq 0 ]
# yq is pinned by content as well as by version: it is the one downloaded
# binary that WRITES what a release contains, stamping the candidate digest
# into installer values and platformVersion into the chart. A version
# variable authenticates nothing about the bytes it names.
printf '%s\n' "$block" | code_lines | grep -qF 'YQ_SHA256: "'
printf '%s\n' "$block" | code_lines | grep -qF 'sha256sum -c -'
done
# One version AND one checksum across all three, so the yq that writes the
# candidate's installer values is the yq that reads them back out of the
# artifact — and so bumping the version in one file without its checksum, or
# either without the other two, cannot pass.
for key in YQ_VERSION YQ_SHA256; do
values="$(cat "$PROMOTE" "$PULL_REQUESTS" "$FINALIZE" | code_lines \
| grep -o "$key: \"[0-9a-f.]*\"" | sort -u)"
[ -n "$values" ]
[ "$(printf '%s\n' "$values" | wc -l | tr -d ' ')" -eq 1 ]
done
}
@test "finalize verifies the pinned candidate before the first stable tag" {
verify="$(step_line 'Verify stable packages candidate' "$FINALIZE")"
create="$(step_line 'Create tag on merge commit (write-once)' "$FINALIZE")"
[ -n "$verify" ] && [ -n "$create" ]
[ "$verify" -lt "$create" ]
block="$(step_block 'Verify stable packages candidate' "$FINALIZE")"
printf '%s\n' "$block" | code_lines | grep -qF '.release-tooling/hack/verify-promoted-packages.sh "${TAG#v}"'
setup="$(step_block 'Set up flux' "$FINALIZE")"
printf '%s\n' "$setup" | code_lines | grep -qF 'uses: fluxcd/flux2/action@'
}
@test "promote rejects an old target base before publishing a candidate" {
validate="$(step_block 'Validate rc is promotable' "$PROMOTE")"
prepare_line="$(step_line 'Prepare stable branch' "$PROMOTE")"
validate_line="$(step_line 'Validate rc is promotable' "$PROMOTE")"
[ -n "$validate" ] && [ -n "$validate_line" ] && [ -n "$prepare_line" ]
[ "$validate_line" -lt "$prepare_line" ]
printf '%s\n' "$validate" | code_lines | grep -qF "['.github/workflows/pull-requests.yaml', 'verify-release-candidate:']"
printf '%s\n' "$validate" | code_lines | grep -qF "['.github/workflows/pull-requests-release.yaml', 'Verify stable packages candidate']"
printf '%s\n' "$validate" | code_lines | grep -qF "['hack/verify-promoted-packages.sh', 'EXPECTED_PACKAGES_REPOSITORY']"
# The verifier resolves its libraries relative to itself at run time, so a
# base carrying the script without them fails the gate it is meant to be.
# BOTH of them: the shared image-reference enumeration is sourced through the
# same `dirname $0` as the promoted-packages library, so a base missing it
# fails in the same place. Pinned per entry rather than only as a block —
# dropping any single pair used to leave every assertion here green.
printf '%s\n' "$validate" | code_lines | grep -qF "['hack/lib/image-refs.sh', 'collect_image_refs()']"
printf '%s\n' "$validate" | code_lines | grep -qF "['hack/lib/promoted-packages.sh', 'PACKAGES_DIGEST_REF_PATTERN']"
printf '%s\n' "$validate" | code_lines | grep -qF "core.setOutput('base_branch', baseBranch)"
open_pr="$(step_block 'Open promote (release) PR' "$PROMOTE")"
printf '%s\n' "$open_pr" | code_lines | grep -qF 'BASE_BRANCH: ${{ needs.promote.outputs.base_branch }}'
printf '%s\n' "$open_pr" | code_lines | grep -qF 'BASE="$BASE_BRANCH"'
}
@test "pull request gate verifies the prospective merge with trusted base tooling" {
job="$(job_block verify-release-candidate "$PULL_REQUESTS")"
[ -n "$job" ]
printf '%s\n' "$job" | code_lines | grep -qF "github.event.pull_request.user.login == 'cozystack-ci[bot]'"
printf '%s\n' "$job" | code_lines | grep -qF 'ref: ${{ github.event.pull_request.base.sha }}'
printf '%s\n' "$job" | code_lines | grep -qF '.release-tooling/hack/verify-promoted-packages.sh "$STABLE_VERSION"'
printf '%s\n' "$job" | code_lines | grep -qF 'uses: fluxcd/flux2/action@'
report="$(job_block e2e-report "$PULL_REQUESTS")"
printf '%s\n' "$report" | code_lines | grep -qF '"verify-release-candidate"'
printf '%s\n' "$report" | code_lines | grep -qF "IS_RELEASE === 'true' && CANDIDATE_RESULT !== 'success'"
# `e2e` is the other consumer, and the one that RUNS the suite. Reporting a
# red is a lesser guarantee than not running a full-e2e promotion suite
# against a candidate that did not verify, so pin both halves: the dependency
# and the clause on the release arm. Removing them together left every other
# assertion in this file green.
e2e="$(job_block e2e "$PULL_REQUESTS")"
[ -n "$e2e" ]
printf '%s\n' "$e2e" | code_lines | grep -qF '"verify-release-candidate"'
printf '%s\n' "$e2e" | code_lines | grep -qF 'needs.verify-release-candidate.result == '"'"'success'"'"''
# gh applies --label after opening a PR. The release label event must replace
# the initial run and publish a fresh status that includes candidate verify.
grep -qF "github.event.label.name != 'release'" "$PULL_REQUESTS"
plan="$(job_block plan "$PULL_REQUESTS")"
printf '%s\n' "$plan" | code_lines | grep -qF "github.event.label.name == 'release'"
printf '%s\n' "$report" | code_lines | grep -qF "github.event.label.name == 'release'"
}