feat(api): POST /deploy/new redeploy=true — replace-in-place by name (DOG-30)#201
Merged
Merged
Conversation
mastermanas805
added a commit
that referenced
this pull request
May 30, 2026
Adds two test files for the POST /deploy/new redeploy=true in-place path introduced in 0d6dff4 (DOG-30). Brings diff-cov on internal/handlers/deploy.go from 73.5% -> 94.0% by pinning every reachable error arm. deploy_redeploy_inplace_whitebox_test.go (package handlers): - TestShouldRedeployInPlace_NilForm (covers 205-207) - TestShouldRedeployInPlace_FalsyValues (covers 215-216 default arm) - TestShouldRedeployInPlace_TruthyValues (sanity-pins truthy contract) - TestShouldRedeployInPlace_MissingField (covers !ok arm) - TestShouldRedeployInPlace_EmptyValuesSlice (covers len==0 arm) deploy_redeploy_inplace_mock_test.go (package handlers, sqlmock): - TestDeployNew_Redeploy_LookupDriverError_Returns503 (covers 678-683) - TestDeployNew_Redeploy_WrongTeam_DefenceInDepth (covers 689-695) - TestDeployNew_Redeploy_UpdateStatusError_StillAccepts (covers 707-710) - TestDeployNew_Redeploy_EmptyProviderID_Returns409 (covers 700-704) - TestDeployNew_Redeploy_MissingName_DocsTrail (doc breadcrumb, skipped) Per-function coverage delta on deploy.go: shouldRedeployInPlace 75.0% -> 100.0% New 60.4% -> 94.8% Coverage block (rule 17): Symptom: PR #201 diff-cov 73.5% blocks the patch-cov CI gate Enumeration: `diff-cover coverage.xml --compare-branch=origin/master` Sites found: 8 uncovered ranges (206-7, 215-6, 655-61, 678-83, 689-95, 701-4, 708-10, plus 654) Sites touched: 7 of 8 Coverage test: see test names above; each pins one diff-cov arm Live verified: not applicable (test-only change, no runtime behaviour delta) Known gap (1 of 8 arms not covered): Lines 655-661 are the body of `if name == "" { ... }` inside the redeploy branch. requireName() at line 604 always returns a non-empty trimmed string OR an error (see provision_helper.go:799-834) — every empty/whitespace/control-char input is rejected with name_required BEFORE the redeploy branch is entered, so the inner check is genuine defence-in-depth that is unreachable via the HTTP surface today. Covering it would require either (a) a production-code seam to swap out requireName in tests (forbidden refactor) or (b) a coverage waiver (forbidden). Flagging here so a reviewer can decide between shipping at 94% diff-cov, adding a tiny test-only seam, or relaxing the diff-cov gate for unreachable arms. Local test run: go test ./internal/handlers/ -run 'TestDeployNew_Redeploy|TestShouldRedeployInPlace' -count=1 -short -> ok (all PASS, 1 SKIP for the doc-breadcrumb) Pre-existing local-only flakes NOT caused by this change: - internal/models TestLinkGitHubID (hardcoded gh_id, race condition) - internal/handlers TestQueue_* + TestProvisionFinal2_* (need customer DB not reachable on a bare laptop — known gap per Makefile gate comment) Both fail identically on origin/master without these test files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mastermanas805
added a commit
that referenced
this pull request
May 30, 2026
Adds two test files for the POST /deploy/new redeploy=true in-place path introduced in 0d6dff4 (DOG-30). Brings diff-cov on internal/handlers/deploy.go from 73.5% -> 94.0% by pinning every reachable error arm. deploy_redeploy_inplace_whitebox_test.go (package handlers): - TestShouldRedeployInPlace_NilForm (covers 205-207) - TestShouldRedeployInPlace_FalsyValues (covers 215-216 default arm) - TestShouldRedeployInPlace_TruthyValues (sanity-pins truthy contract) - TestShouldRedeployInPlace_MissingField (covers !ok arm) - TestShouldRedeployInPlace_EmptyValuesSlice (covers len==0 arm) deploy_redeploy_inplace_mock_test.go (package handlers, sqlmock): - TestDeployNew_Redeploy_LookupDriverError_Returns503 (covers 678-683) - TestDeployNew_Redeploy_WrongTeam_DefenceInDepth (covers 689-695) - TestDeployNew_Redeploy_UpdateStatusError_StillAccepts (covers 707-710) - TestDeployNew_Redeploy_EmptyProviderID_Returns409 (covers 700-704) - TestDeployNew_Redeploy_MissingName_DocsTrail (doc breadcrumb, skipped) Per-function coverage delta on deploy.go: shouldRedeployInPlace 75.0% -> 100.0% New 60.4% -> 94.8% Coverage block (rule 17): Symptom: PR #201 diff-cov 73.5% blocks the patch-cov CI gate Enumeration: `diff-cover coverage.xml --compare-branch=origin/master` Sites found: 8 uncovered ranges (206-7, 215-6, 655-61, 678-83, 689-95, 701-4, 708-10, plus 654) Sites touched: 7 of 8 Coverage test: see test names above; each pins one diff-cov arm Live verified: not applicable (test-only change, no runtime behaviour delta) Known gap (1 of 8 arms not covered): Lines 655-661 are the body of `if name == "" { ... }` inside the redeploy branch. requireName() at line 604 always returns a non-empty trimmed string OR an error (see provision_helper.go:799-834) — every empty/whitespace/control-char input is rejected with name_required BEFORE the redeploy branch is entered, so the inner check is genuine defence-in-depth that is unreachable via the HTTP surface today. Covering it would require either (a) a production-code seam to swap out requireName in tests (forbidden refactor) or (b) a coverage waiver (forbidden). Flagging here so a reviewer can decide between shipping at 94% diff-cov, adding a tiny test-only seam, or relaxing the diff-cov gate for unreachable arms. Local test run: go test ./internal/handlers/ -run 'TestDeployNew_Redeploy|TestShouldRedeployInPlace' -count=1 -short -> ok (all PASS, 1 SKIP for the doc-breadcrumb) Pre-existing local-only flakes NOT caused by this change: - internal/models TestLinkGitHubID (hardcoded gh_id, race condition) - internal/handlers TestQueue_* + TestProvisionFinal2_* (need customer DB not reachable on a bare laptop — known gap per Makefile gate comment) Both fail identically on origin/master without these test files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mastermanas805
force-pushed
the
feat/deploy-new-redeploy-in-place
branch
from
May 30, 2026 12:49
c78422c to
ff798e9
Compare
…(DOG-30)
Closes the agent-UX duplicate-URL gap surfaced 2026-05-30: three
POST /deploy/new calls for "truehomie-web" minted three distinct app_ids
+ URLs because there was no way to upsert by name. MCP `redeploy` existed
but required the caller to already know the app_id of the first deploy —
not a discovery path an agent has.
The fix is an optional `redeploy` multipart form field on /deploy/new.
When `redeploy=true` (truthy values: true/1/yes) + `name` is supplied, the
handler looks up (team_id, env, env_vars._name) for the most-recent
non-terminal deployment and routes through the same compute path as
POST /deploy/:id/redeploy (same app_id, same URL, same provider_id).
Branch fires AFTER tarball+name+env validation and BEFORE the per-tier
deployments_apps cap (a redeploy reuses an existing slot — must not
consume a new one) and BEFORE generateAppID().
Coverage block:
Symptom: POST /deploy/new with same name fans out to N app_ids/URLs
Enumeration: rg -F 'generateAppID' rg -F '/deploy/new' rg -F 'CreateDeploymentWithCap'
Sites found: 1 in api (deploy.go:New); MCP `redeploy` separately broken (see PR body)
Sites touched: 1 — api/internal/handlers/deploy.go:New gains the in-place branch
Coverage test: TestDeployNew_RedeployTrue_MatchFound_ReusesAppID +
TestDeployNew_RedeployFalse_CreatesNewAsBefore +
TestDeployNew_RedeployTrue_NoMatch_Returns404 +
TestDeployNew_RedeployTrue_NoName_Returns400 +
TestDeployNew_RedeployTrue_WrongTeam_Returns404 +
TestDeployNew_Response_AlwaysIncludesRedeployedField
Live verified: pending PR merge + deploy (awaiting user verification of live curl)
Files:
- internal/handlers/deploy.go — `shouldRedeployInPlace(form)` helper,
in-place branch in New(), `runRedeployAsync` extracted from Redeploy so
the compute path (vault refs, env strip, compute.Redeploy, status, audit,
autopsy, build-log fetch) lives in exactly one place.
- internal/models/deployment.go — `FindActiveDeploymentByTeamEnvName`
lookup keyed on env_vars->>'_name' (the same key the dashboard surfaces
as `name`). Status filter: building/deploying/healthy/failed — excludes
deleted/expired/stopped.
- internal/models/audit_kinds.go — `AuditKindDeployRedeployRequested`
emitted from both /deploy/:id/redeploy AND /deploy/new in-place, with
source="redeploy_endpoint" | "deploy_new_in_place".
- internal/metrics/metrics.go — `instant_deploy_redeploy_total{outcome=...}`
Prom counter labels: success / not_found / wrong_team / missing_name.
- internal/handlers/openapi.go — `redeploy` boolean on DeployRequest,
`redeployed` boolean on DeployResponse + DeployResponse.item.
Cross-repo TODOs (rule 22 surface checklist for the docs agent):
- content/llms.txt — document `redeploy=true` on POST /deploy/new
- content/api-reference.md — same
- mcp/src/index.ts — wire `redeploy` arg into create_deploy, OR fix the
bodyless POST in the existing `redeploy` tool (mcp:1444); this PR
unblocks the API side but the MCP tool still needs the multipart fix
- infra/k8s/prometheus-rules.yaml + infra/newrelic/alerts/*.json +
infra/newrelic/dashboards/instanode-reliability.json — add alert + tile
for `instant_deploy_redeploy_total{outcome="not_found"}` (rising rate =
agents guessing names) and the success path
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds two test files for the POST /deploy/new redeploy=true in-place path introduced in 0d6dff4 (DOG-30). Brings diff-cov on internal/handlers/deploy.go from 73.5% -> 94.0% by pinning every reachable error arm. deploy_redeploy_inplace_whitebox_test.go (package handlers): - TestShouldRedeployInPlace_NilForm (covers 205-207) - TestShouldRedeployInPlace_FalsyValues (covers 215-216 default arm) - TestShouldRedeployInPlace_TruthyValues (sanity-pins truthy contract) - TestShouldRedeployInPlace_MissingField (covers !ok arm) - TestShouldRedeployInPlace_EmptyValuesSlice (covers len==0 arm) deploy_redeploy_inplace_mock_test.go (package handlers, sqlmock): - TestDeployNew_Redeploy_LookupDriverError_Returns503 (covers 678-683) - TestDeployNew_Redeploy_WrongTeam_DefenceInDepth (covers 689-695) - TestDeployNew_Redeploy_UpdateStatusError_StillAccepts (covers 707-710) - TestDeployNew_Redeploy_EmptyProviderID_Returns409 (covers 700-704) - TestDeployNew_Redeploy_MissingName_DocsTrail (doc breadcrumb, skipped) Per-function coverage delta on deploy.go: shouldRedeployInPlace 75.0% -> 100.0% New 60.4% -> 94.8% Coverage block (rule 17): Symptom: PR #201 diff-cov 73.5% blocks the patch-cov CI gate Enumeration: `diff-cover coverage.xml --compare-branch=origin/master` Sites found: 8 uncovered ranges (206-7, 215-6, 655-61, 678-83, 689-95, 701-4, 708-10, plus 654) Sites touched: 7 of 8 Coverage test: see test names above; each pins one diff-cov arm Live verified: not applicable (test-only change, no runtime behaviour delta) Known gap (1 of 8 arms not covered): Lines 655-661 are the body of `if name == "" { ... }` inside the redeploy branch. requireName() at line 604 always returns a non-empty trimmed string OR an error (see provision_helper.go:799-834) — every empty/whitespace/control-char input is rejected with name_required BEFORE the redeploy branch is entered, so the inner check is genuine defence-in-depth that is unreachable via the HTTP surface today. Covering it would require either (a) a production-code seam to swap out requireName in tests (forbidden refactor) or (b) a coverage waiver (forbidden). Flagging here so a reviewer can decide between shipping at 94% diff-cov, adding a tiny test-only seam, or relaxing the diff-cov gate for unreachable arms. Local test run: go test ./internal/handlers/ -run 'TestDeployNew_Redeploy|TestShouldRedeployInPlace' -count=1 -short -> ok (all PASS, 1 SKIP for the doc-breadcrumb) Pre-existing local-only flakes NOT caused by this change: - internal/models TestLinkGitHubID (hardcoded gh_id, race condition) - internal/handlers TestQueue_* + TestProvisionFinal2_* (need customer DB not reachable on a bare laptop — known gap per Makefile gate comment) Both fail identically on origin/master without these test files. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ng_deployment_to_redeploy Coverage agent left 6 lines uncovered (diff-cov 94%) at deploy.go:655-661 — the 'if name == ""' arm inside the redeploy=true branch. That arm is unreachable: requireName() at the top of the handler always returns either an error (handler bails) or a non-empty trimmed string, so by line 654 name is provably non-empty. Removing the arm — not adding a test seam to fake-reach it — is the honest fix. Also closes a sibling miss from the upstream coverage agent: the 404 'no_existing_deployment_to_redeploy' error code was emitted but never registered in codeToAgentAction. TestErrorCode_HasAgentAction failed on HEAD; adding the registry entry restores 100% coverage of error codes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt_action contract Two registry-iterating tests caught real misses on this PR: - TestAgentActionContract — no_existing_deployment_to_redeploy agent_action didn't start with 'Tell the user' nor include https://instanode.dev/ URL. - TestReliability_AuditKinds_EveryConstantHasConsumerSpec — the new AuditKindDeployRedeployRequested constant had no auditConsumerSpec entry. Both fixed: agent_action rewritten + consumer spec marked IntentionallyNoConsumer (the rebuild's deploy.healthy is the user-facing success signal; the redeploy.requested row is an audit-trail breadcrumb). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mastermanas805
force-pushed
the
feat/deploy-new-redeploy-in-place
branch
from
May 30, 2026 13:38
2912277 to
de9a4ae
Compare
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
redeploy=truemultipart form field toPOST /deploy/new. When set with a matchingname, the handler looks up the team's most-recent non-terminal deployment for (team_id, env, env_vars._name) and routes through the same compute path asPOST /deploy/:id/redeploy— sameapp_id, same URL, sameprovider_id. Closes the duplicate-URL incident (2026-05-30) where three identical calls for "truehomie-web" minted three distinct subdomains because there was no upsert-by-name path.redeployed: boolfield (always present —falseon the fresh-deploy path,trueon the in-place path). Stable shape across both branches so agents can branch on a single key.runRedeployAsyncso the vault-ref resolution, internal-env-key stripping,compute.Redeploy, status update, audit emit, failure-autopsy capture, and build-log fetch all live in ONE place (called from bothPOST /deploy/:id/redeployand the new in-place branch).Error surfaces
redeploy_requires_nameredeploy=true+ emptynameno_existing_deployment_to_redeployredeploy=truebut no live row matches (team-scoped — cross-team requests resolve here too, never leaking existence)not_readyprovider_idyet (initial build still running)Coverage block (rule 17)
Rule 22 surface checklist
api/internal/handlers/openapi.go—redeployboolean on DeployRequest,redeployedboolean on DeployResponse + DeployResponse.item.content/llms.txt+content/api-reference.md— documentredeploy=trueonPOST /deploy/newso agents discover the pathmcp/src/index.ts— wireredeployarg intocreate_deployand/or fix the bodyless POST in the existingredeploytool (mcp/src/index.ts:1444— server requires multipart tarball, current MCP tool sends no body and gets 400). This PR unblocks the API side but the MCP wedge needs the matching client change.infra/k8s/prometheus-rules.yaml+infra/newrelic/alerts/*.json+infra/newrelic/dashboards/instanode-reliability.json— add alert + dashboard tile forinstant_deploy_redeploy_total{outcome="not_found"}(rising rate = agents guessing names; pair withlist_deploymentsdiscovery), plus the success tile (rule 25 — every new metric ships with its alert + tile).infra/observability/METRICS-CATALOG.md— row forinstant_deploy_redeploy_total.Files
internal/handlers/deploy.go—shouldRedeployInPlace(form)helper; in-place branch inNew;runRedeployAsyncextracted fromRedeploy.internal/models/deployment.go—FindActiveDeploymentByTeamEnvName(status filter: building/deploying/healthy/failed; excludes deleted/expired/stopped).internal/models/audit_kinds.go—AuditKindDeployRedeployRequested(deploy.redeploy.requested), emitted from bothPOST /deploy/:id/redeployandPOST /deploy/newin-place;sourcefield distinguishes the two.internal/metrics/metrics.go—instant_deploy_redeploy_total{outcome="success|not_found|wrong_team|missing_name"}.internal/handlers/openapi.go— schema updates.internal/handlers/deploy_redeploy_inplace_test.go— 6 new tests.Test plan
go test -run 'TestDeployNew_Redeploy|TestDeployNew_Response_AlwaysIncludesRedeployedField' -short -count=1 ./internal/handlers/— all 6 PASS locallygo vet ./...cleanmake gate— single unrelated flake inTestLinkGitHubID(test-DB pollution from prior runs,users_github_id_keyduplicate; passes in isolation:go test -run TestLinkGitHubID -short -count=1 ./internal/models/→ ok)POST https://api.instanode.dev/deploy/newwithredeploy=trueagainst a real deploy, confirm sameapp_id+ URL come back; verifyinstant_deploy_redeploy_total{outcome="success"}ticks in/metrics🤖 Generated with Claude Code