Skip to content

fix: rename branch when recreate-ref deleteRef is blocked by branch protection rules#33163

Merged
pelikhan merged 4 commits into
mainfrom
copilot/propose-new-solutions-for-chaos
May 18, 2026
Merged

fix: rename branch when recreate-ref deleteRef is blocked by branch protection rules#33163
pelikhan merged 4 commits into
mainfrom
copilot/propose-new-solutions-for-chaos

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 18, 2026

chaos/* branches are protected by a ruleset that blocks deletion. When recreate_ref: true and the remote branch already exists, the handler calls DELETE /git/refs/heads/{branch} — which returns HTTP 422 "Repository rule violations found / Cannot delete this branch" — causing a hard failure and fallback-to-issue instead of creating a PR.

Changes

  • create_pull_request.cjshandleRemoteBranchCollision: add a second 422 catch arm matching Cannot delete this branch / Repository rule violations. Instead of throwing, sets deleteBlocked = true and falls through to the existing rename-with-random-suffix path, so the PR is still created under {branch}-{hex}.
  • create_pull_request.test.cjs: new test covering the chaos/* ruleset violation scenario — verifies deleteRef is attempted, a protection-rules warning is emitted, the branch is renamed, and the result is success: true.
// Before: 422 "Cannot delete this branch" → throws → fallback-to-issue
// After:  422 "Cannot delete this branch" → warn → rename to {branch}-{hex} → PR created

All other error paths are unchanged: 422 "Reference does not exist" (concurrent delete) is still treated as success; any other status still throws.

…by branch protection rules

When `recreate_ref: true` and the remote `chaos/*` branch exists, the handler
calls `DELETE /git/refs/heads/{branch}`. If a repository ruleset blocks
deletion (HTTP 422 "Repository rule violations found / Cannot delete this
branch"), the previous code threw an error causing a fallback-to-issue.

Now, that specific 422 variant is caught and the handler falls back to the
existing rename-with-random-suffix strategy instead, so a PR is still created.
All other 422 variants (e.g. "Reference does not exist") and non-422 errors
retain their original behaviour.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan pelikhan marked this pull request as ready for review May 18, 2026 21:23
Copilot AI review requested due to automatic review settings May 18, 2026 21:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a protected-branch fallback for recreate_ref collisions so PR creation can continue when deleting an existing remote ref is blocked by repository rules.

Changes:

  • Handles 422 branch deletion rule violations by renaming the local branch with a random suffix instead of failing.
  • Adds a test for the chaos/* protected branch deletion scenario.
Show a summary per file
File Description
actions/setup/js/create_pull_request.cjs Adds delete-blocked detection and falls through to existing branch renaming behavior.
actions/setup/js/create_pull_request.test.cjs Adds coverage for protected-branch deletion failure fallback.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 1

ref: "heads/chaos/preserve-me",
});
// Should have fallen back to rename with suffix
expect(renameCalled).toBe(true);
@github-actions github-actions Bot mentioned this pull request May 18, 2026
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /diagnose and /tdd — this is a targeted bug fix with a new regression test.

Key Themes

  • Regex precision (/diagnose): The || condition on line 662 includes /Repository rule violations/i which is broader than the actual error case and could silently absorb unrelated 422s. The more specific /Cannot delete this branch/i clause alone is sufficient.
  • Test completeness (/tdd): The new test is well-structured and covers the happy path through the fallback. One missing assertion: the final branch name returned in result.branch is never checked, leaving the rename format unspecified.

Positive Highlights

  • ✅ Clean, minimal code change — deleteBlocked flag is easy to follow and doesn't alter any existing paths
  • ✅ Both GitHub error message variants (Cannot delete this branch and Repository rule violations) are covered defensively
  • ✅ Test is realistic and covers the full observable behaviour (deleteRef called, warning emitted, rename executed, success: true)
  • ✅ PR description is excellent — the before/after comment in the body makes the intent immediately clear

Verdict

Overall a solid, well-scoped fix. Two minor suggestions above — neither is blocking, but the regex tightening in particular is worth considering to avoid future surprises.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · ● 3.2M

// treat that as success and continue.
if (status === 422 && /Reference does not exist/i.test(message)) {
core.info(`Remote branch ${branchName} was already deleted concurrently; continuing`);
} else if (status === 422 && (/Cannot delete this branch/i.test(message) || /Repository rule violations/i.test(message))) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnose] The /Repository rule violations/i branch of the || is quite broad — it will silently swallow any 422 that mentions those words, even violations unrelated to branch deletion (e.g. push rulesets, required status checks on a different flow). The concrete GitHub error message for a blocked delete always includes "Cannot delete this branch", so that first clause is sufficient and more precise. Consider removing the second clause or tightening it to something like /Repository rule violations.*Cannot delete/is.

// Safer: only match the specific phrase that appears when delete is blocked
} else if (status === 422 && /Cannot delete this branch/i.test(message)) {

ref: "heads/chaos/preserve-me",
});
// Should have fallen back to rename with suffix
expect(renameCalled).toBe(true);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] The test verifies renameCalled and the warning messages, but never asserts on the shape of the renamed branch. The result object (from handler(...)) carries the final branch name — asserting it matches the expected pattern would complete the specification:

expect(result.branch).toMatch(/^chaos\/preserve-me-[0-9a-f]{8}$/);

Without this, a refactor that changes the suffix format (e.g. longer hex, different separator) would still pass the test.

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 90/100

Excellent

Metric Value
New/modified tests analyzed 1
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 1 (100%)
Duplicate test clusters 0
Test inflation detected Yes (45 test lines / 13 prod lines ≈ 3.5:1)
🚨 Coding-guideline violations None

Test Classification Details

Test File Classification Issues Detected
should rename with random suffix when deleteRef is blocked by branch protection rules (recreate-ref fallback) actions/setup/js/create_pull_request.test.cjs ✅ Design Minor: test inflation (3.5:1)

Analysis

This test covers the new error-handling branch introduced in create_pull_request.cjs: when deleteRef returns HTTP 422 with a "Repository rule violations / Cannot delete this branch" error, the system must gracefully fall back to appending a random suffix rather than failing hard.

Behavioral assertions verified:

  • result.success === true — the operation succeeds despite the blocked deletion
  • deleteRef was called with the correct parameters — the system attempted deletion before fallback
  • renameCalled === true — the rename actually happened (the fallback path executed)
  • Warning message contains "cannot be deleted due to branch protection rules" — correct user-facing message
  • Warning message contains "appending random suffix" — correct user-facing message

All assertions target observable behavior or user-facing outputs. The mock of global.github.rest.git.deleteRef is appropriate since that is an external GitHub REST API call, not an internal business-logic function.

Test inflation note: 45 test lines were added for 13 production lines (≈3.5:1 ratio, exceeding the 2:1 guideline). This is flagged but not surprising — the test requires significant setup mocking (exec, getExecOutput, github.rest.git.deleteRef) to simulate the protected branch scenario. The complexity is justified.


Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 0 tests
  • 🟨 JavaScript (*.test.cjs, *.test.js): 1 test (vitest)

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). No coding-guideline violations detected.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

References: §26061303760

🧪 Test quality analysis by Test Quality Sentinel · ● 5.5M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 90/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). The new test for the branch-protection 422 fallback path covers the behavioral contract well.

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments

…eleteRef is blocked

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 18, 2026

@copilot review all comments

Addressed in the latest commit. The test now:

  1. Captures the actual renamed branch from the git branch -m call via regex match
  2. Verifies the captured name matches chaos/preserve-me-<8 hex chars>
  3. Asserts pushSignedCommits receives branch: capturedRenamedBranch
  4. Asserts pulls.create receives head: capturedRenamedBranch
  5. Asserts pulls.create is not called with the original chaos/preserve-me

A regression that renames locally but still pushes/creates the PR against the original branch would now fail assertions 3–5.

Copilot AI requested a review from pelikhan May 18, 2026 21:41
@pelikhan pelikhan merged commit ea3fa99 into main May 18, 2026
5 of 7 checks passed
@pelikhan pelikhan deleted the copilot/propose-new-solutions-for-chaos branch May 18, 2026 21:43
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