carlos / platform Public

settings-2 task 5: pruning and purging remove promotion records

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 21096648a361a546786275c1a930cb2b0b7fcd0d parent 4fb51f7
4 files changed, +71 −1
  • internal/release/promotion.go +4 −1
  • internal/release/prune.go +6 −0
  • internal/release/record_lifecycle_test.go +53 −0
  • internal/release/trash.go +8 −0
diff --git a/internal/release/promotion.go b/internal/release/promotion.go
index ed73eef25..25bdc721e 100644
--- a/internal/release/promotion.go
+++ b/internal/release/promotion.go
@@ -11,11 +11,14 @@ import (
"time"
)
+// PromotedPrefix is every promotion record of an app: what PurgeApp sweeps.
+func PromotedPrefix(acct, app string) string { return "releases/" + acct + "/" + app + "/promoted/" }
+
// PromotionRecordKey is the durable record of one promoted build (spec §4.1).
// It sits outside AppPrefix, beside the account's pointer digest marker, so
// PurgeApp sweeps it explicitly.
func PromotionRecordKey(acct, app, version string) string {
- return "releases/" + acct + "/" + app + "/promoted/" + version
+ return PromotedPrefix(acct, app) + version
}
// PromotionRecord binds a version name to the exact manifest that was
diff --git a/internal/release/prune.go b/internal/release/prune.go
index ca41da232..9fe60d0dc 100644
--- a/internal/release/prune.go
+++ b/internal/release/prune.go
@@ -13,6 +13,7 @@ import (
"crypto/ed25519"
"errors"
"fmt"
+ "os"
"strings"
"time"
)
@@ -137,6 +138,11 @@ func claimRawDeleteVersion(ctx context.Context, s Store, acct, app, version stri
return err
}
}
+ // The version's promotion record goes with it (spec §4.4 keeps only a
+ // protected build's record). Idempotent like every delete here.
+ if err := s.Delete(ctx, PromotionRecordKey(acct, app, version)); err != nil && !errors.Is(err, os.ErrNotExist) {
+ return err
+ }
return nil
}
func DeleteVersion(ctx context.Context, s Store, acct, app, version string) (claimResult0 error) {
diff --git a/internal/release/record_lifecycle_test.go b/internal/release/record_lifecycle_test.go
new file mode 100644
index 000000000..6729a71b4
--- /dev/null
+++ b/internal/release/record_lifecycle_test.go
@@ -0,0 +1,53 @@
+package release
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+)
+
+// Review Focus 5.
+func TestPruneThenReshipGetsAFreshRecord(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if err := DeleteVersion(ctx, s, "bab", "km", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := ReadPromotionRecord(ctx, s, pub, "bab", "km", "v1"); !errors.Is(err, ErrNoPromotionRecord) {
+ t.Fatalf("the pruned version's record survived: %v", err)
+ }
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one again, other bytes"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/y", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ if _, err := AuthorizedManifest(ctx, s, pub, "bab", "km", "v1"); err != nil {
+ t.Fatalf("the reshipped, promoted name has no valid record: %v", err)
+ }
+}
+
+func TestPurgeRemovesEveryRecord(t *testing.T) {
+ s := NewDirStore(t.TempDir())
+ ctx := context.Background()
+ priv, pub := testKeys(t)
+ shipRelease(t, s, "bab", "km", "v1", adoptPlatform, []byte("one"))
+ if err := Promote(ctx, s, priv, "bab", "km", "canary/x", "v1"); err != nil {
+ t.Fatal(err)
+ }
+ // Make the app purgeable: trash it and backdate the tombstone
+ if err := TrashApp(ctx, s, "bab", "km", "paul@keymail.dev", time.Now()); err != nil {
+ t.Fatal(err)
+ }
+ if err := PurgeApp(ctx, s, pub, "bab", "km", time.Now().Add(31*24*time.Hour)); err != nil {
+ t.Fatal(err)
+ }
+ keys, _ := s.List(ctx, PromotedPrefix("bab", "km"))
+ if len(keys) != 0 {
+ t.Fatalf("records survived the purge: %v", keys)
+ }
+}
diff --git a/internal/release/trash.go b/internal/release/trash.go
index 62cbeed62..797f09e60 100644
--- a/internal/release/trash.go
+++ b/internal/release/trash.go
@@ -286,6 +286,14 @@ func purgeApp(ctx context.Context, s Store, pub ed25519.PublicKey, acct, app str
return fmt.Errorf("release: list %s: %w", LedgerAppPrefix(acct, app), err)
}
keys = append(keys, ledgerKeys...)
+ // The app's promotion records (settings spec §4.1) live outside its
+ // prefix too, for the same reason as the ledgers: a new app of this name
+ // must not find them.
+ promoted, err := s.List(ctx, PromotedPrefix(acct, app))
+ if err != nil {
+ return fmt.Errorf("release: list %s: %w", PromotedPrefix(acct, app), err)
+ }
+ keys = append(keys, promoted...)
for _, key := range keys {
if key == marker {
continue // last, below