Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5bc4528
feat(tern): key remote dispatches by deployment and verify the operat…
aparajon Aug 16, 2026
f68769d
feat(tern): one deployment correlates to exactly one remote apply
aparajon Aug 17, 2026
3910c62
feat(api): status folds surface the deployment's shared data-plane ap…
aparajon Aug 17, 2026
9b5099a
feat(cli): deployment-filtered status renders the data-plane apply id…
aparajon Aug 17, 2026
862c109
Merge remote-tracking branch 'origin/main' into armand/deployment-sha…
aparajon Aug 17, 2026
4a415dc
Merge remote-tracking branch 'origin/armand/deployment-shared-externa…
aparajon Aug 17, 2026
b6a5098
Merge remote-tracking branch 'origin/armand/deployment-apply-id-read'…
aparajon Aug 17, 2026
34b5418
fix(observability): count and log the own-row remote apply id refusal
aparajon Aug 17, 2026
30c80c7
Merge remote-tracking branch 'origin/armand/deployment-shared-externa…
aparajon Aug 17, 2026
104821f
Merge remote-tracking branch 'origin/armand/deployment-apply-id-read'…
aparajon Aug 17, 2026
5eba177
Merge remote-tracking branch 'origin/main' into armand/deployment-app…
aparajon Aug 21, 2026
5c0cec6
fix(observability): name the shared id neutrally and drop the numeric…
aparajon Aug 21, 2026
063da02
Merge remote-tracking branch 'origin/main' into armand/deployment-app…
aparajon Aug 24, 2026
bbb2143
Merge remote-tracking branch 'origin/armand/deployment-apply-id-read'…
aparajon Aug 24, 2026
b4969f4
test(tern): pin the one-data-plane-apply-per-deployment invariants (#…
aparajon Aug 26, 2026
bfebc3f
Merge remote-tracking branch 'origin/main' into armand/deployment-app…
aparajon Aug 26, 2026
55cbc4c
feat(cli): point the deployment-filtered footer at a usable status ha…
aparajon Aug 27, 2026
aa50625
docs(cli): show a recorded data-plane apply id in the deployment stat…
aparajon Aug 27, 2026
e01cd09
feat(cli): shorten the deployment-filtered status footer
aparajon Aug 27, 2026
b767688
feat(cli): give each remote handle its own status column
aparajon Aug 27, 2026
ab32cd2
fix(cli): always render the requested external id column outside depl…
aparajon Aug 29, 2026
530be27
fix(api): keep the parent apply's remote id when operations recorded …
aparajon Aug 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions TEMPLATES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6792,17 +6792,26 @@ No recent schema changes

1 active schema change

APPLY ID EXTERNAL OP ID DATABASE ENV DEPLOYMENT STATE STARTED SOURCE
apply-multi-a1b2c3d4 remote-op-us-east-001 orders-db production us-east Waiting for cutover 8 minutes ago https://github.com/acme/shop/pull/412
APPLY ID EXTERNAL APPLY ID EXTERNAL OP ID DATABASE ENV STATE STARTED SOURCE
apply-multi-a1b2c3d4 remote-apply-us-east-001 remote-op-us-east-001 orders-db production Waiting for cutover 8 minutes ago https://github.com/acme/shop/pull/412

Use 'schemabot status <apply_id>' to view details

Multiple matching operations:

1 active schema change

APPLY ID EXTERNAL OP ID DATABASE ENV DEPLOYMENT STATE STARTED SOURCE
apply-sharded-d5e6f7g8 - inventory-db production us-east Running 4 minutes ago https://github.com/acme/shop/pull/412
APPLY ID EXTERNAL APPLY ID DATABASE ENV STATE STARTED SOURCE
apply-sharded-d5e6f7g8 remote-apply-us-east-002 inventory-db production Running 4 minutes ago https://github.com/acme/shop/pull/412

Use 'schemabot status <apply_id>' to view details

No data-plane apply id recorded:

1 active schema change

APPLY ID DATABASE ENV STATE STARTED SOURCE
apply-pending-l3m4n5o6 payments-db production Pending 1 minute ago https://github.com/acme/shop/pull/412

Use 'schemabot status <apply_id>' to view details

Expand Down
182 changes: 124 additions & 58 deletions pkg/api/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4316,73 +4316,139 @@ func TestHandleStatusDeploymentFilterProjectsMatchingOperation(t *testing.T) {
assert.Equal(t, state.Apply.Completed, resp.Applies[0].State)
}

func TestHandleStatusDeploymentFilterSummarizesMatchingOperations(t *testing.T) {
// A deployment applied per shard has exactly one data-plane apply, so the
// deployment-filtered status list surfaces exactly one remote apply handle per
// row: the shared id the operations recorded, or the parent apply row's when
// the drive recorded it there instead (a drive that is not operation-scoped
// writes the remote id to the parent). Per-operation remote ids surface only
// when the filter matches a single operation; a fold keeps them in the detail
// views.
func TestHandleStatusDeploymentFilterRemoteHandles(t *testing.T) {
now := time.Now().UTC()
startedAt := now.Add(-2 * time.Minute)
completedAt := now.Add(-time.Minute)
applies := &recentApplyStore{
applies: []*storage.Apply{
{
ID: 101,
ApplyIdentifier: "apply-deployment",
ExternalID: "parent-external",
Database: "orders",
Environment: "staging",
Deployment: "deploy-a",
Engine: storage.EngineSpirit,
State: state.Apply.Completed,
Caller: "cli",
CreatedAt: now,
UpdatedAt: now,
},
},
completedOp := func(externalID, externalOperationID string) *storage.ApplyOperation {
return &storage.ApplyOperation{
ID: 202,
ApplyID: 101,
Deployment: "deploy-a",
ExternalID: externalID,
ExternalOperationID: externalOperationID,
State: state.Apply.Completed,
StartedAt: &startedAt,
CompletedAt: &completedAt,
CreatedAt: now.Add(-2 * time.Minute),
UpdatedAt: completedAt,
}
}
operations := &staticApplyOperationStore{
operations: []*storage.ApplyOperation{
{
ID: 202,
ApplyID: 101,
Deployment: "deploy-a",
ExternalOperationID: "remote-operation-202",
State: state.Apply.Completed,
StartedAt: &startedAt,
CompletedAt: &completedAt,
CreatedAt: now.Add(-2 * time.Minute),
UpdatedAt: completedAt,
},
{
ID: 203,
ApplyID: 101,
Deployment: "deploy-a",
ExternalOperationID: "remote-operation-203",
State: state.Apply.Running,
StartedAt: &startedAt,
CreatedAt: now.Add(-90 * time.Second),
UpdatedAt: now,
},
runningOp := func(externalID, externalOperationID string) *storage.ApplyOperation {
return &storage.ApplyOperation{
ID: 203,
ApplyID: 101,
Deployment: "deploy-a",
ExternalID: externalID,
ExternalOperationID: externalOperationID,
State: state.Apply.Running,
StartedAt: &startedAt,
CreatedAt: now.Add(-90 * time.Second),
UpdatedAt: now,
}
}

cases := []struct {
name string
parentExternalID string
applyState string
operations []*storage.ApplyOperation
wantActiveCount int
wantState string
wantExternalID string
wantExternalOpID string
}{
{
name: "a fold surfaces the shared data-plane apply id and no per-operation id",
applyState: state.Apply.Running,
operations: []*storage.ApplyOperation{completedOp("remote-apply-shared", "remote-operation-202"), runningOp("remote-apply-shared", "remote-operation-203")},
wantActiveCount: 1,
wantState: state.Apply.Running,
wantExternalID: "remote-apply-shared",
wantExternalOpID: "",
},
{
name: "a fold whose operations recorded no remote id keeps the parent apply row's",
parentExternalID: "parent-external",
applyState: state.Apply.Completed,
operations: []*storage.ApplyOperation{completedOp("", "remote-operation-202"), runningOp("", "remote-operation-203")},
wantActiveCount: 1,
wantState: state.Apply.Running,
wantExternalID: "parent-external",
wantExternalOpID: "",
},
{
name: "a single matching operation without a remote id keeps the parent apply row's",
parentExternalID: "parent-external",
applyState: state.Apply.Running,
operations: []*storage.ApplyOperation{runningOp("", "")},
wantActiveCount: 1,
wantState: state.Apply.Running,
wantExternalID: "parent-external",
wantExternalOpID: "",
},
{
name: "a single matching operation's own remote ids win over the parent's",
parentExternalID: "parent-external",
applyState: state.Apply.Completed,
operations: []*storage.ApplyOperation{completedOp("remote-apply-own", "remote-operation-202")},
wantActiveCount: 0,
wantState: state.Apply.Completed,
wantExternalID: "remote-apply-own",
wantExternalOpID: "remote-operation-202",
},
}
stor := &mockStorageWithApplyStores{applies: applies, operations: operations}
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError}))
svc := New(stor, testServerConfig(), nil, logger)
mux := http.NewServeMux()
svc.ConfigureRoutes(mux)

req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/status?environment=staging&deployment=deploy-a", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
applies := &recentApplyStore{
applies: []*storage.Apply{
{
ID: 101,
ApplyIdentifier: "apply-deployment",
ExternalID: tc.parentExternalID,
Database: "orders",
Environment: "staging",
Deployment: "deploy-a",
Engine: storage.EngineSpirit,
State: tc.applyState,
Caller: "cli",
CreatedAt: now,
UpdatedAt: now,
},
},
}
operations := &staticApplyOperationStore{operations: tc.operations}
stor := &mockStorageWithApplyStores{applies: applies, operations: operations}
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError}))
svc := New(stor, testServerConfig(), nil, logger)
mux := http.NewServeMux()
svc.ConfigureRoutes(mux)

