carlos / platform Public

console: a failed deploy rollback says nothing was rolled back (#229)

Human

Prompt

approved

From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.

Summary

The deploy rollback's signer-failure 503 reused D26, "Nothing was
promoted." Paul approved D26r for the rollback path: "could not prepare a
signing key for this deploy credential. Nothing was rolled back." The
promote path keeps D26. A test pins the rollback text and that the
channel does not move.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 88681b20b54fb3e43d03a90ee80e8c428e2a458a parent 592b667
2 files changed, +35 −1
  • internal/console/apicli.go +1 −1
  • internal/console/deploysign_test.go +34 −0

CI passed — run details

diff --git a/internal/console/apicli.go b/internal/console/apicli.go
index 5dba6cde8..338e42ee3 100644
--- a/internal/console/apicli.go
+++ b/internal/console/apicli.go
@@ -1117,7 +1117,7 @@ func (s *Server) handleAPIRollback(w http.ResponseWriter, r *http.Request) {
sg, serr := s.deploySigner(p)
if serr != nil {
log.Printf("console: deploy signer for %s/%s: %v", acct.Sqid, app, serr)
- jsonError(w, http.StatusServiceUnavailable, "could not prepare a signing key for this deploy credential. Nothing was promoted.")
+ jsonError(w, http.StatusServiceUnavailable, "could not prepare a signing key for this deploy credential. Nothing was rolled back.")
return
}
signer = sg
diff --git a/internal/console/deploysign_test.go b/internal/console/deploysign_test.go
index 94472451e..47bead0cf 100644
--- a/internal/console/deploysign_test.go
+++ b/internal/console/deploysign_test.go
@@ -249,3 +249,37 @@ func TestDeployPromoteCannotFastTrack(t *testing.T) {
})
}
}
+
+// TestDeployRollbackSignerFailureSaysRolledBack: when the request-local
+// signer cannot be built on a rollback, the 503 says what did not happen —
+// a rollback (approved copy D26r), not D26's "Nothing was promoted." The
+// signer is made to fail with a credential name the grant format refuses;
+// only the store's own non-empty check stands between such a row and the
+// door, and the mint doors never write one.
+func TestDeployRollbackSignerFailureSaysRolledBack(t *testing.T) {
+ fx, person, _ := deployFixture(t)
+ for _, v := range []string{"v1", "v2"} {
+ if status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/promote",
+ map[string]any{"version": v, "channel": "edge"}, person); status != http.StatusOK {
+ t.Fatalf("person promote %s = %d %v", v, status, out)
+ }
+ }
+ bad, err := fx.st.NewServicePrincipalScoped(OpsAccountID, "bad name!", "hello", svcrole.Deploy,
+ []string{"canary", "edge"}, "paul@keymail.dev", time.Now())
+ if err != nil {
+ t.Fatal(err)
+ }
+ status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/rollback",
+ map[string]any{"channel": "edge"}, bad)
+ const want = "could not prepare a signing key for this deploy credential. Nothing was rolled back."
+ if status != http.StatusServiceUnavailable || str(t, out, "error") != want {
+ t.Fatalf("rollback with a failing signer = %d %v, want 503 %q", status, out, want)
+ }
+ cp, err := release.Channel(context.Background(), fx.rel, fx.pub, "bab", "hello", "edge")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if cp.Version != "v2" {
+ t.Fatalf("edge = %q after a refused rollback, want v2", cp.Version)
+ }
+}