feat(api): GET /api/v1/deployments/:id/events — failure timeline read surface#200
Merged
Merged
Conversation
… surface
Closes the silent-deploy-failure gap (swarm 2026-05-30): GET
/api/v1/deployments/:id surfaces only the LATEST failure_autopsy row
inside the optional `failure` field. Agents debugging a stuck deploy need
the full chronological timeline so they can distinguish a single OOM from
a retry storm.
Read-only — events are written by the worker (deploy_failure_autopsy +
deploy_status_reconcile), never by the api. Default limit 50, hard cap
200, silent clamp. RBAC mirrors GET /deployments/:id exactly: cross-team
returns 404 (NOT 403) so the platform never confirms the existence of
deployments owned by another team.
Rule 22 surface checklist:
- openapi.go: new path + DeploymentEventsResponse + DeploymentEvent schemas
- TestOpenAPI_CoversAllRegisteredRoutes still green (route documented)
- content/llms.txt: lives in a sibling repo (project_repo_topology) →
follow-up TODO in PR body
- dashboard: no UI surface in this PR — FailureTimeline panel is a
separate workstream
Rule 25 observability:
- instant_deploy_events_query_total{result} counter (ok|not_found|invalid|error)
- alert + dashboard tile + METRICS-CATALOG row: infra repo follow-up
(separate review surface)
Rule 17 coverage block:
Symptom: no read API for deployment_events autopsy rows
Enumeration: rg -F 'deployment_events' internal/handlers (read paths)
Sites found: 1 (deploymentToMapWithDB → latest autopsy only)
Sites touched: +1 (new Events handler, separate read path; existing
failure-field read path preserved verbatim)
Coverage test: TestOpenAPI_CoversAllRegisteredRoutes guards a future
/deployments/:id/* route landing without an OpenAPI entry
Live verified: awaiting post-merge SHA verify on api.instanode.dev
per rule 14
Tests (100% patch coverage):
- Model (sqlmock):
- TestGetDeploymentEvents_LimitClamp (default + max + verbatim)
- TestGetDeploymentEvents_EmptyResult
- TestGetDeploymentEvents_HappyPath_OrderingPreserved
- TestGetDeploymentEvents_CorruptJSONB_Recovers
- TestGetDeploymentEvents_QueryError
- TestGetDeploymentEvents_ScanError
- TestGetDeploymentEvents_RowsErr
- TestDeploymentEventsListConstants_AreSane
- Handler (live Fiber test app):
- TestDeployEvents_HappyPath_OrderingAndShape
- TestDeployEvents_Empty_ReturnsZeroCountWithEmptyArray
- TestDeployEvents_CrossTeam_Returns404
- TestDeployEvents_UnknownID_Returns404
- TestDeployEvents_LimitClamp_AboveMaxReturnsAtMost200
- TestDeployEvents_Unauthenticated_Returns401
- TestDeployEvents_InvalidLimit_FallsBackToDefault
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the four uncovered branches in deploy.go GET /api/v1/deployments/:id/events: - L927-929 requireTeam error path (covered via Events added to TestDeploy_RequireTeamError_AllRoutes sweep) - L933-936 empty :id path-param (TestDeployEvents_EmptyAppID_Returns400 mounts handler on a no-:id route) - L945-947 GetDeploymentByAppID non-NotFound DB error (TestDeployEvents_MidHandler503_FetchFailed fault-DB sweep) - L970-975 GetDeploymentEvents DB error (TestDeployEvents_EventsQueryFailed_ExactErrorCode asserts the exact envelope code) Also covers testhelpers.go L1167-1170 (route registration) via TestNewTestAppWithServices_RegistersDeployEventsRoute in the testhelpers package — handler-package tests don't attribute coverage to testhelpers. Local verification: go test ./internal/handlers/ -run TestDeployEvents_ -count=1 -short → ok go test ./internal/handlers/ -run TestDeploy_RequireTeamError_AllRoutes -count=1 -short → ok go test ./internal/testhelpers/ -run TestNewTestAppWithServices_RegistersDeployEventsRoute -count=1 -short → ok go tool cover -func=c.out | grep deploy.go:924 → Events 100.0%
TestAgentActionContract enforces every codeToAgentAction entry contain a full https://instanode.dev/ URL so LLM agents can reproduce a usable recovery link verbatim. The events_query_failed entry added in 7b67fe3 shipped without one, breaking the contract test in CI on PR #200. Add https://instanode.dev/status to the agent_action so the contract test passes. No behavior change — same 30-second-retry guidance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mastermanas805
added a commit
that referenced
this pull request
May 30, 2026
mastermanas805
added a commit
that referenced
this pull request
May 30, 2026
* ci: snapshot OpenAPI spec for cross-stack contract testing
The bug class: today's prod login broke for ~24h because /auth/exchange
(AUTH-004) shipped server-side without the dashboard's client being
updated. Per-repo unit tests on both sides stayed green because each repo
only saw its own half of the contract.
This PR makes the contract a committed artifact (openapi.snapshot.json)
so the dashboard + instanode-web repos can generate typed clients from it
deterministically. When the api spec changes, this CI gate fails the PR
until the snapshot is regenerated — surfacing the contract change so the
engineer can also update the client repos.
What ships:
- handlers.OpenAPISpecProduction() — exported accessor for the
production-rendered spec (with the dev-only /internal/set-tier path
stripped). Source of truth for downstream snapshotting.
- cmd/openapi-snapshot/ — small Go tool that canonicalises the spec
(sorted keys, 2-space indent) so whitespace-only edits to the const
do not flip the snapshot.
- openapi.snapshot.json — the committed snapshot (9931 lines, 384KB).
- make openapi-snapshot / openapi-snapshot-check — local regen +
drift check.
- .github/workflows/openapi-snapshot.yml — fast (<30s) PR gate that
fails with a clear "run make openapi-snapshot" message when the
committed file drifts from a fresh regeneration. Emits a structured
log line per run (rule 25 observability) so we can track contract
drift rate over time.
Coverage block (rule 17):
Symptom: Contract drift between api spec and TS clients goes
undetected at PR time → runtime login break.
Enumeration: rg -F 'openAPISpec' --include='*.go'
Sites found: Single const + ServeOpenAPI handler in
internal/handlers/openapi.go.
Sites touched: Added OpenAPISpecProduction() accessor next to
ServeOpenAPI; both go through the same
stripInternalSetTierPath path so what the snapshot
emits == what production serves.
Coverage test: make openapi-snapshot-check (CI job
snapshot-drift) regenerates and diffs on every PR
touching openapi.go / cmd/openapi-snapshot/ /
openapi.snapshot.json.
Live verified: make openapi-snapshot wrote 383616 bytes;
make openapi-snapshot-check returns "snapshot
matches handlers.OpenAPISpecProduction()". The
/internal/set-tier dev-only path is correctly
stripped (grep -c returns 0).
Follow-ups (separate PRs, scoped):
- dashboard PR: openapi-typescript dev dep + npm run generate:api-types
fetches this snapshot from origin/master and regenerates
src/lib/api.generated.ts; CI diffs the regenerated file vs committed.
Plus a Playwright AUTH-004 smoke test (POST /auth/email/start, OPTIONS
/auth/exchange CORS preflight, POST /auth/exchange with credentials).
- instanode-web PR: same pattern.
- infra PR: NR dashboard tile + alert on cross_stack_contract_drift
CI log events (rule 25).
Note: /auth/exchange is currently NOT in the OpenAPI spec — that gap
is the proximate cause of today's outage. This PR builds the
infrastructure to catch the next gap; adding /auth/exchange to the
spec is a separate fix that should follow this PR (it will trigger
this gate and force the regeneration discipline from day one).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: cover openapi-snapshot canonicalise + OpenAPISpecProduction (100% patch)
* test: fix canonicalise HTML-escape assertion (direction was inverted)
* test: use literal backslash-u0026 sequence in HTML-escape negative assertion
* test(openapi-snapshot): refactor main→run for testability + cover 4 new branches
Splits main() into a thin os.Exit wrapper around run(args, stdout, stderr),
the idiomatic Go pattern for testable mains. Adds 4 new tests covering
stdout mode, file mode, bad-flag path, and unwritable -out path (Linux-only
via /dev/full, skipped elsewhere). Removes the unreachable encode-error
fallback in canonicalise (re-encoding a valid-JSON-derived value cannot
fail) per Go's defensive-coding guidelines.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(openapi-snapshot): silence errcheck on final fmt.Fprintf
* ci: regenerate openapi.snapshot.json after #200 (events endpoint) merged
* test(openapi-snapshot): inject specSource for full run() coverage
* test(openapi-snapshot): cover main() via exitFn injection (100% patch)
* test(openapi-snapshot): use short-var-decl per staticcheck QF1011
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
GET /api/v1/deployments/:id/events— read-only failure-timeline endpoint.failure_autopsyrow inside the optionalfailurefield; agents debugging a stuck deploy need the full chronological timeline so they can distinguish a single OOM from a retry storm.deploy_failure_autopsy+deploy_status_reconcile), never by the api.Contract
Per-row shape:
{ kind, reason, exit_code, event, last_lines, hint, created_at }. Ordered DESC bycreated_at. Default limit 50, hard cap 200, silent clamp. Cross-team returns 404 (NOT 403) — the platform never confirms the existence of deployments owned by another team.Rule 22 surface checklist
api/internal/handlers/openapi.go— new path entry +DeploymentEventsResponse+DeploymentEventschemas.TestOpenAPI_CoversAllRegisteredRoutesstill green.content/llms.txt— lives in a sibling repo (instanodedev/content, per memoryproject_repo_topology). Follow-up TODO: add a one-line entry tocontent/llms.txtlisting the new endpoint so the public agent reference stays in sync.FailureTimelinepanel is a separate workstream the dashboard team can land on top.Rule 25 observability
instant_deploy_events_query_total{result}Prom counter —result∈ {ok, not_found, invalid, error}. Incremented on every code path.deploy events query error rate— fires whenrate(instant_deploy_events_query_total{result=\"error\"}[10m]) / rate(instant_deploy_events_query_total[10m]) > 5%. Plus dashboard tile oninstanode-reliability.jsonand the row ininfra/observability/METRICS-CATALOG.md. Tracked here because the infra repo is a separate review surface.Rule 17 coverage block
Test plan
go build ./...cleango vet ./...cleango test ./internal/handlers/ -run 'TestDeployEvents_|TestOpenAPI' -count=1→ PASSgo test ./internal/models/ -run 'TestGetDeploymentEvents|TestDeploymentEventsListConstants' -count=1→ PASSgo test ./internal/handlers/ -run 'TestDeploy' -count=1(regression) → PASScurl https://api.instanode.dev/healthz | jq .commit_idmust equalgit rev-parse --short HEADcurl -H "Authorization: Bearer ..." https://api.instanode.dev/api/v1/deployments/<id>/eventson a failed deploy returns the autopsy rowTest coverage (8 model + 7 handler tests)
Model (sqlmock):
TestGetDeploymentEvents_LimitClamp— default + max + verbatimTestGetDeploymentEvents_EmptyResultTestGetDeploymentEvents_HappyPath_OrderingPreservedTestGetDeploymentEvents_CorruptJSONB_RecoversTestGetDeploymentEvents_QueryErrorTestGetDeploymentEvents_ScanErrorTestGetDeploymentEvents_RowsErrTestDeploymentEventsListConstants_AreSaneHandler (live Fiber test app + real Postgres):
TestDeployEvents_HappyPath_OrderingAndShapeTestDeployEvents_Empty_ReturnsZeroCountWithEmptyArrayTestDeployEvents_CrossTeam_Returns404TestDeployEvents_UnknownID_Returns404TestDeployEvents_LimitClamp_AboveMaxReturnsAtMost200TestDeployEvents_Unauthenticated_Returns401TestDeployEvents_InvalidLimit_FallsBackToDefaultAnti-goals (deliberately not in scope)
instanodedev/workerfrom agents A + B in the swarm).FailureTimelineUI — separate workstream.content/llms.txtedit — sibling repo, follow-up.🤖 Generated with Claude Code