Skip to content

feat(api): POST /deploy/new redeploy=true — replace-in-place by name (DOG-30)#201

Merged
mastermanas805 merged 6 commits into
masterfrom
feat/deploy-new-redeploy-in-place
May 30, 2026
Merged

feat(api): POST /deploy/new redeploy=true — replace-in-place by name (DOG-30)#201
mastermanas805 merged 6 commits into
masterfrom
feat/deploy-new-redeploy-in-place

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

Summary

  • Adds an optional redeploy=true multipart form field to POST /deploy/new. When set with a matching name, 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 as POST /deploy/:id/redeploy — same app_id, same URL, same provider_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.
  • Response gains a redeployed: bool field (always present — false on the fresh-deploy path, true on the in-place path). Stable shape across both branches so agents can branch on a single key.
  • Extracts the redeploy compute goroutine into runRedeployAsync so 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 both POST /deploy/:id/redeploy and the new in-place branch).

Error surfaces

code status when
redeploy_requires_name 400 redeploy=true + empty name
no_existing_deployment_to_redeploy 404 redeploy=true but no live row matches (team-scoped — cross-team requests resolve here too, never leaking existence)
not_ready 409 match found but the existing row has no provider_id yet (initial build still running)

Coverage block (rule 17)

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 (handlers/deploy.go:New)
                + MCP `redeploy` separately broken (see cross-repo TODOs)
Sites touched:  1 — handlers/deploy.go:New gains the in-place branch
Coverage tests: TestDeployNew_RedeployTrue_MatchFound_ReusesAppID
                TestDeployNew_RedeployTrue_NoMatch_Returns404
                TestDeployNew_RedeployTrue_NoName_Returns400
                TestDeployNew_RedeployFalse_CreatesNewAsBefore
                TestDeployNew_RedeployTrue_WrongTeam_Returns404
                TestDeployNew_Response_AlwaysIncludesRedeployedField
Live verified:  awaiting user verification after merge + deploy
                (rule 14: curl https://api.instanode.dev/healthz → commit_id match
                 then POST /deploy/new redeploy=true against staging app)

Rule 22 surface checklist

  • api/internal/handlers/openapi.goredeploy boolean on DeployRequest, redeployed boolean on DeployResponse + DeployResponse.item.
  • cross-repo TODOs (call-out for the docs/MCP/infra agents):
    • content/llms.txt + content/api-reference.md — document redeploy=true on POST /deploy/new so agents discover the path
    • mcp/src/index.ts — wire redeploy arg into create_deploy and/or fix the bodyless POST in the existing redeploy tool (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 for instant_deploy_redeploy_total{outcome="not_found"} (rising rate = agents guessing names; pair with list_deployments discovery), plus the success tile (rule 25 — every new metric ships with its alert + tile).
    • infra/observability/METRICS-CATALOG.md — row for instant_deploy_redeploy_total.

Files

  • internal/handlers/deploy.goshouldRedeployInPlace(form) helper; in-place branch in New; runRedeployAsync extracted from Redeploy.
  • internal/models/deployment.goFindActiveDeploymentByTeamEnvName (status filter: building/deploying/healthy/failed; excludes deleted/expired/stopped).
  • internal/models/audit_kinds.goAuditKindDeployRedeployRequested (deploy.redeploy.requested), emitted from both POST /deploy/:id/redeploy and POST /deploy/new in-place; source field distinguishes the two.
  • internal/metrics/metrics.goinstant_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 locally
  • go vet ./... clean
  • make gate — single unrelated flake in TestLinkGitHubID (test-DB pollution from prior runs, users_github_id_key duplicate; passes in isolation: go test -run TestLinkGitHubID -short -count=1 ./internal/models/ → ok)
  • Live verification: after merge + auto-deploy, POST https://api.instanode.dev/deploy/new with redeploy=true against a real deploy, confirm same app_id + URL come back; verify instant_deploy_redeploy_total{outcome="success"} ticks in /metrics

🤖 Generated with Claude Code

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
mastermanas805 force-pushed the feat/deploy-new-redeploy-in-place branch from c78422c to ff798e9 Compare May 30, 2026 12:49
mastermanas805 and others added 6 commits May 30, 2026 19:08
…(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
mastermanas805 force-pushed the feat/deploy-new-redeploy-in-place branch from 2912277 to de9a4ae Compare May 30, 2026 13:38
@mastermanas805
mastermanas805 merged commit 00865ab into master May 30, 2026
15 checks passed
@mastermanas805
mastermanas805 deleted the feat/deploy-new-redeploy-in-place branch May 30, 2026 13:54
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