diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 0e50a32a88..f32d14a9d5 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -336,7 +336,8 @@ TLS floor in the dynamic config. itself is expected to finish much earlier. Tarball entry validation must extract the requested files once per archive; it must not decompress a multi-gigabyte release archive again for every - required entry. + required entry, and the release-promotion contract test must reject a return + to per-entry archive streaming. A manually dispatched release rehearsal must activate the same signed candidate build whenever its required `version` input is non-empty. Scheduled watchdog rehearsals omit that input and must skip candidate diff --git a/scripts/release_control/release_promotion_policy_test.py b/scripts/release_control/release_promotion_policy_test.py index c077f33366..dcb28e368a 100644 --- a/scripts/release_control/release_promotion_policy_test.py +++ b/scripts/release_control/release_promotion_policy_test.py @@ -533,6 +533,7 @@ class ReleasePromotionPolicyTest(unittest.TestCase): demo_reachability_helper = read(".github/scripts/check-demo-reachability.sh") validation_workflow = read(".github/workflows/validate-release-assets.yml") candidate_workflow = read(".github/workflows/build-release-candidate.yml") + release_validator = read("scripts/validate-release.sh") helper = read("scripts/trigger-release.sh") renderer = read("scripts/release_control/render_release_body.py") policy = read("docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md") @@ -619,6 +620,8 @@ class ReleasePromotionPolicyTest(unittest.TestCase): self.assertIn("PULSE_UPDATE_SIGNING_PUBLIC_KEY=${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}", content) self.assertIn("Validate installer signing key pins", candidate_workflow) self.assertIn("timeout-minutes: 60", candidate_workflow) + self.assertIn('tar -xzf "$tarball" -C "$extract_dir" -- "$@"', release_validator) + self.assertNotIn('tar -xOf "$tarball" "$entry"', release_validator) self.assertIn("go run ./scripts/release_update_key.go public-key-ssh", candidate_workflow) self.assertIn("does not trust the configured release signing key", candidate_workflow) self.assertIn("TRUSTED_SSH_PUBLIC_KEY", update_demo_workflow) diff --git a/scripts/validate-release.sh b/scripts/validate-release.sh index 2ac8ea258a..d91011c6f7 100755 --- a/scripts/validate-release.sh +++ b/scripts/validate-release.sh @@ -52,24 +52,26 @@ with_network_blocked() { check_tar_entries_nonempty() { local tarball="$1" shift + local extract_dir + extract_dir="$(mktemp -d "$tmp_root/tar-entries.XXXXXX")" + + if ! tar -xzf "$tarball" -C "$extract_dir" -- "$@"; then + error "$(basename "$tarball") is missing one or more required entries" + exit 1 + fi + for entry in "$@"; do - if ! tar -tzf "$tarball" "$entry" >/dev/null 2>&1; then - error "$(basename "$tarball") missing entry: $entry" - exit 1 - fi - # Examine type; skip size enforcement for symlinks - local type - type=$(tar -tvf "$tarball" "$entry" 2>/dev/null | awk 'NR==1 {print substr($0,1,1)}') - if [ "$type" = "l" ]; then + local extracted_path="$extract_dir/${entry#./}" + if [ -L "$extracted_path" ]; then continue fi - local size - size=$(tar -xOf "$tarball" "$entry" 2>/dev/null | wc -c | tr -d '[:space:]') - if [ -z "$size" ] || [ "$size" -le 0 ]; then + if [ ! -s "$extracted_path" ]; then error "$(basename "$tarball") missing or empty entry: $entry" exit 1 fi done + + rm -rf "$extract_dir" } http_header_value() {