feat(jobs): hourly synthetic deploy prober (DEPLOY-001) - #69
Merged
Conversation
Drives a real end-to-end deploy against the prod /deploy/new pipeline
every 60 minutes (POST /deploy/new with redeploy=true → Kaniko build +
k8s rollout → status poll until healthy → public-host GET expecting 200)
and pages NR P0 on any leg failure. Closes the gap that hid the
2026-05-30 morning truehomie-api stuck-build incident for ~30 minutes
until the user reported it. Sibling to AUTH-004 (worker#68): same
metric/leg/result taxonomy, same audit_log + structured-slog dual
surface, same fail-open posture when the probe bearer is unset.
Three legs per tick:
1. submit — POST /deploy/new with embedded nginx tarball (in-memory,
no fixture dep), name=deploy-probe-hourly, port=80, env=development,
redeploy=true. The redeploy flag reuses one persistent probe-app
row forever — no deployment-slot accumulation.
2. status — poll GET /deploy/<app_id> every 5s for up to 90s,
asserting status flips to "healthy". `building` past 90s and
`failed` are both fail-alerts.
3. serve — GET https://<app_id>.deployment.instanode.dev/ expecting
200, covering the Ingress / TLS / pod-readiness surface the api's
status field doesn't see.
Metric: instant_deploy_probe_outcome_total{leg,result} +
instant_deploy_probe_latency_seconds{leg} (lazy CounterVec +
HistogramVec). Latency observation suppressed on DNS/TCP errors so
the histogram isn't polluted with 0s timeouts.
Audit: kind=deploy_probe_failed, actor='system:deploy_probe',
team_id=NULL (platform-level).
Schedule: every 60m on the reconcile queue (UniqueOpts guards a
replicas:2 cluster from double-firing). RunOnStart=false — the leg-1
submit is heavyweight (~30s of cluster work per tick) so a worker
restart inside the hour doesn't add useful signal beyond the
previous tick's metric.
Anti-goals (per brief):
- No DELETE: redeploy=true reuses one app row across ticks.
- Synthetic probe team: DEPLOY_PROBE_BEARER_TOKEN owns the row,
not a real customer.
- 60m cadence: a stuck build auto-flips within 30s (worker#65) so
hourly is plenty; 5m would 12x cluster traffic.
- Wildcard *.deployment.instanode.dev is platform-owned (no
customer DNS dependency).
Coverage:
Symptom: /deploy/new pipeline broken (api crash, Kaniko gone,
GHCR auth, Ingress / TLS regression)
Enumeration: rg -F 'deploy_probe' internal/ (3 files; all
deploy_probe.go + test + registration in workers.go)
Sites found: 3
Sites touched: 3
Coverage test: TestPeriodicJobs_AllCarryUniqueOpts iterates the
live periodic-job registry — adding the deploy
probe automatically asserts its UniqueOpts shape.
Live verified: awaiting operator verification of
DEPLOY_PROBE_BEARER_TOKEN secret seed + probe-team
creation (rule 14 + rule 21). PR companion infra/ +
metric/alert/dashboard surface lands same-time per
rule 25.
Operator follow-ups (not blocking PR land):
- Create synthetic probe team via /internal/teams or onboarding;
mint a long-lived JWT, store as k8s secret instant-secrets
DEPLOY_PROBE_BEARER_TOKEN.
- Operator confirms first hourly tick on prod shows result=pass
on all three legs after the rollout (build-SHA gate per rule 14).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 tasks
Two follow-up cleanups so the CI patch-coverage gate at 100% passes:
- Make `*Budget` / `pollInterval` injectable on DeployProbeWorker
(zero = use the package-level constants — production behaviour
unchanged). The four unexported fields let internal tests drive
the degraded-latency + after-deadline + ctx-cancelled-during-poll
branches inside a second of wall-clock instead of needing the real
30s / 90s budgets.
- Decouple serveBudget from the request's ctx timeout (matches the
auth_probe.legEmailStart pattern). Today the leg-3 request was
cancelled by servCtx the moment its budget elapsed, so the
degraded-latency branch on `latency > serveBudget` was provably
unreachable in production (a slow-but-working serve always landed
in http_error). Now the request uses the http client's hard
timeout (deployProbeHTTPTimeout = 120s) and the budget is a pure
post-hoc latency assertion → degraded fires correctly on a slow
serve that completed before the hard ceiling.
- Drop defensive `error` returns from `buildDeployProbeMultipart` +
`buildDeployProbeNginxTarball` — both write to in-memory
bytes.Buffer writers which physically cannot fail. Same posture
as auth_probe.legEmailStart's `_ = json.Marshal(...)`. Removes
the unreachable error branches that were the modal
patch-coverage gap.
- Add `deploy_probe_internal_test.go` (jobs package) with white-box
tests covering: tarball roundtrip, multipart shape, recordLeg's
three branches, legStatus ctx-cancelled-at-top-of-loop +
budget-elapsed + poll-interval-sleep-completes, legSubmit
degraded-latency, legServe degraded-latency + http_error +
build_request, fetchDeployStatus build_request + http_error,
effective-budget zero-fallback + override, and the
CheckRedirect closure on the default client.
Coverage (locally measured against origin/master):
internal/jobs/deploy_probe.go (100%)
internal/jobs/workers.go (100%)
Total: 437 lines, 0 missing — gate passes.
No production behaviour change beyond the serveBudget decoupling
documented above. `make gate` green.
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
DeployProbeWorker— every 60 minutes drives a real end-to-end deploy against the prod/deploy/newpipeline (Kaniko build → k8s pod → public-host GET) and pages NR P0 on any leg failure.Three legs per tick
POST /deploy/newwith embedded nginx tarball (in-memory, no fixture dep),name=deploy-probe-hourly,port=80,env=development,redeploy=true. The redeploy flag reuses one persistent probe-app row forever — no deployment-slot accumulation.GET /deploy/<app_id>every 5s for up to 90s, asserting status flips tohealthy.buildingpast 90s andfailedare both fail-alerts (worker#65 + fix(deploy_failure_autopsy): capture build logs + emit failure email backstop #66 already flip the row within ~30s).GET https://<app_id>.deployment.instanode.dev/expecting 200, covering the Ingress / TLS / pod-readiness surface the api's status field doesn't see.Metrics
instant_deploy_probe_outcome_total{leg,result}(CounterVec) — lazy:pass/degradedmaterialise on the first happy tick;failonly appears on real regression.instant_deploy_probe_latency_seconds{leg}(HistogramVec) — observation suppressed on DNS/TCP errors so the histogram isn't polluted with 0s timeouts.Rule-25 surface — companion infra PR
The metric + alert + dashboard + catalog row land in the same PR-time per CLAUDE.md rule 25:
deploy-probe-fail.jsonNR alert +DeployProbeFailProm rule + three dashboard tiles mirroring the AUTH-004 layout + twoMETRICS-CATALOG.mdrows).Coverage block
Anti-goals (per brief — all honoured)
redeploy=truereuses one row.*.deployment.instanode.devwildcard is platform-owned.Test plan
make gategreen locally (build + vet +go test ./... -short -count=1)go test ./internal/jobs/ -run 'TestDeployProbe|TestValidateDeployProbeBaseURL' -count=1 -short→ok instant.dev/worker/internal/jobs 1.850s(27 tests pass)httptest.Servermocking the api's/deploy/new+/deploy/:id+ public-host surface.instant-secrets.DEPLOY_PROBE_BEARER_TOKEN(long-lived JWT for synthetic probe team)./internal/teamsor onboarding).curl https://api.instanode.dev/healthz | jq .commit_idmatches HEAD.result=passon all three legs (operator confirmation per rule 21).Operator follow-ups (not blocking land)
instant-secrets:kubectl create secret generic instant-secrets --from-literal=DEPLOY_PROBE_BEARER_TOKEN=<jwt> --dry-run=client -o yaml | kubectl apply -f -(then update the worker Deployment env so the var is picked up at pod start).passon all three legs.🤖 Generated with Claude Code