Skip to content

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

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

feat(cli): add branch remove for preview branches#110
AmanVarshney01 wants to merge 4 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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 27b59bff-940a-4db7-b4a0-499e5719e9ca

📥 Commits

Reviewing files that changed from the base of the PR and between 9c667dc and 97bee23.

📒 Files selected for processing (5)
  • packages/cli/src/commands/app/index.ts
  • packages/cli/src/controllers/app.ts
  • packages/cli/src/presenters/branch.ts
  • packages/cli/tests/app-controller.test.ts
  • packages/cli/tests/branch-remove.test.ts

Summary by CodeRabbit

  • New Features
    • Added branch remove for preview branches with required --confirm, optional --project selection, and --cascade to remove related apps/databases first.
    • Added app remove --branch so apps can be removed from a specified branch even when it isn’t checked out locally.
  • Bug Fixes
    • Prevented removal of production and default branches; added clearer errors for not found, protected, non-empty, and cascade-incomplete cases.
  • Documentation
    • Updated command specs, error conventions, and the resource model for branch removal behavior.
  • Tests
    • Expanded coverage for branch remove --cascade and scoped app removal by branch.

Walkthrough

This pull request adds a new branch remove command with exact branch confirmation, protection checks, non-empty branch handling, optional cascade deletion, updated help text, new branch-related error codes, and related documentation. It also extends app remove so an explicit --branch value can scope app removal to a chosen branch.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
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 documents branch removal, cascade cleanup, error codes, and tests.
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.
✨ 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

Comment thread packages/cli/src/controllers/branch.ts
Comment thread packages/cli/src/presenters/branch.ts
Comment thread packages/cli/tests/branch-remove.test.ts
Comment thread packages/cli/tests/branch-remove.test.ts Outdated
- extract formatRemovedResourceLine helper in the branch presenter
- assert non-cascade removals omit result.removed
- reuse setupLinkedProject in the cascade test suite
- bundle runAppRemove optional inputs into an options object

@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 (3)
packages/cli/src/adapters/mock-api.ts (2)

283-330: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

cascadeBranchResources shares the same protection/liveness policy shape.

This return type and the deployment/database matching logic (branch.projectId/branch.name pairing) duplicate what removeBranch does. If the isBranchProtected/named-type refactor above is applied, consider reusing the same helpers here for consistency, and giving the return shape a name (e.g. BranchCascadeResult) so it can be shared with BranchRemoveResult["removed"] in the types file instead of two independently-typed but structurally identical objects.

🤖 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/adapters/mock-api.ts` around lines 283 - 330,
cascadeBranchResources duplicates the same branch protection and
deployment/database matching logic used by removeBranch, so factor that behavior
through the shared isBranchProtected and branch-lookup helpers instead of
rechecking branch.projectId and branch.name inline. Also name the return shape
in mock-api.ts with a shared type such as BranchCascadeResult, and reuse it
alongside BranchRemoveResult["removed"] in the types so both methods share one
consistent result contract.

332-369: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Name the outcome union type and extract the removal rules.

The inline outcome union on removeBranch duplicates the production || isDefault protection check and the hasDatabases || hasDeployments liveness check that also appear (in slightly different form) in cascadeBranchResources and in controllers/branch.ts. Extracting a named BranchRemovalOutcome type plus small predicates (isBranchProtected, hasLiveBranchResources) would give a single source of truth for this policy and make the method read as an orchestration of named rules rather than inline conditionals.

♻️ Proposed refactor
+type BranchRemovalOutcome =
+  | { outcome: "removed"; branch: BranchRecord }
+  | { outcome: "not-found" }
+  | { outcome: "protected" }
+  | { outcome: "not-empty" };
+
+function isBranchProtected(branch: BranchRecord): boolean {
+  return branch.role === "production" || Boolean(branch.isDefault);
+}
+
+function hasLiveBranchResources(
+  data: MockApiData,
+  branch: BranchRecord,
+): boolean {
+  const hasDatabases = (data.databases ?? []).some(
+    (database) => database.branchId === branch.id,
+  );
+  const hasDeployments = data.deployments.some(
+    (deployment) =>
+      deployment.projectId === branch.projectId &&
+      deployment.branch === branch.name,
+  );
+  return hasDatabases || hasDeployments;
+}
+
-  removeBranch(
-    branchId: string,
-  ):
-    | { outcome: "removed"; branch: BranchRecord }
-    | { outcome: "not-found" }
-    | { outcome: "protected" }
-    | { outcome: "not-empty" } {
+  removeBranch(branchId: string): BranchRemovalOutcome {
     const branch = this.data.branches.find(
       (candidate) => candidate.id === branchId,
     );
     if (!branch) {
       return { outcome: "not-found" };
     }
-    // Mirrors the platform rule: production and default branches are
-    // protected outright.
-    if (branch.role === "production" || branch.isDefault) {
+    if (isBranchProtected(branch)) {
       return { outcome: "protected" };
     }

-    // Mirrors the platform rule: removal is refused while the branch still
-    // has live member resources.
-    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) {
+    if (hasLiveBranchResources(this.data, branch)) {
       return { outcome: "not-empty" };
     }

     this.data.branches = this.data.branches.filter(
       (candidate) => candidate.id !== branchId,
     );
     return { outcome: "removed", branch };
   }

As per PR comments summary: "recommends naming the branch removal result union type instead of keeping it inline" and "extracting branch-removal rules into small helper functions, such as checks for databases, deployments, and live branch resources."

🤖 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/adapters/mock-api.ts` around lines 332 - 369, The
`removeBranch` method currently defines its result union inline and repeats
branch-removal policy checks that should be shared. Introduce a named
`BranchRemovalOutcome` type for the return value, then extract the protection
and liveness logic into small helpers such as `isBranchProtected` and
`hasLiveBranchResources` (with database/deployment checks factored out if
helpful). Update `removeBranch` to orchestrate these helpers instead of
embedding the conditionals, and align the same rules with the related removal
logic used in `cascadeBranchResources` and `controllers/branch.ts`.
packages/cli/src/controllers/branch.ts (1)

