From a27350378e0b8164bdd9a68fb70e928052607cb2 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Thu, 9 Jul 2026 23:28:02 +0100 Subject: [PATCH] Validate release archives in one pass --- .../subsystems/deployment-installability.md | 3 ++- internal/agentupdate/coverage_test.go | 15 ------------ internal/agentupdate/restart_unix_test.go | 23 ++++++++++++++++++ .../release_promotion_policy_test.py | 3 +++ scripts/validate-release.sh | 24 ++++++++++--------- 5 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 internal/agentupdate/restart_unix_test.go 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/internal/agentupdate/coverage_test.go b/internal/agentupdate/coverage_test.go index 8abf3924e3..1215785ded 100644 --- a/internal/agentupdate/coverage_test.go +++ b/internal/agentupdate/coverage_test.go @@ -168,21 +168,6 @@ func TestRunDownloadedBinarySelfTestPropagatesFailureWithoutTokenArg(t *testing. } } -func TestRestartProcess(t *testing.T) { - orig := execFn - t.Cleanup(func() { execFn = orig }) - - execFn = func(string, []string, []string) error { return nil } - if err := restartProcess("/bin/true"); err != nil { - t.Fatalf("expected nil error, got %v", err) - } - - execFn = func(string, []string, []string) error { return errors.New("boom") } - if err := restartProcess("/bin/false"); err == nil { - t.Fatalf("expected error") - } -} - func TestDetermineArchOverrides(t *testing.T) { origOS, origArch, origUname := runtimeGOOS, runtimeGOARCH, unameCommand t.Cleanup(func() { diff --git a/internal/agentupdate/restart_unix_test.go b/internal/agentupdate/restart_unix_test.go new file mode 100644 index 0000000000..d74f1d0092 --- /dev/null +++ b/internal/agentupdate/restart_unix_test.go @@ -0,0 +1,23 @@ +//go:build !windows + +package agentupdate + +import ( + "errors" + "testing" +) + +func TestRestartProcess(t *testing.T) { + orig := execFn + t.Cleanup(func() { execFn = orig }) + + execFn = func(string, []string, []string) error { return nil } + if err := restartProcess("/bin/true"); err != nil { + t.Fatalf("expected nil error, got %v", err) + } + + execFn = func(string, []string, []string) error { return errors.New("boom") } + if err := restartProcess("/bin/false"); err == nil { + t.Fatal("expected error") + } +} 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() {