Skip to content

feat(api): GET /api/v1/deployments/:id/events — failure timeline read surface#200

Merged
mastermanas805 merged 5 commits into
masterfrom
feat/api-deployments-events-endpoint
May 30, 2026
Merged

feat(api): GET /api/v1/deployments/:id/events — failure timeline read surface#200
mastermanas805 merged 5 commits into
masterfrom
feat/api-deployments-events-endpoint

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

Summary

  • Adds GET /api/v1/deployments/:id/events — read-only failure-timeline endpoint.
  • Closes the silent-deploy-failure gap from the 2026-05-30 swarm: GET /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.

Contract

GET /api/v1/deployments/:id/events?limit=50
Auth: Bearer token (same RBAC as GET /deployments/:id)
200 → { ok, deployment_id, events: [...], count }
400 → invalid_id          (empty id)
401 → unauthorized
404 → not_found           (unknown id OR cross-team)
503 → fetch_failed / events_query_failed

Per-row shape: { kind, reason, exit_code, event, last_lines, hint, created_at }. Ordered DESC by created_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 + DeploymentEvent schemas. TestOpenAPI_CoversAllRegisteredRoutes still green.
  • content/llms.txt — lives in a sibling repo (instanodedev/content, per memory project_repo_topology). Follow-up TODO: add a one-line entry to content/llms.txt listing the new endpoint so the public agent reference stays in sync.
  • Dashboard surface: no UI change in this PR — FailureTimeline panel 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.
  • Follow-up (infra repo): alert deploy events query error rate — fires when rate(instant_deploy_events_query_total{result=\"error\"}[10m]) / rate(instant_deploy_events_query_total[10m]) > 5%. Plus dashboard tile on instanode-reliability.json and the row in infra/observability/METRICS-CATALOG.md. Tracked here because the infra repo is a 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

Test plan

  • go build ./... clean
  • go vet ./... clean
  • go test ./internal/handlers/ -run 'TestDeployEvents_|TestOpenAPI' -count=1 → PASS
  • go test ./internal/models/ -run 'TestGetDeploymentEvents|TestDeploymentEventsListConstants' -count=1 → PASS
  • go test ./internal/handlers/ -run 'TestDeploy' -count=1 (regression) → PASS
  • Post-merge: rule 14 SHA verify — curl https://api.instanode.dev/healthz | jq .commit_id must equal git rev-parse --short HEAD
  • Post-merge: live curl -H "Authorization: Bearer ..." https://api.instanode.dev/api/v1/deployments/<id>/events on a failed deploy returns the autopsy row

Test coverage (8 model + 7 handler tests)

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 + real Postgres):

  • TestDeployEvents_HappyPath_OrderingAndShape
  • TestDeployEvents_Empty_ReturnsZeroCountWithEmptyArray
  • TestDeployEvents_CrossTeam_Returns404
  • TestDeployEvents_UnknownID_Returns404
  • TestDeployEvents_LimitClamp_AboveMaxReturnsAtMost200
  • TestDeployEvents_Unauthenticated_Returns401
  • TestDeployEvents_InvalidLimit_FallsBackToDefault

Anti-goals (deliberately not in scope)

  • No autopsy creation / event writes — the worker owns that path (sibling PRs in instanodedev/worker from agents A + B in the swarm).
  • No dashboard FailureTimeline UI — separate workstream.
  • No content/llms.txt edit — sibling repo, follow-up.
  • No infra alert JSON — sibling repo, follow-up.

🤖 Generated with Claude Code

mastermanas805 and others added 5 commits May 30, 2026 16:53
… 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
mastermanas805 merged commit db43214 into master May 30, 2026
14 checks passed
@mastermanas805
mastermanas805 deleted the feat/api-deployments-events-endpoint branch May 30, 2026 12:47
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant