carlos / platform Public

ship: both doors refuse a change of program without --program-change

carlos ship (direct bucket) and the console's API ship door both call release.CheckShipProgram before release.Ship. --program-change rides the meta part as program_change. runShipAPI and apiSession.ship gain a programChange parameter; deploy and canary pass false so they never change program silently.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev ca93acf77ed125a89fc6f9bf34697d83fefb2cf3 parent b44cab1
8 files changed, +143 −31
  • apimode.go +13 −8
  • apimode_test.go +16 −16
  • canary.go +1 −1
  • cli.go +11 −4
  • deploy.go +1 −1
  • internal/console/apicli.go +21 −0
  • internal/console/apicli_test.go +79 −0
  • mutation_message_test.go +1 −1
diff --git a/apimode.go b/apimode.go
index 409353694..7e4351ead 100644
--- a/apimode.go
+++ b/apimode.go
@@ -3950,7 +3950,11 @@ const shipTimeout = 15 * time.Minute
// unknown-inside-a-known ones the strict decoder already reported and
// ignored. nil (no --capabilities) omits the key entirely, so the meta part's
// bytes stay byte-identical to what this verb sent yesterday.
-func (s *apiSession) ship(ctx context.Context, a apiAcct, app, version, kind, label, notes string, files map[string]string, capsSet *caps.Set, messages ...string) (apiShipResult, error) {
+//
+// programChange rides in the meta part too (`program_change`, omitted when
+// false so the bytes match what this verb sent before the flag): it is the
+// operator's --program-change, and only `carlos ship` ever passes true.
+func (s *apiSession) ship(ctx context.Context, a apiAcct, app, version, kind, label, notes string, files map[string]string, capsSet *caps.Set, programChange bool, messages ...string) (apiShipResult, error) {
message := firstMutationMessage(messages)
ctx, cancel := context.WithTimeout(ctx, shipTimeout)
defer cancel()
@@ -3971,13 +3975,14 @@ func (s *apiSession) ship(ctx context.Context, a apiAcct, app, version, kind, la
capsRaw = b
}
metaJSON, err := json.Marshal(struct {
- Version string `json:"version"`
- Kind string `json:"kind"`
- Label string `json:"label,omitempty"`
- Notes string `json:"notes,omitempty"`
- Message string `json:"message,omitempty"`
- Capabilities json.RawMessage `json:"capabilities,omitempty"`
- }{version, kind, label, notes, message, capsRaw})
+ Version string `json:"version"`
+ Kind string `json:"kind"`
+ Label string `json:"label,omitempty"`
+ Notes string `json:"notes,omitempty"`
+ Message string `json:"message,omitempty"`
+ Capabilities json.RawMessage `json:"capabilities,omitempty"`
+ ProgramChange bool `json:"program_change,omitempty"`
+ }{version, kind, label, notes, message, capsRaw, programChange})
if err != nil {
return err
}
diff --git a/apimode_test.go b/apimode_test.go
index 0c7ab101b..4090bb6ab 100644
--- a/apimode_test.go
+++ b/apimode_test.go
@@ -271,7 +271,7 @@ func TestAPISessionShipMultipart(t *testing.T) {
res, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", map[string]string{
"zeta-amd64": fileB,
"alpha-amd64": fileA,
- }, nil)
+ }, nil, false)
if err != nil {
t.Fatalf("ship: %v", err)
}
@@ -331,7 +331,7 @@ func TestAPISessionShipNoOp(t *testing.T) {
t.Fatal(err)
}
s := newTestSession(ts.URL)
- res, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", map[string]string{"alpha-amd64": file}, nil)
+ res, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", map[string]string{"alpha-amd64": file}, nil, false)
if err != nil {
t.Fatalf("ship: %v", err)
}
@@ -355,7 +355,7 @@ func TestAPISessionShipErrorSurfacesServerMessage(t *testing.T) {
t.Fatal(err)
}
s := newTestSession(ts.URL)
- _, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", map[string]string{"alpha-amd64": file}, nil)
+ _, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", map[string]string{"alpha-amd64": file}, nil, false)
if err == nil || !strings.Contains(err.Error(), "already shipped with different content") {
t.Fatalf("ship error = %v, want the server's conflict message", err)
}
@@ -397,7 +397,7 @@ func TestAPISessionShipSurvivesSlowServer(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
- _, err := s.ship(ctx, apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", map[string]string{"alpha-amd64": file}, nil)
+ _, err := s.ship(ctx, apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", map[string]string{"alpha-amd64": file}, nil, false)
if err == nil {
t.Fatal("ship with a tiny absolute client Timeout against a slow-to-answer server must fail")
}
@@ -421,7 +421,7 @@ func TestAPISessionShipSurvivesSlowServer(t *testing.T) {
// shipHTTPClientTimeout is back at its zero production default here
// (the sibling subtest above restored it) — only the context bounds
// this call.
- res, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", map[string]string{"alpha-amd64": file}, nil)
+ res, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", map[string]string{"alpha-amd64": file}, nil, false)
if err != nil {
t.Fatalf("ship against a slow-but-healthy server = %v, want success", err)
}
@@ -1621,7 +1621,7 @@ func TestShipAPIStaticNamesSiteTarGz(t *testing.T) {
sess := newTestSession(ts.URL)
fs, acctRaw, acctIDRaw := newShipFlagSet(t, apiTestAccount, siteDir)
- if _, err := runShipAPI(sess, apiAccount(acctRaw, acctIDRaw), fs, "titesite", "abc1234", "static", "", "", "", nil, true); err != nil {
+ if _, err := runShipAPI(sess, apiAccount(acctRaw, acctIDRaw), fs, "titesite", "abc1234", "static", "", "", "", nil, true, false); err != nil {
t.Fatalf("runShipAPI: %v", err)
}
@@ -1668,7 +1668,7 @@ func TestShipAPIStaticCleansUpTempFileOnServerError(t *testing.T) {
sess := newTestSession(ts.URL)
fs, acctRaw, acctIDRaw := newShipFlagSet(t, apiTestAccount, siteDir)
- if _, err := runShipAPI(sess, apiAccount(acctRaw, acctIDRaw), fs, "titesite", "abc1234", "static", "", "", "", nil, true); err == nil {
+ if _, err := runShipAPI(sess, apiAccount(acctRaw, acctIDRaw), fs, "titesite", "abc1234", "static", "", "", "", nil, true, false); err == nil {
t.Fatal("runShipAPI against a failing server must error")
}
@@ -1734,7 +1734,7 @@ func TestShipAPIMultiFileGoodBasenames(t *testing.T) {
sess := newTestSession(ts.URL)
fs, acctRaw, acctIDRaw := newShipFlagSet(t, apiTestAccount, fileArm, fileAmd)
- if _, err := runShipAPI(sess, apiAccount(acctRaw, acctIDRaw), fs, "titogo", "abc1234", "binary", "linux-arm64", "", "", nil, true); err != nil {
+ if _, err := runShipAPI(sess, apiAccount(acctRaw, acctIDRaw), fs, "titogo", "abc1234", "binary", "linux-arm64", "", "", nil, true, false); err != nil {
t.Fatalf("runShipAPI: %v", err)
}
@@ -1776,7 +1776,7 @@ func TestShipAPIMultiFileBadBasenameRefused(t *testing.T) {
sess := newTestSession(ts.URL)
fs, acctRaw, acctIDRaw := newShipFlagSet(t, apiTestAccount, fileGood, fileBad)
- _, err := runShipAPI(sess, apiAccount(acctRaw, acctIDRaw), fs, "titogo", "abc1234", "binary", "linux-arm64", "", "", nil, true)
+ _, err := runShipAPI(sess, apiAccount(acctRaw, acctIDRaw), fs, "titogo", "abc1234", "binary", "linux-arm64", "", "", nil, true, false)
if err == nil {
t.Fatal("runShipAPI with a bad basename must error")
}
@@ -1811,7 +1811,7 @@ func TestShipAPIMissingArtifactRefusedBeforeDialing(t *testing.T) {
sess := newTestSession(ts.URL)
fs, acctRaw, acctIDRaw := newShipFlagSet(t, apiTestAccount, missing)
- _, err := runShipAPI(sess, apiAccount(acctRaw, acctIDRaw), fs, "titogo", "abc1234", "binary", "linux-arm64", "", "", nil, true)
+ _, err := runShipAPI(sess, apiAccount(acctRaw, acctIDRaw), fs, "titogo", "abc1234", "binary", "linux-arm64", "", "", nil, true, false)
if err == nil {
t.Fatal("runShipAPI with a missing artifact path must error")
}
@@ -1872,7 +1872,7 @@ func TestAPISessionShipCarriesCapabilitiesInMeta(t *testing.T) {
s := newTestSession(ts.URL)
if _, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "",
- map[string]string{"alpha-amd64": file}, set); err != nil {
+ map[string]string{"alpha-amd64": file}, set, false); err != nil {
t.Fatalf("ship: %v", err)
}
want := `{"version":"1.2.3","kind":"binary","capabilities":{"v":1,"daemon":true,"ports":[{"port":25,"name":"mx"}]}}`
@@ -1933,7 +1933,7 @@ func TestAPISessionShipAcceptsAMatchingCapabilitiesEcho(t *testing.T) {
defer ts.Close()
s := newTestSession(ts.URL)
- res, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", shipTestArtifact(t), shipTestCaps(t))
+ res, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", shipTestArtifact(t), shipTestCaps(t), false)
if err != nil {
t.Fatalf("ship with a matching echo: %v", err)
}
@@ -1954,7 +1954,7 @@ func TestAPISessionShipRefusesAConsoleThatDropsCapabilities(t *testing.T) {
defer ts.Close()
s := newTestSession(ts.URL)
- _, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", shipTestArtifact(t), shipTestCaps(t))
+ _, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", shipTestArtifact(t), shipTestCaps(t), false)
if err == nil {
t.Fatal("a console that echoed no capability block was accepted — that is an old console silently dropping the declaration")
}
@@ -1972,7 +1972,7 @@ func TestAPISessionShipRefusesADifferentCapabilitiesEcho(t *testing.T) {
defer ts.Close()
s := newTestSession(ts.URL)
- _, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", shipTestArtifact(t), shipTestCaps(t))
+ _, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", shipTestArtifact(t), shipTestCaps(t), false)
if err == nil {
t.Fatal("a console that stored a DIFFERENT capability block was accepted")
}
@@ -1991,7 +1991,7 @@ func TestAPISessionShipWithoutCapabilitiesNeedsNoEcho(t *testing.T) {
defer ts.Close()
s := newTestSession(ts.URL)
- if _, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", shipTestArtifact(t), nil); err != nil {
+ if _, err := s.ship(context.Background(), apiAcct{Seg: "acct"}, "app", "1.2.3", "binary", "", "", shipTestArtifact(t), nil, false); err != nil {
t.Fatalf("ship without --capabilities must not require an echo: %v", err)
}
}
@@ -4025,7 +4025,7 @@ func TestCarlosDeployRoutinePathUsesOnlyDeployDoors(t *testing.T) {
run func() error
}{
{"ship", func() error {
- _, err := s.ship(ctx, a, "app", "1.2.3", "binary", "", "", map[string]string{"alpha-amd64": file}, nil)
+ _, err := s.ship(ctx, a, "app", "1.2.3", "binary", "", "", map[string]string{"alpha-amd64": file}, nil, false)
return err
}},
{"promote", func() error { _, err := s.promote(ctx, a, "app", "1.2.3", "canary", false, false); return err }},
diff --git a/canary.go b/canary.go
index 02f9fd1d1..9fe29e3ee 100644
--- a/canary.go
+++ b/canary.go
@@ -638,7 +638,7 @@ func runCanaryDeploy(args []string) {
}
// printBanner false: printUsing above already said it once.
- if _, err = runShipAPI(sess, acct, fs, app2, *ver, *kind, *target, *label, "", nil, false); err != nil {
+ if _, err = runShipAPI(sess, acct, fs, app2, *ver, *kind, *target, *label, "", nil, false, false); err != nil {
log.Fatalf("canary: ship: %v", err)
}
diff --git a/cli.go b/cli.go
index f843ef76c..eaa178390 100644
--- a/cli.go
+++ b/cli.go
@@ -2018,6 +2018,7 @@ func runShip(args []string) {
label := fs.String("label", "", "one-line human label for this release (\"fixing modals\")")
notes := fs.String("notes", "", "changelog fragment for this release")
notesFile := fs.String("notes-file", "", "file holding the changelog fragment (mutually exclusive with --notes)")
+ programChange := fs.Bool("program-change", false, "accept that this release's Go main package differs from the app's previous release (recorded in the manifest)")
fs.Usage = flagUsage(fs, "")
fs.Parse(args)
// --version is no longer required: with a version target set, the next
@@ -2025,7 +2026,7 @@ func runShip(args []string) {
// the target (release versioning spec, 2026-08-08). Everything else about
// this check is unchanged — an app and at least one file are still the
// minimum a ship can mean.
- usage := "usage: carlos ship --app <a> [--version <sha>] [--label <one line>] [--notes <text> | --notes-file <file>] [--kind binary|static] [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>] [--capabilities <file.json>] <files-or-site-dir>"
+ usage := "usage: carlos ship --app <a> [--version <sha>] [--label <one line>] [--notes <text> | --notes-file <file>] [--kind binary|static] [--account <sqid-or-name>] [--account-id <sqid>] [--console <origin>] [--capabilities <file.json>] [--program-change] <files-or-site-dir>"
if fs.NArg() == 0 {
log.Fatal(usage)
}
@@ -2103,7 +2104,7 @@ func runShip(args []string) {
log.Fatal(err)
}
}
- if _, err := runShipAPI(sess, acct, fs, app2, *ver, *kind, *target, *label, noteText, capsSet, false, message); err != nil {
+ if _, err := runShipAPI(sess, acct, fs, app2, *ver, *kind, *target, *label, noteText, capsSet, false, *programChange, message); err != nil {
log.Fatal(err)
}
return
@@ -2189,6 +2190,12 @@ func runShip(args []string) {
// manifest bytes byte-identical to what this verb wrote yesterday.
Capabilities: capsSet,
}
+ // The wrong-program rule, shared with the console's API door: refuse a
+ // release whose Go main package differs from the app's previous release
+ // unless --program-change says it is meant.
+ if err := release.CheckShipProgram(context.Background(), st, acct, &m, files, *programChange); err != nil {
+ log.Fatal(err)
+ }
if err := release.Ship(context.Background(), st, acct, m, files); err != nil {
log.Fatal(err)
}
@@ -2265,7 +2272,7 @@ var shipTargetRe = regexp.MustCompile(`^[a-z0-9]+-[a-z0-9]+$`)
// would otherwise print it twice. The "shipped"/"already shipped" line
// below is never suppressed; it is the one true account of what ship did,
// and deploy relies on that rather than printing its own.
-func runShipAPI(sess *apiSession, acct apiAcct, fs *flag.FlagSet, app, ver, kind, target, label, notes string, capsSet *caps.Set, printBanner bool, messages ...string) (apiShipResult, error) {
+func runShipAPI(sess *apiSession, acct apiAcct, fs *flag.FlagSet, app, ver, kind, target, label, notes string, capsSet *caps.Set, printBanner, programChange bool, messages ...string) (apiShipResult, error) {
message := firstMutationMessage(messages)
files := map[string]string{}
if kind == "static" {
@@ -2324,7 +2331,7 @@ func runShipAPI(sess *apiSession, acct apiAcct, fs *flag.FlagSet, app, ver, kind
if printBanner {
printUsing(stdout, sess)
}
- res, err := sess.ship(context.Background(), acct, app, ver, kind, label, notes, files, capsSet, message)
+ res, err := sess.ship(context.Background(), acct, app, ver, kind, label, notes, files, capsSet, programChange, message)
if err != nil {
return apiShipResult{}, err
}
diff --git a/deploy.go b/deploy.go
index 3d71ff985..cf7973496 100644
--- a/deploy.go
+++ b/deploy.go
@@ -297,7 +297,7 @@ func runDeploy(args []string) {
// printBanner=false: the "using ..." line already printed above, before
// the instance list — so a listing failure still names the console —
// and runShipAPI would otherwise print it a second time.
- shipRes, err := runShipAPI(sess, acct, fs, *app, *ver, *kind, *target, *label, *notes, capsSet, false, message)
+ shipRes, err := runShipAPI(sess, acct, fs, *app, *ver, *kind, *target, *label, *notes, capsSet, false, false, message)
if err != nil {
log.Fatalf("deploy: ship: %v", err)
}
diff --git a/internal/console/apicli.go b/internal/console/apicli.go
index 6fd6d2040..825f204b5 100644
--- a/internal/console/apicli.go
+++ b/internal/console/apicli.go
@@ -1750,6 +1750,11 @@ func (s *Server) handleAPIShip(w http.ResponseWriter, r *http.Request) {
// than "malformed meta part" — an unknown capability name is a
// specific, actionable thing to be told.
Capabilities json.RawMessage `json:"capabilities,omitempty"`
+ // ProgramChange is `carlos ship --program-change`: the operator's
+ // statement that this release's Go main package really differs from
+ // the app's previous release. Absent from every request a CLI shipped
+ // before the flag existed, which then meets the refusal below.
+ ProgramChange bool `json:"program_change,omitempty"`
}
// The budget is release.MaxNotesLen — the biggest single field the meta
// part now carries — plus 8KiB of headroom for everything else in it:
@@ -1996,6 +2001,22 @@ func (s *Server) handleAPIShip(w http.ResponseWriter, r *http.Request) {
Label: meta.Label,
Notes: meta.Notes,
}
+ // The wrong-program rule, the same one the direct-bucket `carlos ship`
+ // calls: a release whose Go main package differs from the app's previous
+ // release is refused unless the caller said --program-change. It reads
+ // the artifact bytes that arrived, so it runs here, after m exists and
+ // before anything is written. A mismatch is a 409 (the request is well
+ // formed, the app's history disagrees); anything else is a 400, the
+ // artifact or the app's releases being unreadable.
+ if err := release.CheckShipProgram(r.Context(), s.cfg.Releases, acct.Sqid, &m, files, meta.ProgramChange); err != nil {
+ var pm *release.ProgramMismatchError
+ if errors.As(err, &pm) {
+ jsonError(w, http.StatusConflict, err.Error())
+ return
+ }
+ jsonError(w, http.StatusBadRequest, err.Error())
+ return
+ }
// Longer than the other CLI doors' 15-20s: this one moves real bytes —
// up to apiShipMaxBytes of them — into the bucket, not a handful of
// short strings.
diff --git a/internal/console/apicli_test.go b/internal/console/apicli_test.go
index b592e9c78..c652091b7 100644
--- a/internal/console/apicli_test.go
+++ b/internal/console/apicli_test.go
@@ -21,6 +21,9 @@ import (
"mime/multipart"
"net/http"
"net/url"
+ "os"
+ "os/exec"
+ "path/filepath"
"reflect"
"strings"
"sync"
@@ -1347,6 +1350,82 @@ func TestAPIShipImmutableConflict(t *testing.T) {
}
}
+// tinyProgramForConsole is internal/release's tinyProgram, copied because a
+// test helper cannot be imported across packages: a stripped linux/arm64 Go
+// binary whose main package is modPath, built as the console is built, at
+// test time. Nothing is checked in.
+func tinyProgramForConsole(t *testing.T, modPath string) []byte {
+ t.Helper()
+ if testing.Short() {
+ t.Skip("builds a binary")
+ }
+ dir := t.TempDir()
+ must := func(err error) {
+ t.Helper()
+ if err != nil {
+ t.Fatal(err)
+ }
+ }
+ must(os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module "+modPath+"\n\ngo 1.22\n"), 0o644))
+ must(os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\nfunc main() {}\n"), 0o644))
+ out := filepath.Join(dir, "bin")
+ cmd := exec.Command("go", "build", "-trimpath", "-ldflags=-s -w", "-o", out, ".")
+ cmd.Dir = dir
+ cmd.Env = append(os.Environ(), "GOOS=linux", "GOARCH=arm64", "CGO_ENABLED=0", "GOFLAGS=-buildvcs=false")
+ if b, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("build %s: %v\n%s", modPath, err, b)
+ }
+ b, err := os.ReadFile(out)
+ must(err)
+ return b
+}
+
+// TestAPIShipRefusesAProgramChange: the wrong-program rule at the API door.
+// A second release whose Go main package differs from the app's previous
+// release is a 409 naming --program-change, and the same ship with the
+// meta part's program_change set goes through and is recorded.
+func TestAPIShipRefusesAProgramChange(t *testing.T) {
+ fx := newConsoleFixture(t)
+ now := time.Now()
+ if _, err := fx.st.CreateAppIn(OpsAccountID, "titogo", "paul@keymail.dev", now); err != nil {
+ t.Fatal(err)
+ }
+ token := cliToken(t, fx, "paul@keymail.dev", "code-ship-program")
+ liftShipBudget(fx)
+ url := fx.ts.URL + "/api/cli/apps/bab/titogo/releases"
+ console := tinyProgramForConsole(t, "example.com/console")
+ carlos := tinyProgramForConsole(t, "example.com/carlos")
+
+ status, out := postShip(t, url, token,
+ []shipPart{metaPart(t, "v1", "binary"), {name: "linux-arm64", body: console}})
+ if status != http.StatusCreated {
+ t.Fatalf("first ship = %d %v", status, out)
+ }
+
+ status, out = postShip(t, url, token,
+ []shipPart{metaPart(t, "v2", "binary"), {name: "linux-arm64", body: carlos}})
+ if status != http.StatusConflict || !strings.Contains(str(t, out, "error"), "--program-change") {
+ t.Fatalf("second ship = %d %v, want 409 naming --program-change", status, out)
+ }
+
+ raw, err := json.Marshal(map[string]any{"version": "v3", "kind": "binary", "program_change": true})
+ if err != nil {
+ t.Fatal(err)
+ }
+ status, out = postShip(t, url, token,
+ []shipPart{{name: "meta", body: raw}, {name: "linux-arm64", body: carlos}})
+ if status != http.StatusCreated {
+ t.Fatalf("ship with program_change = %d %v, want 201", status, out)
+ }
+ m, err := release.GetManifest(context.Background(), fx.rel, release.OpsAccount, "titogo", "v3")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if m.Program != "example.com/carlos" || !m.ProgramChange {
+ t.Fatalf("manifest program = %q change = %v, want example.com/carlos recorded as a change", m.Program, m.ProgramChange)
+ }
+}
+
// TestAPIShipConcurrencyLimit: the rate limiter bounds requests over TIME,
// but says nothing about how many run AT ONCE. Two ships already in flight
// (each holding the server's ship semaphore while its body trickles in,
diff --git a/mutation_message_test.go b/mutation_message_test.go
index bf1349533..b8783aa14 100644
--- a/mutation_message_test.go
+++ b/mutation_message_test.go
@@ -179,7 +179,7 @@ func TestMutationMessageTravelsInShipMetadata(t *testing.T) {
t.Fatal(err)
}
_, err := newTestSession(ts.URL).ship(context.Background(), apiAcct{Seg: "bab"}, "titogo",
- "abc1234", "binary", "", "", map[string]string{"linux-arm64": artifact}, nil,
+ "abc1234", "binary", "", "", map[string]string{"linux-arm64": artifact}, nil, false,
"ship the checkout fix")
if err != nil {
t.Fatal(err)