125-219: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the protection rule and result construction out of runBranchRemove.

The function currently mixes orchestration (resolving project/branch, confirming, cascading, deleting) with an inline business rule (lines 172-176) and inline response shaping (lines 203-218). Pulling the protection check into a shared predicate (ideally reused with the mock API's isBranchProtected, see the companion comment in mock-api.ts) and extracting a createBranchRemoveResult(...) helper for the return object would let runBranchRemove read purely as a sequence of named steps.

♻️ Proposed refactor (protection check only)
+function assertBranchRemovable(branch: {
+  role: string;
+  isDefault?: boolean;
+  gitName: string;
+}): void {
+  // Production and default branches are protected outright; --cascade never
+  // widens that. The check runs before any member resource is touched.
+  if (branch.role === "production" || branch.isDefault) {
+    throw branchProtectedError(branch.gitName);
+  }
+}
+
   const branch = resolveBranchForRemoval(branchRef, branches, target);

-  // Production and default branches are protected outright; --cascade never
-  // widens that. The check runs before any member resource is touched.
-  if (branch.role === "production" || branch.isDefault) {
-    throw branchProtectedError(branch.gitName);
-  }
+  assertBranchRemovable(branch);

As per PR comments summary: "runBranchRemove should stay focused on orchestration, with separate helpers handling context resolution, confirmation, deletion, and response creation" and "moving branch removal policy into one place."

🤖 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` around lines 125 - 219,
`runBranchRemove` is still mixing orchestration with branch-policy logic and
response shaping. Extract the inline protected-branch check into a shared
predicate (aligned with the mock API’s `isBranchProtected`) and move the final
return-object assembly into a `createBranchRemoveResult(...)` helper. Keep
`runBranchRemove` focused on the sequence of steps: resolve target, resolve
branch, enforce protection, confirm, cascade/delete, then return the
helper-built result.
♻️ Duplicate comments (1)
packages/cli/tests/branch-remove.test.ts (1)

199-216: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate setup fixture still unresolved.

This setup() still reimplements setupLinkedProject() verbatim rather than reusing it, as flagged in a prior review on this same block.

♻️ 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;
🤖 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 199 - 216, The branch
remove test setup is still duplicating the linked-project fixture instead of
reusing the shared helper. Update the `setup()` helper in
`branch-remove.test.ts` to call `setupLinkedProject()` and keep only the
branch-remove-specific steps, so the shared auth/login and `.prisma/local.json`
initialization stays centralized and consistent.
🤖 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/adapters/mock-api.ts`:
- Around line 283-330: cascadeBranchResources duplicates the same branch
protection and deployment/database matching logic used by removeBranch, so
factor that behavior through the shared isBranchProtected and branch-lookup
helpers instead of rechecking branch.projectId and branch.name inline. Also name
the return shape in mock-api.ts with a shared type such as BranchCascadeResult,
and reuse it alongside BranchRemoveResult["removed"] in the types so both
methods share one consistent result contract.
- Around line 332-369: The `removeBranch` method currently defines its result
union inline and repeats branch-removal policy checks that should be shared.
Introduce a named `BranchRemovalOutcome` type for the return value, then extract
the protection and liveness logic into small helpers such as `isBranchProtected`
and `hasLiveBranchResources` (with database/deployment checks factored out if
helpful). Update `removeBranch` to orchestrate these helpers instead of
embedding the conditionals, and align the same rules with the related removal
logic used in `cascadeBranchResources` and `controllers/branch.ts`.

In `@packages/cli/src/controllers/branch.ts`:
- Around line 125-219: `runBranchRemove` is still mixing orchestration with
branch-policy logic and response shaping. Extract the inline protected-branch
check into a shared predicate (aligned with the mock API’s `isBranchProtected`)
and move the final return-object assembly into a `createBranchRemoveResult(...)`
helper. Keep `runBranchRemove` focused on the sequence of steps: resolve target,
resolve branch, enforce protection, confirm, cascade/delete, then return the
helper-built result.

---

Duplicate comments:
In `@packages/cli/tests/branch-remove.test.ts`:
- Around line 199-216: The branch remove test setup is still duplicating the
linked-project fixture instead of reusing the shared helper. Update the
`setup()` helper in `branch-remove.test.ts` to call `setupLinkedProject()` and
keep only the branch-remove-specific steps, so the shared auth/login and
`.prisma/local.json` initialization stays centralized and consistent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a2e2afac-e9f6-493b-8267-208442b28f0b

📥 Commits

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

📒 Files selected for processing (3)
  • packages/cli/src/adapters/mock-api.ts
  • packages/cli/src/controllers/branch.ts
  • packages/cli/tests/branch-remove.test.ts

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