require.Equal(t, http.StatusOK, w.Code)
var resp apitypes.StatusResponse
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/api/status?environment=staging&deployment=deploy-a", nil)
w := httptest.NewRecorder()
mux.ServeHTTP(w, req)

assert.Equal(t, 1, resp.ActiveCount)
require.Len(t, resp.Applies, 1)
assert.Equal(t, "apply-deployment", resp.Applies[0].ApplyID)
assert.Empty(t, resp.Applies[0].ExternalID)
assert.Empty(t, resp.Applies[0].ExternalOperationID)
assert.Equal(t, "deploy-a", resp.Applies[0].Deployment)
assert.Equal(t, state.Apply.Running, resp.Applies[0].State)
require.Equal(t, http.StatusOK, w.Code)
var resp apitypes.StatusResponse
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))

assert.Equal(t, tc.wantActiveCount, resp.ActiveCount)
require.Len(t, resp.Applies, 1)
assert.Equal(t, "apply-deployment", resp.Applies[0].ApplyID)
assert.Equal(t, tc.wantExternalID, resp.Applies[0].ExternalID)
assert.Equal(t, tc.wantExternalOpID, resp.Applies[0].ExternalOperationID)
assert.Equal(t, "deploy-a", resp.Applies[0].Deployment)
assert.Equal(t, tc.wantState, resp.Applies[0].State)
})
}
}

