Skip to content

feat(cli): add branch remove for preview branches#110

Open
AmanVarshney01 wants to merge 2 commits into
mainfrom
feat/branch-remove
Open

feat(cli): add branch remove for preview branches#110
AmanVarshney01 wants to merge 2 commits into
mainfrom
feat/branch-remove

Conversation

@AmanVarshney01

@AmanVarshney01 AmanVarshney01 commented Jul 3, 2026

Copy link
Copy Markdown
Member

Preview branch cleanup previously required the Console. Per the team breakout: removal yes, creation no. Branch creation stays implicit (git-push automation and app deploy); there is deliberately no branch create.

$ prisma-cli branch remove feat-login --confirm br_123
✔ Removing branch...
  The branch was removed from the platform. Local Git branches are untouched.

$ prisma-cli branch remove production --confirm br_456
✘ Branch is protected [BRANCH_PROTECTED]

$ prisma-cli branch remove preview --confirm br_123
✘ Branch still has live resources [BRANCH_NOT_EMPTY]
Fix: Rerun with --cascade to remove the branch's apps and databases with it, or remove them individually first.

$ prisma-cli branch remove preview --confirm br_123 --cascade
✔ Removing branch...
  The branch was removed from the platform. Local Git branches are untouched.
  Removed apps: my-app
  Removed databases: preview-db

Design notes

  • Explicit positional target (branch id or git name in the resolved project) plus the exact-id --confirm convention; --yes never satisfies it.
  • Maps the platform's own guarantees to structured codes: BRANCH_PROTECTED (422: production/default branch) and BRANCH_NOT_EMPTY (409: live apps or databases remain). Plain removal never touches member resources. Production/default is also refused client-side before the confirm prompt.
  • Removal is a platform soft-delete via DELETE /v1/branches/{branchId}; local Git branches are untouched.
  • Recovery commands render through the detected package runner; new codes (BRANCH_NOT_FOUND, BRANCH_PROTECTED, BRANCH_NOT_EMPTY, BRANCH_CASCADE_INCOMPLETE) registered in error-conventions.md.

Review follow-ups (this PR, per @luanvdw)

  • app remove --branch <name>: the BRANCH_NOT_EMPTY recovery hint used to point at a flag that did not exist. The flag is real now, so an app can be removed from a branch that is not checked out locally (or from a directory with no git repo at all).
  • branch remove --cascade: explicit opt-in teardown for non-production branches. Guardrails: the protection check (production or default role) runs before any member resource is touched, so --cascade never widens what can be removed; the exact-id --confirm is still required. The CLI removes the branch's apps, then its databases, then the branch, and lists the blast radius in the output and in JSON result.removed. A partial failure stops immediately and reports exactly what was already removed (BRANCH_CASCADE_INCOMPLETE).
  • BRANCH_NOT_EMPTY next steps now lead with the --cascade rerun, then the per-resource path.

Testing: 9 branch-remove integration tests (confirmation gate, git-name removal, protected, not-empty with cascade-first next steps, not-found, explicit --project, cascade success with blast radius, cascade refused on production) plus an app remove --branch controller test; full suite 601 green. Also live-verified end to end against a real workspace: real 409 to BRANCH_NOT_EMPTY mapping, app remove --branch from a git-less directory, cascade removing app + database + branch with the blast radius listed, production refusal with --cascade, wrong --confirm rejected.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This pull request adds a new branch remove CLI command for preview branches with exact-id confirmation, protection checks, non-empty branch handling, optional cascade deletion, updated help text, new error codes, and product documentation. It also extends app remove with a --branch option so app removal can target a branch other than the current local checkout.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Title check ✅ Passed The title clearly summarizes the main change: adding branch removal for preview branches.
Description check ✅ Passed The description matches the changeset and accurately describes the new branch removal behavior and related updates.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/branch-remove
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch feat/branch-remove

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

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 3, 2026
Preview branch cleanup previously required the Console. branch remove
takes an explicit branch id or git name in the resolved project, the
exact-id --confirm convention, and maps the platform's guarantees to
structured codes: BRANCH_PROTECTED for production/default branches
(422) and BRANCH_NOT_EMPTY when live apps or databases remain (409),
so removal never cascades into member resources. Removal is a platform
soft-delete and never touches local Git branches.

