release: canary/x pointer-protection test + DeleteVersion race note
TestPruneEligibleProtectsCanaryBranchPointerAndHistory pins the TrimPrefix+CutSuffix nested-channel-name parsing in PruneEligible against a channel that itself contains a slash — production fleets run on canary/* today and nothing exercised the nested case. Verified non-vacuous by temporarily breaking the parse (skip names containing "/") and confirming the new test goes red before reverting. DeleteVersion's comment gets one more sentence: a concurrent re-ship of the same version name can interleave with a prune (Ship writes artifacts first, manifest last; DeleteVersion deletes manifest first from a pre-race listing), leaving a manifest-present/artifacts-missing state until the next tick re-deletes it — same accepted-TOCTOU family as the spec's promote-vs-prune ruling.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 files changed,
+38
−0
internal/release/prune.go+7 −0internal/release/prune_test.go+31 −0
diff --git a/internal/release/prune.go b/internal/release/prune.go| index 5091440cb..28f2e543a 100644 |
| --- a/internal/release/prune.go |
| +++ b/internal/release/prune.go |
| @@ -103,6 +103,13 @@ func PruneEligible(ctx context.Context, s Store, pub ed25519.PublicKey, acct, ap |
| // error row), never as a complete version with missing artifacts. Built |
| // from idempotent Deletes, so re-running after a partial failure — or |
| // racing another box — finishes the job instead of erroring. |
| +// |
| +// A concurrent re-ship of the same version name can still interleave here: |
| +// Ship writes artifacts first and the manifest last, while DeleteVersion |
| +// deletes the manifest first from a pre-race listing — the window can leave |
| +// a manifest-present/artifacts-missing state until the next prune tick |
| +// re-deletes it. Same accepted-TOCTOU family as the spec's promote-vs-prune |
| +// ruling. |
| func DeleteVersion(ctx context.Context, s Store, acct, app, version string) error { |
| if !ValidAccount(acct) { |
| return fmt.Errorf("delete version: invalid account %q", acct) |
diff --git a/internal/release/prune_test.go b/internal/release/prune_test.go| index 2fa121555..25a2f2f0f 100644 |
| --- a/internal/release/prune_test.go |
| +++ b/internal/release/prune_test.go |
| @@ -129,6 +129,37 @@ func TestPruneEligibleProtectsPointerVersionAndHistory(t *testing.T) { |
| } |
| } |
| +func TestPruneEligibleProtectsCanaryBranchPointerAndHistory(t *testing.T) { |
| + _, s, now := pruneFixture(t, 25) |
| + seedB64, pubB64, _ := GenerateKey() |
| + priv, _ := ParseSigningKey(seedB64, "test") |
| + pub, _ := ParsePublicKey(pubB64, "test") |
| + ctx := context.Background() |
| + // Promote v01 then v02 onto the nested channel name canary/x: pointer |
| + // names v02, History carries v01. Pins the TrimPrefix+CutSuffix parsing |
| + // in PruneEligible against a channel name that itself contains a slash — |
| + // production fleets run on canary/* channels today. |
| + if err := Promote(ctx, s, priv, "bab", "kass", "canary/x", "v01"); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := Promote(ctx, s, priv, "bab", "kass", "canary/x", "v02"); err != nil { |
| + t.Fatal(err) |
| + } |
| + got, err := PruneEligible(ctx, s, pub, "bab", "kass", 20, now, nil) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + for _, v := range got { |
| + if v == "v01" || v == "v02" { |
| + t.Fatalf("eligible = %v, want v01 and v02 protected (canary/x pointer + history)", got) |
| + } |
| + } |
| + want := []string{"v03", "v04", "v05"} |
| + if strings.Join(got, ",") != strings.Join(want, ",") { |
| + t.Fatalf("eligible = %v, want %v (canary/x pointer + history protected)", got, want) |
| + } |
| +} |
| + |
| func TestPruneEligibleProtectsTagReleases(t *testing.T) { |
| _, s, now := pruneFixture(t, 25) |
| _, pubB64, _ := GenerateKey() |