Skip to content

feat(app): add --no-promote for promotionless deploys#105

Merged
kristof-siket merged 2 commits into
mainfrom
feat/app-deploy-no-promote
Jul 1, 2026
Merged

feat(app): add --no-promote for promotionless deploys#105
kristof-siket merged 2 commits into
mainfrom
feat/app-deploy-no-promote

Conversation

@kristof-siket

Copy link
Copy Markdown
Contributor

What

Adds a --no-promote flag to prisma-cli app deploy — a promotionless deployment that builds a new deployment without flipping the live pointer. Deploy stays promote-by-default; this is strictly opt-in.

When --no-promote is set:

  • The new deployment is built, started, and polled to running, but not promoted.
  • The previous deployment keeps serving live (its own URL and the App's live pointer are unchanged).
  • The new deployment is "built, not live" and reachable only at its own candidate URL.
  • A later prisma-cli app promote <deployment-id> makes it live (existing command, unchanged).

promote / rollback / list-deploys / show-deploy / domain and deploy's default behavior are untouched.

Why — the CI build-then-verify path

CI wants to build a candidate, health-check it against a stable URL, and only then flip production. Promote-on-build gives CI no window to verify before customers see the new code. --no-promote splits the two steps:

prisma-cli app deploy --no-promote --json   # build candidate, get deployment.url
#  → curl / smoke-test result.deployment.url (the candidate's own URL)
prisma-cli app promote <deployment-id>       # flip production once green

Because a promotionless deploy never replaces the live deployment, it bypasses the production-confirmation gate (enforceProductionDeployGatePROD_DEPLOY_REQUIRES_FLAG): app deploy --no-promote on the production branch builds a candidate without --prod and without a confirmation prompt.

Grounding — SDK already supports it (no SDK / management-api change)

@prisma/compute-sdk ComputeClient.deploy(options) accepts skipPromote?: boolean (DeployOptions in compute-client.d.ts). With skipPromote: true, deploy() builds + starts + polls to running and returns:

{ ..., deploymentId, deploymentEndpointDomain, appEndpointDomain: null,
  promoted: false, previousDeploymentId, previousDeploymentAction: "still-active", ... }

deploymentEndpointDomain is the candidate's own URL (what CI health-checks); on the promoted path the SDK instead returns appEndpointDomain: <live url>, promoted: true. This PR only wires the CLI onto that existing SDK behavior.

Changes

  • lib/app/app-provider.tsdeployApp accepts skipPromote, forwards it to sdk.deploy(...), and maps the return by deployed.promoted. When not promoted: app.liveDeploymentId = previousDeploymentId (the still-live old deployment), deployment.live = false, promoted = false. DeployRecord gains promoted and deployment.live. (The liveUrl / deployment.url expressions are unchanged from before — appEndpointDomain is null on the not-promoted path, so they already resolve to the candidate endpoint.)
  • controllers/app.ts — threads noPromote into deployApp({ skipPromote }); skips enforceProductionDeployGate when noPromote (treating firstProductionDeploy as false); surfaces promoted + deployment.live; nextSteps lead with app promote <id> when not promoted.
  • commands/app/index.ts — adds the --no-promote Commander option (same --no-* negatable idiom as the existing --no-db; exposes options.promote === false) and threads noPromote.
  • presenters/app.ts — human output gains a "built, not live" branch: Built <id> in <duration> (not promoted), the candidate URL, a note that the live deployment is unchanged, and a Promote hint. The promoted path is unchanged.
  • types/app.tsAppDeployResult gains promoted and deployment.live. --json (serializeAppDeploy) surfaces result.promoted and the candidate result.deployment.{id,url,live} (field name url kept, so existing consumers reading .result.deployment.url get the candidate URL under --no-promote).
  • docs/product/command-spec.md — documents the flag (signature, behavior, gate bypass, output, deploy-all propagation, example), per this package's doc-driven-development rule.

Tests

Extended the existing suites (562 passing, +7 new):

  • provider: deployApp({ skipPromote: true }) forwards it to sdk.deploy and maps the promotionless return (candidate URL, deployment.live: false, promoted: false, live pointer stays on previousDeploymentId); plus the promoted mapping.
  • controller: --no-promote calls deployApp with skipPromote: true, returns promoted: false + candidate URL + deployment.live: false, and nextSteps include app promote <id>; and --no-promote on the production branch does not call listDeployments (gate bypassed) and deploys without --prod.
  • CLI layer: --no-promote threads noPromote: true into runAppDeploy and --json surfaces promoted: false + the candidate deployment.url.
  • presenter: not-promoted human output (built-not-live + promote hint) and JSON shape.

pnpm --filter @prisma/cli test (562 pass), tsc --noEmit, and biome check all pass; CLI builds; node dist/cli.js app deploy --help shows --no-promote.

Note: the CI rewrite in pdp-control-plane that consumes this flag is a separate follow-up, gated on the CLI release.

🤖 Generated with Claude Code

Add a `--no-promote` flag to `prisma-cli app deploy` that builds a new
deployment without flipping the live pointer. The previous deployment
keeps serving live; the new one is reachable only at its own candidate
URL until a later `app promote <deployment-id>` makes it live.

This is the CI build-then-verify path: build the candidate, health-check
its URL, then promote. Because a promotionless deploy never replaces the
live deployment, it bypasses the production-confirmation gate — so
`app deploy --no-promote` on the production branch works without `--prod`.

Wiring:
- ComputeClient.deploy already supports `skipPromote`; the provider now
  forwards it and maps the not-promoted return (candidate URL, live
  pointer stays on the previous deployment, `promoted: false`).
- Controller threads `noPromote`, skips the gate when set, surfaces
  `promoted` + `deployment.live`, and leads nextSteps with `app promote`.
- Human output renders a built-not-live headline with a promote hint;
  `--json` carries `promoted` and the candidate `deployment.{url,live}`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 17 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fd65ed7a-d344-4775-b266-23cfd717570b

📥 Commits

Reviewing files that changed from the base of the PR and between 0e29f9c and 2417940.

📒 Files selected for processing (2)
  • packages/cli/src/controllers/app.ts
  • packages/cli/tests/app-controller.test.ts

Walkthrough

This PR adds a --no-promote flag to the prisma-cli app deploy command, allowing a candidate deployment to be built without promoting it to live, bypassing the production deployment gate. Changes span the CLI command definition, controller logic (conditional gate enforcement, skipPromote passthrough, conditional next-steps), provider typing and deployment result mapping (promoted/live fields), presenter rendering (conditional headline and promote hint), type definitions, documentation, and corresponding test coverage across controller, CLI, presenter, and provider test suites.

Changes

Cohort: --no-promote flag implementation

  • Documentation: describes new flag syntax, scope, behavior, and output format
  • CLI command: defines --no-promote option and wires it into deploy invocation
  • Provider: adds skipPromote option and promoted/live fields to deployment records
  • Controller: conditionally bypasses production gate, passes skipPromote, computes conditional next-steps
  • Types/Presenter: adds promoted field to result type; renders different output for promoted vs. non-promoted deploys
  • Tests: coverage for new flag behavior across controller, CLI, presenter, and provider layers

Sequence Diagram(s)

Included within the hidden review stack artifact for the provider and controller layers.

Compact Metadata
Estimated Effort: Medium-High
Files Changed: 10

Suggested Labels: cli, enhancement, deploy

Suggested Reviewers: (based on ownership of packages/cli/src/controllers/app.ts and packages/cli/src/lib/app/app-provider.ts)

Poem
A rabbit hops to build, not leap,
A candidate URL, promises to keep,
The live one rests, untouched, serene,
While tests confirm what "promoted" means,
Then app promote — the final beat.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a promotionless deploy flag.
Description check ✅ Passed The description is detailed and directly describes the same no-promote deploy change and its behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/app-deploy-no-promote
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/app-deploy-no-promote

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cli/src/controllers/app.ts (1)

807-830: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Don’t cache a promotionless candidate as the known live deployment.

With skipPromote: noPromote, deployResult.deployment.id is a candidate when deployResult.promoted is false, but Lines 826-830 still persist it as the known live deployment. Store the actual live id instead, or skip this write when no live deployment exists.

Proposed fix
   await context.stateStore.setSelectedApp(projectId, {
     id: deployResult.app.id,
     name: deployResult.app.name,
   });
-  await context.stateStore.setKnownLiveDeployment(
-    projectId,
-    deployResult.app.id,
-    deployResult.deployment.id,
-  );
+  const knownLiveDeploymentId = deployResult.promoted
+    ? deployResult.deployment.id
+    : deployResult.app.liveDeploymentId;
+  if (knownLiveDeploymentId) {
+    await context.stateStore.setKnownLiveDeployment(
+      projectId,
+      deployResult.app.id,
+      knownLiveDeploymentId,
+    );
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/controllers/app.ts` around lines 807 - 830, The deploy flow
in app.ts is persisting the wrong deployment as live when
`skipPromote`/`noPromote` leaves `deployResult.promoted` false. Update the logic
around the `deployResult` handling to use the actual live deployment id only
when a promotion succeeded, and avoid calling `setKnownLiveDeployment` with
`deployResult.deployment.id` for a promotionless candidate; reference
`deployResult.promoted`, `deployResult.app`, and `setKnownLiveDeployment` to
place the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@packages/cli/src/controllers/app.ts`:
- Around line 807-830: The deploy flow in app.ts is persisting the wrong
deployment as live when `skipPromote`/`noPromote` leaves `deployResult.promoted`
false. Update the logic around the `deployResult` handling to use the actual
live deployment id only when a promotion succeeded, and avoid calling
`setKnownLiveDeployment` with `deployResult.deployment.id` for a promotionless
candidate; reference `deployResult.promoted`, `deployResult.app`, and
`setKnownLiveDeployment` to place the fix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 54ca020b-b4ca-4e88-8b8f-33c7b542bf5a

📥 Commits

Reviewing files that changed from the base of the PR and between 0d559ab and 0e29f9c.

📒 Files selected for processing (10)
  • docs/product/command-spec.md
  • packages/cli/src/commands/app/index.ts
  • packages/cli/src/controllers/app.ts
  • packages/cli/src/lib/app/app-provider.ts
  • packages/cli/src/presenters/app.ts
  • packages/cli/src/types/app.ts
  • packages/cli/tests/app-controller.test.ts
  • packages/cli/tests/app-env-vars.test.ts
  • packages/cli/tests/app-presenter.test.ts
  • packages/cli/tests/app-provider.test.ts

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 1, 2026
setKnownLiveDeployment cached the un-promoted candidate as the known-live deployment; with --no-promote that id is not live. Cache the actually-live id instead (the previous deployment that keeps serving), and skip the write when the app has no live deployment yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kristof-siket

Copy link
Copy Markdown
Contributor Author

Good catch — fixed in 2417940.

With --no-promote, setKnownLiveDeployment was persisting the un-promoted candidate (deployResult.deployment.id) as the known-live deployment. It now caches the actually-live id — deployResult.app.liveDeploymentId (the previous deployment that keeps serving) when not promoted, the new deployment when promoted — and skips the write entirely when the app has no live deployment yet (first deploy with --no-promote).

Added a controller test asserting the candidate is not cached as live (the store returns the previous live id after a --no-promote deploy). tsc --noEmit, biome check, and the app suite (101 tests) all pass.

@kristof-siket

Copy link
Copy Markdown
Contributor Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kristof-siket

Copy link
Copy Markdown
Contributor Author

@CodeRabbit approve

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
✅ Action performed

Comments resolved and changes approved.

@kristof-siket
kristof-siket merged commit f152fe3 into main Jul 1, 2026
10 checks passed
@kristof-siket
kristof-siket deleted the feat/app-deploy-no-promote branch July 1, 2026 14:09
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