carlos / platform Public

release: manifests carry an optional label and notes

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 90b5127f4c80c8db051074c42875e773513b1e01 parent bc01f98
2 files changed, +72 −0
  • internal/release/release.go +21 −0
  • internal/release/release_test.go +51 −0
diff --git a/internal/release/release.go b/internal/release/release.go
index c9f689deb..b75385103 100644
--- a/internal/release/release.go
+++ b/internal/release/release.go
@@ -51,6 +51,14 @@ type Manifest struct {
Commit string `json:"commit,omitempty"`
BuiltBy string `json:"built_by,omitempty"`
BuiltAt string `json:"built_at,omitempty"`
+ // Label and Notes are the human half of a release (release versioning
+ // spec, 2026-08-08): a short one-line intent ("fixing modals") and a
+ // changelog fragment, both written at ship time. ADDITIVE to schema 1
+ // exactly as Capabilities was — absent from every manifest written
+ // before the fields existed, omitted from marshal when empty, ignored
+ // by old binaries. Nothing on the adopt path reads either.
+ Label string `json:"label,omitempty"`
+ Notes string `json:"notes,omitempty"`
// Artifacts is keyed "linux-arm64" etc for binaries, or "site" for a
// static tree tar.
Artifacts map[string]Artifact `json:"artifacts"`
@@ -149,6 +157,13 @@ const (
auditMax = 20
)
+// MaxLabelLen / MaxNotesLen bound the human half of a manifest. The label
+// is a table cell; the notes are a changelog fragment, not a novel.
+const (
+ MaxLabelLen = 120
+ MaxNotesLen = 16 << 10
+)
+
// Bucket keys, account-scoped: app names are unique WITHIN an account, so the
// account sqid is part of every key and the key path itself carries the
// app→account linkage (there is no separate linkage manifest). Channel names
@@ -265,6 +280,12 @@ func validateManifest(m *Manifest) error {
return fmt.Errorf("artifact %s: negative size", k)
}
}
+ if len(m.Label) > MaxLabelLen || strings.ContainsAny(m.Label, "\r\n") {
+ return fmt.Errorf("label: one line, at most %d characters", MaxLabelLen)
+ }
+ if len(m.Notes) > MaxNotesLen {
+ return fmt.Errorf("notes: at most %d bytes", MaxNotesLen)
+ }
// LAST, and deliberately the same call on both sides of the trust chain:
// Ship runs validateManifest before writing a byte (a human is present to
// read the refusal), and GetManifest runs it on every read (so a box
diff --git a/internal/release/release_test.go b/internal/release/release_test.go
index 727a9fcd6..fc9b380f2 100644
--- a/internal/release/release_test.go
+++ b/internal/release/release_test.go
@@ -1,6 +1,7 @@
package release
import (
+ "bytes"
"context"
"crypto/ed25519"
"encoding/base64"
@@ -776,3 +777,53 @@ func TestFastTrackIsAdditiveOnTheWire(t *testing.T) {
t.Fatalf("old shape read version %q", old.Version)
}
}
+
+func TestManifestLabelNotesAdditive(t *testing.T) {
+ // A manifest written before label/notes existed must still parse (the
+ // production buckets are full of them) …
+ old := []byte(`{"schema":1,"app":"km","version":"de2766f8","kind":"binary","artifacts":{"linux-arm64":{"file":"linux-arm64","sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","size":1}}}`)
+ var m Manifest
+ if err := json.Unmarshal(old, &m); err != nil {
+ t.Fatalf("old manifest no longer parses: %v", err)
+ }
+ if m.Label != "" || m.Notes != "" {
+ t.Fatalf("absent fields decoded non-empty: %q %q", m.Label, m.Notes)
+ }
+ // … and a label-free manifest must MARSHAL byte-identically to before,
+ // or idempotent re-ship of an old release breaks on its own metadata.
+ out, err := json.Marshal(m)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if bytes.Contains(out, []byte("label")) || bytes.Contains(out, []byte("notes")) {
+ t.Fatalf("empty label/notes leaked into marshal: %s", out)
+ }
+}
+
+func TestValidateManifestLabelNotesLimits(t *testing.T) {
+ base := func() *Manifest {
+ return &Manifest{Schema: 1, App: "km", Version: "0.5.0-1", Kind: "binary",
+ Artifacts: map[string]Artifact{"linux-arm64": {File: "linux-arm64", SHA256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", Size: 1}}}
+ }
+ m := base()
+ m.Label = "fixing modals"
+ m.Notes = "- rounded the corners\n- fixed the focus trap"
+ if err := validateManifest(m); err != nil {
+ t.Fatalf("valid label/notes refused: %v", err)
+ }
+ m = base()
+ m.Label = strings.Repeat("x", MaxLabelLen+1)
+ if err := validateManifest(m); err == nil {
+ t.Fatal("overlong label accepted")
+ }
+ m = base()
+ m.Label = "two\nlines"
+ if err := validateManifest(m); err == nil {
+ t.Fatal("multi-line label accepted")
+ }
+ m = base()
+ m.Notes = strings.Repeat("x", MaxNotesLen+1)
+ if err := validateManifest(m); err == nil {
+ t.Fatal("overlong notes accepted")
+ }
+}