func TestHandleStatusFailedFilter(t *testing.T) {
Expand Down
8 changes: 7 additions & 1 deletion pkg/api/progress_handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -992,7 +992,13 @@ func activeApplyResponseFromStorage(apply *storage.Apply, op *storage.ApplyOpera
}
if op != nil {
active.Deployment = op.Deployment
active.ExternalID = op.ExternalID
// A drive that is not operation-scoped records the remote apply id on
// the parent apply row, not the operation, so an empty operation-level
// id keeps the parent's rather than hiding the deployment's one remote
// handle.
if op.ExternalID != "" {
active.ExternalID = op.ExternalID
}
active.ExternalOperationID = op.ExternalOperationID
active.State = op.State
active.ErrorMessage = op.ErrorMessage
Expand Down
24 changes: 24 additions & 0 deletions pkg/cmd/internal/templates/preview_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ func previewStatusDeploymentOutput() {
Applies: []ActiveApplyData{
{
ApplyID: "apply-multi-a1b2c3d4",
ExternalID: "remote-apply-us-east-001",
ExternalOperationID: "remote-op-us-east-001",
Database: "orders-db",
Environment: "production",
Expand All @@ -112,6 +113,7 @@ func previewStatusDeploymentOutput() {
Applies: []ActiveApplyData{
{
ApplyID: "apply-sharded-d5e6f7g8",
ExternalID: "remote-apply-us-east-002",
Database: "inventory-db",
Environment: "production",
Deployment: "us-east",
Expand All @@ -123,6 +125,28 @@ func previewStatusDeploymentOutput() {
},
},
})

fmt.Println()
fmt.Println("No data-plane apply id recorded:")
fmt.Println()
WriteStatusList(StatusListData{
ActiveCount: 1,
ShowExternalID: true,
Deployment: "us-east",
Applies: []ActiveApplyData{
{
ApplyID: "apply-pending-l3m4n5o6",
Database: "payments-db",
Environment: "production",
Deployment: "us-east",
State: state.Apply.Pending,
Engine: "Spirit",
Caller: "github:octocat@acme/shop#412",
StartedAt: previewTime.Add(-1 * time.Minute).Format(time.RFC3339),
UpdatedAt: previewTime.Add(-10 * time.Second).Format(time.RFC3339),
},
},
})
}

func previewStatusHistoryOutput() {
Expand Down
Loading
Loading