Branch creation deliberately stays implicit (git-push automation and
app deploy); there is no branch create, per team alignment.
@ankur-arch

Copy link
Copy Markdown

@AmanVarshney01 here's some feedback on the PR ( in collab with GPT ;))

Maintainability and readability feedback

We should make the branch removal flow easier to read, extend, and safely change over time. The current implementation works, but some parts rely on inline types, repeated string values, and controller-level branching that will become harder to maintain as more branch lifecycle rules are added.

1. Name the branch removal result type

We should avoid defining complex union return types directly inside method signatures.

Instead of:

removeBranch(
  branchId: string,
):
  | { outcome: "removed"; branch: BranchRecord }
  | { outcome: "not-found" }
  | { outcome: "protected" }
  | { outcome: "not-empty" } {

Do this:

type BranchRemoveResult =
  | { outcome: "removed"; branch: BranchRecord }
  | { outcome: "not-found" }
  | { outcome: "protected" }
  | { outcome: "not-empty" };

removeBranch(branchId: BranchId): BranchRemoveResult {

This helps because the method signature becomes easier to scan, and the result now has a clear domain name. It also gives us one place to update the result shape if branch removal gains more outcomes later.


2. Avoid raw strings for important domain states

We should avoid repeating important values like "removed", "not-found", "protected", "not-empty", "production", and "preview" directly in multiple places.

Instead of:

if (branch.role === "production") {
  return { outcome: "protected" };
}

Do this:

const BranchRole = {
  Production: "production",
  Preview: "preview",
} as const;

const BranchRemoveOutcome = {
  Removed: "removed",
  NotFound: "not-found",
  Protected: "protected",
  NotEmpty: "not-empty",
} as const;

if (branch.role === BranchRole.Production) {
  return { outcome: BranchRemoveOutcome.Protected };
}

This helps because these values are part of the product behavior, not incidental strings. Centralizing them makes typos less likely and makes it easier to see the supported branch states and removal outcomes.


3. Extract branch removal rules into small helper functions

We should keep the removal method focused on the flow, not the details of every rule.

Instead of:

const hasDatabases = (this.data.databases ?? []).some(
  (database) => database.branchId === branchId,
);

const hasDeployments = this.data.deployments.some(
  (deployment) =>
    deployment.projectId === branch.projectId &&
    deployment.branch === branch.name,
);

if (hasDatabases || hasDeployments) {
  return { outcome: "not-empty" };
}

Do this:

function hasLiveBranchResources(data: MockApiData, branch: BranchRecord): boolean {
  return hasBranchDatabases(data, branch.id) || hasBranchDeployments(data, branch);
}

function hasBranchDatabases(data: MockApiData, branchId: BranchId): boolean {
  return (data.databases ?? []).some((database) => database.branchId === branchId);
}

function hasBranchDeployments(data: MockApiData, branch: BranchRecord): boolean {
  return data.deployments.some(
    (deployment) =>
      deployment.projectId === branch.projectId &&
      deployment.branch === branch.name,
  );
}

This helps because the main method can read like the product rule: “find branch, reject protected branch, reject branch with live resources, remove branch.” The details are still there, but they are behind names that explain intent.


4. Keep runBranchRemove as orchestration only

We should reduce the amount of decision-making inside runBranchRemove. It currently handles auth, project resolution, branch lookup, confirmation, real-vs-fixture deletion, error mapping, and response shaping.

Instead of growing one large controller function, do this:

export async function runBranchRemove(
  context: CommandContext,
  branchRef: string,
  flags: BranchRemoveFlags,
): Promise<CommandSuccess<BranchRemoveResult>> {
  const removeContext = await resolveBranchRemoveContext(context, flags);
  const branch = resolveBranchForRemoval(branchRef, removeContext.branches, removeContext.target);

  assertBranchCanBeRemoved(branch);
  requireBranchRemoveConfirmation({
    id: branch.id,
    confirm: flags.confirm,
    formatCommand: removeContext.formatCommand,
  });

  await removeBranch(removeContext, branch);

  return createBranchRemoveSuccess(removeContext, branch);
}

This helps because each part has one clear job. When something changes later, such as project resolution, confirmation behavior, API handling, or response formatting, we can update that specific part without re-reading the entire command flow.


5. Use a single mapper from removal outcome to CLI error

We should avoid scattering outcome checks across the controller.

Instead of:

const removed = context.api.removeBranch(branch.id);

if (removed.outcome === "protected") {
  throw branchProtectedError(branch.gitName);
}

if (removed.outcome === "not-empty") {
  throw branchNotEmptyError(branch.gitName, formatCommand);
}

Do this:

const result = context.api.removeBranch(branch.id);
assertBranchRemoveSucceeded(result, branch, formatCommand);
function assertBranchRemoveSucceeded(
  result: BranchRemoveResult,
  branch: RawBranchRecord,
  formatCommand: PrismaCliPackageCommandFormatter,
): void {
  switch (result.outcome) {
    case "removed":
      return;

    case "protected":
      throw branchProtectedError(branch.gitName);

    case "not-empty":
      throw branchNotEmptyError(branch.gitName, formatCommand);

    case "not-found":
      throw branchNotFoundError(branch.gitName);
  }
}

This helps because all outcome-to-error behavior lives in one place. It also makes it easier to check that every possible outcome is handled.


6. Use clearer names for destructive command inputs

We should make destructive command inputs very explicit.

Instead of:

branchRef: string
confirm?: string

Do this:

branchRef: BranchRef
confirmBranchId?: BranchId

This helps because branchRef and confirmBranchId are not the same thing. The branch reference can be a branch id or git name, but confirmation must be the exact branch id. Naming that distinction directly makes the safety rule easier to understand.


7. Move branch removal policy into one place

We should separate the branch removal rules from the mechanics of deleting the branch.

Instead of spreading policy across the mock API, controller, and API response handling, do this:

function getBranchRemoveBlocker(
  branch: BranchRecord,
  resources: BranchResources,
): BranchRemoveBlocker | null {
  if (branch.role === BranchRole.Production) {
    return "protected";
  }

  if (resources.databases.length > 0 || resources.deployments.length > 0) {
    return "not-empty";
  }

  return null;
}

This helps because the product rule becomes reusable and testable on its own: production branches cannot be removed, and branches with live apps or databases cannot be removed. Both the mock path and real path should follow the same rule shape where possible.


8. Prefer readable condition helpers over nested conditionals

We should avoid nested ternaries and complex inline conditionals in command code.

Instead of:

const live = providerLiveDeploymentId
  ? deployment.deployment.id === providerLiveDeploymentId
  : knownLiveDeploymentId
    ? deployment.deployment.id === knownLiveDeploymentId
    : deployment.deployment.live;

Do this:

function resolveDeploymentLiveState(options: {
  deploymentId: string;
  defaultLiveState: boolean;
  providerLiveDeploymentId?: string;
  knownLiveDeploymentId?: string;
}): boolean {
  if (options.providerLiveDeploymentId) {
    return options.deploymentId === options.providerLiveDeploymentId;
  }

  if (options.knownLiveDeploymentId) {
    return options.deploymentId === options.knownLiveDeploymentId;
  }

  return options.defaultLiveState;
}

This helps because the reader does not need to mentally parse the condition tree. The helper name explains the intent, and the branching becomes easier to test.


Overall recommendation

We should keep the current behavior, but make the implementation more explicit:

  • name important result types
  • centralize branch roles and removal outcomes
  • extract branch removal rules into small helpers
  • keep the controller focused on orchestration
  • map domain outcomes to CLI errors in one place
  • make destructive command inputs clearly named

This will reduce cognitive complexity and make the branch removal flow safer to extend as more branch lifecycle behavior is added.

@luanvdw luanvdw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks @AmanVarshney01

If I tried this correctly, the BRANCH_NOT_EMPTY recovery experience currently sends the user into a dead end. The flow:

  1. User runs prisma-cli branch remove preview --confirm br_123
  2. CLI correctly refuses because the branch still has live resources
  3. CLI suggests removing the child app with prisma-cli app remove --app <name> --branch preview
  4. That command fails, because app remove does not currently support --branch

So the user is blocked, follows the CLI’s next step, and gets unknown option '--branch'. Can we add --branch support to app remove rather than only changing the message, because branch cleanup naturally needs “remove this app from this branch, then retry branch removal.”

Let me know if I miss-understood the flow.

Then lastly, (as a follow up, maybe as a separate PR) I think we should support an explicit cascade path for non-production branches. Blocking by default is reasonable, but users should not have to manually tear down every child resource forever. Something like:

prisma-cli branch remove feat-login --confirm br_123 --cascade

The important guardrail is that cascade should only be allowed when the resolved branch is not role=production, and it should stay refused for production/default branches. Ideally the cascade response also makes the blast radius clear by listing the apps/databases that will be removed, so this is explicit destructive intent rather than a hidden side effect of confirming the branch id.

wdyt?

Review feedback from #110:

- app remove now accepts --branch, honored as-is like the other
  read-branch commands, so branch cleanup can target apps on branches
  that are not checked out locally. The BRANCH_NOT_EMPTY recovery
  command previously pointed at this flag before it existed, which was
  a dead end.

- branch remove --cascade removes the branch's apps, then its
  databases, then the branch, for preview branches only: production
  and default branches stay refused before any member resource is
  touched. The result lists every removed resource (human output and
  result.removed in JSON) so the blast radius is explicit. Cascade is
  client-orchestrated because the platform's branch delete refuses
  non-empty branches; a mid-cascade failure stops immediately with
  BRANCH_CASCADE_INCOMPLETE, whose meta lists what was already removed.

- BRANCH_NOT_EMPTY recovery steps now offer the cascade rerun first,
  then individual app/database cleanup.

@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.

Actionable comments posted: 4

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)

2041-2066: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider an options object for runAppRemove's growing parameter list.

runAppRemove now takes 5 positional args (context, appName, projectRef, configTarget, branchName), most optional strings — easy to transpose at call sites and hard to extend further. Bundling appName/projectRef/configTarget/branchName into a single options object would improve call-site clarity and align with the sibling branch-remove cohort's push toward clearer, named inputs.

♻️ Suggested direction
-export async function runAppRemove(
-  context: CommandContext,
-  appName: string | undefined,
-  projectRef?: string,
-  configTarget?: string,
-  branchName?: string,
-): Promise<CommandSuccess<AppRemoveResult>> {
+export async function runAppRemove(
+  context: CommandContext,
+  appName: string | undefined,
+  options: {
+    projectRef?: string;
+    configTarget?: string;
+    branchName?: string;
+  } = {},
+): Promise<CommandSuccess<AppRemoveResult>> {
+  const { projectRef, configTarget, branchName } = options;
🤖 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 2041 - 2066, `runAppRemove`
has a growing positional parameter list that is easy to misuse at call sites.
Refactor it to accept a single options object for the app-specific inputs
(`appName`, `projectRef`, `configTarget`, `branchName`) while keeping `context`
separate, and update the `runAppRemove` implementation to destructure those
named fields before calling `resolveComputeManagementContext` and
`requireProviderAndProjectContext`. Then update every caller to pass named
properties so the signature is clearer and easier to extend.
🤖 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.

Inline comments:
In `@packages/cli/src/controllers/branch.ts`:
- Line 47: The fixture-backed branch flow is dropping the isDefault flag, so
default-branch protection can be bypassed in tests. Update the branch handling
path in branch.ts to preserve and pass through isDefault from fixture/mock data
just like the real API path does, and make sure the mock-api.ts BranchRecord
includes the field. Add a regression test around branch remove to verify default
branches still hit the protection check in fixture mode.

In `@packages/cli/src/presenters/branch.ts`:
- Around line 88-100: The branch presenter in the details array is inlining
duplicated ternary logic and hardcoded removal messages; extract this into a
small helper used by branch summary rendering. Centralize the removed-resource
copy for apps and databases so the presenter just calls a helper from branch.ts
rather than nesting conditionals inline, and reuse the helper to format
live-resource summary text consistently.

In `@packages/cli/tests/branch-remove.test.ts`:
- Around line 55-75: The branch removal test only checks a partial object, so it
does not verify that non-cascade removals omit the removed payload. Update the
assertion in branch-remove.test.ts for the branch.remove JSON result to
explicitly check the result shape from executeCli/payload, and add a negative
assertion that result.removed is absent when removing the preview branch by git
name. Keep the existing checks for command, projectId, and branch identity, but
make the test fail if removed is unexpectedly included.
- Around line 156-173: The branch remove --cascade test has a duplicate setup
helper that repeats setupLinkedProject() logic, so refactor setup() to reuse
setupLinkedProject() instead of reimplementing the auth/login and local project
fixture steps. Keep the cascade-specific test setup focused only on its extra
needs, and reference the existing setupLinkedProject() helper so both tests stay
aligned if the fixture changes.

---

Outside diff comments:
In `@packages/cli/src/controllers/app.ts`:
- Around line 2041-2066: `runAppRemove` has a growing positional parameter list
that is easy to misuse at call sites. Refactor it to accept a single options
object for the app-specific inputs (`appName`, `projectRef`, `configTarget`,
`branchName`) while keeping `context` separate, and update the `runAppRemove`
implementation to destructure those named fields before calling
`resolveComputeManagementContext` and `requireProviderAndProjectContext`. Then
update every caller to pass named properties so the signature is clearer and
easier to extend.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 26d87594-0344-4a67-bc9e-2f8a59ef1e25

📥 Commits

Reviewing files that changed from the base of the PR and between b7c4576 and f437d87.

📒 Files selected for processing (15)
  • docs/product/command-spec.md
  • docs/product/error-conventions.md
  • docs/product/resource-model.md
  • packages/cli/src/adapters/mock-api.ts
  • packages/cli/src/commands/app/index.ts
  • packages/cli/src/commands/branch/index.ts
  • packages/cli/src/controllers/app.ts
  • packages/cli/src/controllers/branch.ts
  • packages/cli/src/presenters/branch.ts
  • packages/cli/src/shell/command-meta.ts
  • packages/cli/src/types/branch.ts
  • packages/cli/tests/app-controller.test.ts
  • packages/cli/tests/branch-remove.test.ts
  • packages/cli/tests/branch.test.ts
  • packages/cli/tests/shell.test.ts

id: string;
gitName: string;
role: BranchRole;
isDefault?: boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map relevant files/symbols first
ast-grep outline packages/cli/src/controllers/branch.ts --view expanded || true

printf '\n--- branch.ts lines ---\n'
sed -n '1,260p' packages/cli/src/controllers/branch.ts

printf '\n--- search isDefault and BranchRecord ---\n'
rg -n "isDefault|BranchRecord|listBranchesForProject|listBranches\(" packages/cli/src -S

Repository: prisma/prisma-cli

Length of output: 13917


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline packages/cli/src/controllers/branch.ts --view expanded || true

printf '\n--- branch.ts lines ---\n'
sed -n '1,260p' packages/cli/src/controllers/branch.ts

printf '\n--- search isDefault and BranchRecord ---\n'
rg -n "isDefault|BranchRecord|listBranchesForProject|listBranches\(" packages/cli/src -S

Repository: prisma/prisma-cli

Length of output: 13917


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n--- mock-api BranchRecord and listBranchesForProject ---\n'
sed -n '1,140p' packages/cli/src/adapters/mock-api.ts
sed -n '250,310p' packages/cli/src/adapters/mock-api.ts
sed -n '390,430p' packages/cli/src/adapters/mock-api.ts

printf '\n--- branch.ts listBranches and toBranchSummary ---\n'
sed -n '470,525p' packages/cli/src/controllers/branch.ts

printf '\n--- app-provider branch mapping ---\n'
sed -n '760,900p' packages/cli/src/lib/app/app-provider.ts

printf '\n--- tests mentioning branch protection/default ---\n'
rg -n "branchProtectedError|isDefault|default branches|branch remove|branch.list|production" packages/cli/src -g '*test*' -g '*spec*' -S

Repository: prisma/prisma-cli

Length of output: 10653


Map isDefault through fixture branches
Fixture-backed branch remove drops isDefault, so a default branch in test runs bypasses the protection check. Real mode already forwards isDefault from the platform API, so this is limited to the mock/fixture path. Add isDefault to mock-api.ts's BranchRecord, pass it through here, and add a regression test.

🤖 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/branch.ts` at line 47, The fixture-backed branch
flow is dropping the isDefault flag, so default-branch protection can be
bypassed in tests. Update the branch handling path in branch.ts to preserve and
pass through isDefault from fixture/mock data just like the real API path does,
and make sure the mock-api.ts BranchRecord includes the field. Add a regression
test around branch remove to verify default branches still hit the protection
check in fixture mode.

Comment on lines +88 to +100
details: [
"The branch was removed from the platform. Local Git branches are untouched.",
...(result.removed
? [
result.removed.apps.length > 0
? `Removed apps: ${result.removed.apps.map((app) => app.name).join(", ")}`
: "No apps were on the branch.",
result.removed.databases.length > 0
? `Removed databases: ${result.removed.databases.map((database) => database.name).join(", ")}`
: "No databases were on the branch.",
]
: []),
],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract removed-resource summary into a helper.

The apps/databases ternary pair duplicates shape and hardcodes outcome strings inline. Per PR feedback on this stack, centralize removal-outcome copy and extract a small helper for live-resource summarization instead of inlining nested ternaries in the presenter.

♻️ Suggested extraction
+function formatRemovedResourceLine(
+  label: string,
+  resources: BranchRemovedResource[],
+): string {
+  return resources.length > 0
+    ? `Removed ${label}: ${resources.map((r) => r.name).join(", ")}`
+    : `No ${label} were on the branch.`;
+}
+
 details: [
   "The branch was removed from the platform. Local Git branches are untouched.",
   ...(result.removed
-    ? [
-        result.removed.apps.length > 0
-          ? `Removed apps: ${result.removed.apps.map((app) => app.name).join(", ")}`
-          : "No apps were on the branch.",
-        result.removed.databases.length > 0
-          ? `Removed databases: ${result.removed.databases.map((database) => database.name).join(", ")}`
-          : "No databases were on the branch.",
-      ]
+    ? [
+        formatRemovedResourceLine("apps", result.removed.apps),
+        formatRemovedResourceLine("databases", result.removed.databases),
+      ]
     : []),
 ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
details: [
"The branch was removed from the platform. Local Git branches are untouched.",
...(result.removed
? [
result.removed.apps.length > 0
? `Removed apps: ${result.removed.apps.map((app) => app.name).join(", ")}`
: "No apps were on the branch.",
result.removed.databases.length > 0
? `Removed databases: ${result.removed.databases.map((database) => database.name).join(", ")}`
: "No databases were on the branch.",
]
: []),
],
function formatRemovedResourceLine(
label: string,
resources: BranchRemovedResource[],
): string {
return resources.length > 0
? `Removed ${label}: ${resources.map((r) => r.name).join(", ")}`
: `No ${label} were on the branch.`;
}
details: [
"The branch was removed from the platform. Local Git branches are untouched.",
...(result.removed
? [
formatRemovedResourceLine("apps", result.removed.apps),
formatRemovedResourceLine("databases", result.removed.databases),
]
: []),
],
🤖 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/presenters/branch.ts` around lines 88 - 100, The branch
presenter in the details array is inlining duplicated ternary logic and
hardcoded removal messages; extract this into a small helper used by branch
summary rendering. Centralize the removed-resource copy for apps and databases
so the presenter just calls a helper from branch.ts rather than nesting
conditionals inline, and reuse the helper to format live-resource summary text
consistently.

Comment on lines +55 to +75
it("removes an empty preview branch by git name", async () => {
const { cwd, stateDir } = await setupLinkedProject();

const result = await executeCli({
argv: ["branch", "remove", "staging", "--confirm", "br_345", "--json"],
cwd,
stateDir,
fixturePath,
});
const payload = JSON.parse(result.stdout);

expect(result.exitCode).toBe(0);
expect(payload).toMatchObject({
ok: true,
command: "branch.remove",
result: {
projectId: "proj_123",
branch: { id: "br_345", name: "staging", role: "preview" },
},
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assertion doesn't actually verify removed is absent.

toMatchObject permits extra keys, so this test would still pass if result.removed were unexpectedly present for a non-cascade removal.

     expect(payload).toMatchObject({
       ok: true,
       command: "branch.remove",
       result: {
         projectId: "proj_123",
         branch: { id: "br_345", name: "staging", role: "preview" },
       },
     });
+    expect(payload.result).not.toHaveProperty("removed");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it("removes an empty preview branch by git name", async () => {
const { cwd, stateDir } = await setupLinkedProject();
const result = await executeCli({
argv: ["branch", "remove", "staging", "--confirm", "br_345", "--json"],
cwd,
stateDir,
fixturePath,
});
const payload = JSON.parse(result.stdout);
expect(result.exitCode).toBe(0);
expect(payload).toMatchObject({
ok: true,
command: "branch.remove",
result: {
projectId: "proj_123",
branch: { id: "br_345", name: "staging", role: "preview" },
},
});
});
it("removes an empty preview branch by git name", async () => {
const { cwd, stateDir } = await setupLinkedProject();
const result = await executeCli({
argv: ["branch", "remove", "staging", "--confirm", "br_345", "--json"],
cwd,
stateDir,
fixturePath,
});
const payload = JSON.parse(result.stdout);
expect(result.exitCode).toBe(0);
expect(payload).toMatchObject({
ok: true,
command: "branch.remove",
result: {
projectId: "proj_123",
branch: { id: "br_345", name: "staging", role: "preview" },
},
});
expect(payload.result).not.toHaveProperty("removed");
});
🤖 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/tests/branch-remove.test.ts` around lines 55 - 75, The branch
removal test only checks a partial object, so it does not verify that
non-cascade removals omit the removed payload. Update the assertion in
branch-remove.test.ts for the branch.remove JSON result to explicitly check the
result shape from executeCli/payload, and add a negative assertion that
result.removed is absent when removing the preview branch by git name. Keep the
existing checks for command, projectId, and branch identity, but make the test
fail if removed is unexpectedly included.

Comment on lines +156 to +173
describe("branch remove --cascade", () => {
async function setup() {
const cwd = await createTempCwd();
const stateDir = path.join(cwd, ".state");
await executeCli({
argv: ["auth", "login", "--provider", "github", "--user", "usr_456"],
cwd,
stateDir,
fixturePath,
});
await mkdir(path.join(cwd, ".prisma"), { recursive: true });
await writeFile(
path.join(cwd, ".prisma/local.json"),
`${JSON.stringify({ workspaceId: "ws_123", projectId: "proj_123" }, null, 2)}\n`,
"utf8",
);
return { cwd, stateDir };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate setup fixture.

This setup() reimplements setupLinkedProject() (Lines 9-25) verbatim. Reuse it to avoid the two drifting apart.

♻️ Proposed fix
 describe("branch remove --cascade", () => {
-  async function setup() {
-    const cwd = await createTempCwd();
-    const stateDir = path.join(cwd, ".state");
-    await executeCli({
-      argv: ["auth", "login", "--provider", "github", "--user", "usr_456"],
-      cwd,
-      stateDir,
-      fixturePath,
-    });
-    await mkdir(path.join(cwd, ".prisma"), { recursive: true });
-    await writeFile(
-      path.join(cwd, ".prisma/local.json"),
-      `${JSON.stringify({ workspaceId: "ws_123", projectId: "proj_123" }, null, 2)}\n`,
-      "utf8",
-    );
-    return { cwd, stateDir };
-  }
+  const setup = setupLinkedProject;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe("branch remove --cascade", () => {
async function setup() {
const cwd = await createTempCwd();
const stateDir = path.join(cwd, ".state");
await executeCli({
argv: ["auth", "login", "--provider", "github", "--user", "usr_456"],
cwd,
stateDir,
fixturePath,
});
await mkdir(path.join(cwd, ".prisma"), { recursive: true });
await writeFile(
path.join(cwd, ".prisma/local.json"),
`${JSON.stringify({ workspaceId: "ws_123", projectId: "proj_123" }, null, 2)}\n`,
"utf8",
);
return { cwd, stateDir };
}
describe("branch remove --cascade", () => {
const setup = setupLinkedProject;
🤖 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/tests/branch-remove.test.ts` around lines 156 - 173, The branch
remove --cascade test has a duplicate setup helper that repeats
setupLinkedProject() logic, so refactor setup() to reuse setupLinkedProject()
instead of reimplementing the auth/login and local project fixture steps. Keep
the cascade-specific test setup focused only on its extra needs, and reference
the existing setupLinkedProject() helper so both tests stay aligned if the
fixture changes.

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.

3 participants