carlos / platform Public

release: fail closed on store errors in program check

PreviousProgram now only skips missing manifests (os.ErrNotExist) and returns any other error to fail the check closed. A transient store failure no longer silently disables the wrong-program check. Added TestPreviousProgramFailsClosedOnStoreError to verify this behavior and added doc comments to ArtifactProgram, FileProgram, and ProgramMismatchError.
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev f1c33a09852b995b4155f6d28f2e26acadb7e6fc parent fee7f2c
2 files changed, +67 −2
  • internal/release/program.go +17 −2
  • internal/release/program_test.go +50 −0
diff --git a/internal/release/program.go b/internal/release/program.go
index 2bcf02525..4bc42d714 100644
--- a/internal/release/program.go
+++ b/internal/release/program.go
@@ -13,11 +13,15 @@ import (
"bytes"
"context"
"debug/buildinfo"
+ "errors"
"fmt"
+ "os"
"sort"
"strings"
)
+// ArtifactProgram extracts the Go main package path from a binary's debug/buildinfo,
+// returning "" if the bytes are not a Go binary or buildinfo is unreadable.
func ArtifactProgram(b []byte) string {
bi, err := buildinfo.Read(bytes.NewReader(b))
if err != nil {
@@ -26,6 +30,8 @@ func ArtifactProgram(b []byte) string {
return bi.Path
}
+// FileProgram extracts the Go main package path from a file at path,
+// returning "" if the file is not a Go binary or buildinfo is unreadable.
func FileProgram(path string) string {
bi, err := buildinfo.ReadFile(path)
if err != nil {
@@ -59,7 +65,8 @@ func ReleaseProgram(files map[string][]byte) (string, error) {
}
// PreviousProgram is the program of the newest release (among the 20 newest)
-// whose manifest declares one, and that release's version.
+// whose manifest declares one, and that release's version. It skips only missing
+// manifests (os.ErrNotExist), returning any other error to fail the check closed.
func PreviousProgram(ctx context.Context, s Store, acct, app string) (string, string, error) {
vers, _, err := VersionCandidates(ctx, s, acct, app, 20)
if err != nil {
@@ -68,7 +75,13 @@ func PreviousProgram(ctx context.Context, s Store, acct, app string) (string, st
var best *Manifest
for _, v := range vers {
m, err := GetManifest(ctx, s, acct, app, v)
- if err != nil || m == nil || m.Program == "" {
+ if err != nil {
+ if errors.Is(err, os.ErrNotExist) {
+ continue
+ }
+ return "", "", err
+ }
+ if m == nil || m.Program == "" {
continue
}
if best == nil || m.BuiltAt > best.BuiltAt {
@@ -81,6 +94,8 @@ func PreviousProgram(ctx context.Context, s Store, acct, app string) (string, st
return best.Program, best.Version, nil
}
+// ProgramMismatchError is returned when a release attempts to ship a different program
+// than the app's previous releases without the --program-change override.
type ProgramMismatchError struct{ App, Got, Want, WantVersion string }
func (e *ProgramMismatchError) Error() string {
diff --git a/internal/release/program_test.go b/internal/release/program_test.go
index a4f2269ea..6d3bac9e4 100644
--- a/internal/release/program_test.go
+++ b/internal/release/program_test.go
@@ -109,3 +109,53 @@ func TestPreviousProgramSkipsReleasesWithoutOne(t *testing.T) {
t.Fatalf("PreviousProgram = %q %q %v, want example.com/console from v1", got, ver, err)
}
}
+
+// failingStore wraps a Store and fails Get for a specific key with a non-NotExist error.
+type failingStore struct {
+ Store
+ failKey string
+}
+
+func (fs *failingStore) Get(ctx context.Context, key string) ([]byte, string, error) {
+ if key == fs.failKey {
+ return nil, "", errors.New("simulated store failure")
+ }
+ return fs.Store.Get(ctx, key)
+}
+
+// A store error (not just missing manifest) must fail the check closed.
+func TestPreviousProgramFailsClosedOnStoreError(t *testing.T) {
+ base := NewDirStore(t.TempDir())
+ console := tinyProgram(t, "example.com/console")
+
+ // Ship v1 with console program, then v2 normally.
+ if err := shipWithProgram(t, base, "v1", console, false); err != nil {
+ t.Fatalf("ship v1: %v", err)
+ }
+ shipRelease(t, base, "bab", "console", "v2", adoptPlatform, []byte("v2 content"))
+
+ // Wrap the store to fail on v1's manifest key.
+ manifestKey := ManifestKey("bab", "console", "v1")
+ s := &failingStore{Store: base, failKey: manifestKey}
+
+ // PreviousProgram must return an error (not silently skip the failure).
+ prog, ver, err := PreviousProgram(context.Background(), s, "bab", "console")
+ if err == nil || prog != "" || ver != "" {
+ t.Fatalf("PreviousProgram = %q %q %v, want non-nil error", prog, ver, err)
+ }
+
+ // CheckShipProgram must propagate the error.
+ files := map[string][]byte{adoptPlatform: console}
+ m := Manifest{Schema: 1, App: "console", Version: "v3", Kind: "binary",
+ Artifacts: map[string]Artifact{adoptPlatform: {}}}
+ checkErr := CheckShipProgram(context.Background(), s, "bab", &m, files, false)
+ if checkErr == nil {
+ t.Fatalf("CheckShipProgram returned nil, want error")
+ }
+ // The error should not be a ProgramMismatchError (which would indicate the check
+ // wrongly disabled itself and allowed the program change).
+ var pm *ProgramMismatchError
+ if errors.As(checkErr, &pm) {
+ t.Fatalf("CheckShipProgram = ProgramMismatchError, want other error: %v", checkErr)
+ }
+}