console: a failed deploy rollback says nothing was rolled back (#229)
The deploy rollback's signer-failure 503 now uses the approved D26r, 'Nothing was rolled back.', instead of D26's 'Nothing was promoted.' The promote path keeps D26. A test pins the text and that the channel does not move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Squashed from fix-deploy-rollback-copy into main.
2 files changed,
+35
−1
internal/console/apicli.go+1 −1internal/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) |
| + } |
| +} |