Skip to content

Improve signed-commits push refusal rendering in fallback issue bodies#47056

Merged
pelikhan merged 4 commits into
mainfrom
copilot/improve-merge-issues-error-template
Jul 21, 2026
Merged

Improve signed-commits push refusal rendering in fallback issue bodies#47056
pelikhan merged 4 commits into
mainfrom
copilot/improve-merge-issues-error-template

Conversation

Copilot AI commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

When create_pull_request falls back to an issue after a signed-commits push refusal (e.g. merge commit topology), the error message was emitted as a single sanitized line — a wall of unreadable text buried in a blockquote.

Changes

  • create_pull_request_helpers.cjs — adds buildPushErrorSection(rawErrorMessage, sanitizedErrorMessage):

    • Detects pushSignedCommits: refusing unsigned push pattern
    • Extracts the specific cause (e.g. merge commit detected, submodule change detected)
    • Returns a structured blockquote block with cause, a bullet list of unsupported shapes with modes, and remediation steps
    • Falls back to the existing > **Original error:** … single line for all other errors
  • create_pull_request.cjs — uses buildPushErrorSection in both the bundle-path and patch-path push-failure fallback bodies, replacing the inline > **Original error:** ${pushFailureMessage} line

  • create_pull_request_helpers.test.cjs — 8 new unit tests covering detection, cause extraction, shape listing, remediation text, blockquote-only output invariant, and edge cases

Before

> **Original error:** pushSignedCommits: refusing unsigned push for branch 'my-branch': merge commit detected. GitHub's createCommitOnBranch GraphQL mutation cannot represent merge commits, symlinks (mode 120000), submodule entries (mode 160000), or executable bits (mode 100755). Rewrite the commits to use only regular files (mode 100644) with no merge commits, or set signed-commits: false if the repository does not require signed commits.

After

> **Error:** Signed commit push refused — merge commit detected
>
> GitHub's `createCommitOnBranch` GraphQL API cannot represent:
> - Merge commits
> - Symlinks (mode `120000`)
> - Submodule entries (mode `160000`)
> - Executable files (mode `100755`)
>
> **To fix:** Use `git rebase` instead of `git merge` to incorporate upstream changes,
> or set `signed-commits: false` in your workflow step if signed commits are not required.

…odies

Add `buildPushErrorSection` helper that detects signed-commits refusal
errors and expands them into structured markdown with cause, a list of
unsupported commit shapes, and remediation steps (git rebase / signed-
commits: false), instead of rendering as a wall of text.

Use it in both the bundle-path and patch-path fallback issue bodies
in create_pull_request.cjs, and add 8 unit tests in the helpers test file.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Improve push signed commits merge error rendering in fallback issue bodies Improve signed-commits push refusal rendering in fallback issue bodies Jul 21, 2026
Copilot AI requested a review from pelikhan July 21, 2026 10:17
@pelikhan
pelikhan marked this pull request as ready for review July 21, 2026 11:06
Copilot AI review requested due to automatic review settings July 21, 2026 11:06

Copilot AI left a comment

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.

Pull request overview

Improves fallback issue readability for signed-commit push refusals.

Changes:

  • Adds structured push-error rendering.
  • Integrates it into bundle and patch fallback paths.
  • Adds helper unit tests.
Show a summary per file
File Description
actions/setup/js/create_pull_request.cjs Uses structured error sections in fallback issues.
actions/setup/js/create_pull_request_helpers.cjs Implements signed-push error formatting.
actions/setup/js/create_pull_request_helpers.test.cjs Tests the new formatter.

Review details

Tip

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

  • Files reviewed: 3/3 changed files
  • Comments generated: 3
  • Review effort level: Medium

* @returns {string} One or more blockquote lines for the error section (no leading/trailing blank lines).
*/
function buildPushErrorSection(rawErrorMessage, sanitizedErrorMessage) {
if (!/pushSignedCommits: refusing unsigned push/.test(rawErrorMessage)) {
}

// Extract the specific cause (e.g. "merge commit detected") embedded in the raw message.
const causeMatch = rawErrorMessage.match(/refusing unsigned push for branch '[^']*': ([^.]+)/);
Comment on lines +282 to +283
`> **To fix:** Use \`git rebase\` instead of \`git merge\` to incorporate upstream changes,`,
`> or set \`signed-commits: false\` in your workflow step if signed commits are not required.`,
@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #47056 does not have the 'implementation' label and has 0 new lines of code in business logic directories (threshold: 100).

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

@github-actions

github-actions Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions github-actions Bot left a comment

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.

Review Summary

The structured error rendering for signed-commits push refusals is a clear readability improvement. Three issues from inline comments warrant attention:

  1. Overly broad pattern match — The regex also matches PushSignedCommitsPolicyViolation errors (max-patch, file-protection), incorrectly rendering those as signed-commits shape errors.
  2. Branch-name regex fragility — The cause extraction regex fails silently for branch names containing an apostrophe.
  3. Inaccurate remediation for non-merge-commit causes — git rebase only helps for merge commits; symlink/submodule/executable-bit causes need cause-specific guidance.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 13.9 AIC · ⌖ 4.37 AIC · ⊞ 4.9K

@github-actions github-actions Bot mentioned this pull request Jul 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 71/100 — Acceptable

Analyzed 8 test(s): 6 design, 2 implementation, 0 violation(s).

📊 Metrics (8 tests)
Metric Value
Analyzed 8 (Go: 0, JS: 8)
✅ Design 6 (75%)
⚠️ Implementation 2 (25%)
Edge/error coverage 3 (37.5%)
Duplicate clusters 0
Inflation No (1.39:1)
🚨 Violations 0
Test File Classification Issues
returns a generic fallback line for non-signed-commits errors create_pull_request_helpers.test.cjs ✅ design_test
returns structured markdown for a signed-commits refusal create_pull_request_helpers.test.cjs ✅ design_test
extracts the cause from the error message create_pull_request_helpers.test.cjs ⚠️ implementation_test Checks internal string extraction detail
lists all unsupported commit shapes create_pull_request_helpers.test.cjs ⚠️ implementation_test Checks specific output strings
includes remediation steps mentioning git rebase and signed-commits: false create_pull_request_helpers.test.cjs ✅ design_test
produces only blockquote lines create_pull_request_helpers.test.cjs ✅ design_test Format invariant
uses a generic cause when the branch name pattern does not match create_pull_request_helpers.test.cjs ✅ design_test Edge case
handles submodule change detected cause create_pull_request_helpers.test.cjs ✅ design_test Variant cause

Verdict

Passed. 25% implementation tests (threshold: 30%). No guideline violations.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 24.4 AIC · ⌖ 7.12 AIC · ⊞ 6.9K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

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.

✅ Test Quality Sentinel: 71/100. 25% implementation tests (threshold: 30%).

@github-actions github-actions Bot left a comment

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.

Skills-Based Review 🧠

Applied /tdd and /grill-with-docs — requesting changes on three correctness issues in the new helper.

📋 Key Themes & Highlights

Key Findings

  • Overly broad pattern match (line 265): pushSignedCommits: also prefixes PushSignedCommitsPolicyViolation errors (max-patch, file-protection), which will be incorrectly rendered as topology errors with wrong remediation steps.
  • Regex fragility (line 270): Branch names with apostrophes break the cause-extraction regex; a malformed result silently falls back to the generic phrase.
  • Inaccurate remediation (line 283): git rebase fixes merge-commit topology but does not help for submodule or symlink causes — those need content removal, not rebase.
  • Hardcoded bullet list (line 277): All four shapes are listed for every cause, even when only one is relevant, which can confuse users.

Positive Highlights

  • ✅ Excellent test suite — 8 focused tests covering detection, extraction, invariants, and fallback
  • ✅ Clean JSDoc with embedded usage example
  • ✅ Graceful fallback for unrecognised errors
  • ✅ Both push-path call sites updated consistently

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 25.4 AIC · ⌖ 4.58 AIC · ⊞ 6.6K
Comment /matt to run again

`> **Error:** Signed commit push refused — ${cause}`,
`>`,
`> GitHub's \`createCommitOnBranch\` GraphQL API cannot represent:`,
`> - Merge commits`,

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 bullet list always enumerates all four unsupported shapes regardless of which cause was detected, which can mislead users.

If the cause is submodule change detected, listing Merge commits as equally applicable implies the commit has both problems. The test suite does not verify this — a test that checks only relevant shapes appear for each cause would catch this.

💡 Suggested improvement

Either scope the bullet list to shapes relevant to the detected cause, or make the intro more neutral:

`> GitHub's \`createCommitOnBranch\` GraphQL API does not support any of the following commit shapes:`

A coverage-based test would look like:

it('does not list merge commits for submodule cause', () => {
  const raw = "pushSignedCommits: refusing unsigned push for branch 'b': submodule change detected.";
  expect(buildPushErrorSection(raw, raw)).not.toContain('Merge commits');
});

@copilot please address this.

@github-actions github-actions Bot left a comment

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.

REQUEST_CHANGES — two issues must be fixed before merge.

Blocking findings

1. Unsanitized cause embedded in markdown output (high)

The cause captured from rawErrorMessage is interpolated directly into the returned string. rawErrorMessage is the unprocessed git error at the call site — it bypasses sanitizeContent and neutralizeClosingKeywordsForIssueBody. If an attacker controls the remote error text or branch name, they can inject arbitrary markdown into the fallback issue body (e.g. mention aliases, issue-closing keywords, links). The safe string — sanitizedErrorMessage — is passed to the function but never used for cause extraction. See inline comment at line 272.

2. [^.]+ regex silently truncates causes containing a period (medium)

Any cause string with a . (e.g. executable file at path ./run.sh detected) is cut off at the dot, leaving a misleading partial message. See inline comment at line 268.

Already flagged by earlier comments (not duplicated): over-broad detection pattern (#265), apostrophe in branch name breaking the regex (#270), and git rebase advice not applicable to symlink/submodule causes (#283). Those issues remain open and should be addressed together with these.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 43.5 AIC · ⌖ 4.69 AIC · ⊞ 5.6K
Comment /review to run again

// Extract the specific cause (e.g. "merge commit detected") embedded in the raw message.
const causeMatch = rawErrorMessage.match(/refusing unsigned push for branch '[^']*': ([^.]+)/);
const cause = causeMatch ? causeMatch[1].trim() : "unsupported commit shape";

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.

Unsanitized cause injected into markdown: cause is captured from rawErrorMessage — the unprocessed git error — and interpolated directly into the output string without any escaping. A crafted remote branch name or git error text could embed arbitrary markdown (extra blockquote lines, @mentions, closing keywords, links) into the fallback issue body.

💡 Suggested fix

Extract the cause from sanitizedErrorMessage instead, or sanitize the captured group before embedding it:

const causeMatch = sanitizedErrorMessage.match(/refusing unsigned push for branch [^:]*: ([^.]+)/);
const cause = causeMatch ? causeMatch[1].trim() : "unsupported commit shape";

sanitizedErrorMessage is already processed through sanitizeContent + neutralizeClosingKeywordsForIssueBody at the call site. The raw message is fine for the guard regex, but any captured text that lands in the output must come from the sanitized path.

if (!/pushSignedCommits: refusing unsigned push/.test(rawErrorMessage)) {
return `> **Original error:** ${sanitizedErrorMessage}`;
}

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.

[^.]+ cause regex truncates at the first period: any cause text containing a . (e.g. executable file at path ./scripts/run.sh detected) is silently truncated, leaving a misleading partial message in the issue body.

💡 Suggested fix

Capture up to the sentence-ending period more carefully, or use a greedier stop before the next \n or end-of-line:

// Stop at '. ' (period+space) or end-of-line instead of any period
const causeMatch = rawErrorMessage.match(/refusing unsigned push for branch '[^']*': ([^\n]+?)(?:\. |$)/);

Alternatively, if the upstream error format always puts the cause between the colon and the first . , use a non-greedy match against . specifically:

/refusing unsigned push for branch '[^']*': (.+?)(?=\. )/

Either way, add a test case with a cause that contains a period character.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher and lint js

Copilot AI and others added 2 commits July 21, 2026 12:07
…oilerplate, fix apostrophe regex, add cause-specific remediation

- Issue 1: tighten detection to require 'createCommitOnBranch GraphQL mutation cannot represent' boilerplate so PushSignedCommitsPolicyViolation errors (max-patch, file-protection) still use the sanitized original-error fallback
- Issue 2: change '[^']*' to lazy '.*?' and anchor cause extraction to the boilerplate with (?=\. GitHub's createCommitOnBranch) so branch names containing apostrophes are matched correctly
- Issue 3: extract _remediationForCause() with cause-specific guidance: merge commit → git rebase, submodule → remove submodule entry, symlink → remove symlink, default → shape-neutral rewrite
- Tests: add policy-violation regression test, apostrophe-branch test, symlink remediation test; update submodule test to assert cause-specific text and absence of git rebase; update malformed test to use boilerplate-present-but-no-branch-pattern input

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 21, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher and lint js

Done — addressed all three reviewer comments and linted:

  • Detection scope (line 265): now requires both pushSignedCommits: refusing unsigned push and createCommitOnBranch GraphQL mutation cannot represent, so PushSignedCommitsPolicyViolation errors (max-patch, file-protection) fall back to the sanitized original-error line. Added a regression test.
  • Apostrophe in branch name (line 270): changed [^']* → lazy '.*?' and anchored the cause extraction to the boilerplate with (?=\. GitHub's createCommitOnBranch). Added an apostrophe-branch test.
  • Cause-specific remediation (lines 282–283): extracted _remediationForCause() that selects guidance by cause — merge commit → git rebase, submodule → remove the submodule entry, symlink → remove the symlink, default → shape-neutral rewrite. Updated the submodule test to assert the cause-specific text and absence of git rebase; added a symlink test.

All 80 unit tests pass, make lint-cjs passes with zero errors.

@pelikhan
pelikhan merged commit fc1ecb8 into main Jul 21, 2026
8 checks passed
@pelikhan
pelikhan deleted the copilot/improve-merge-issues-error-template branch July 21, 2026 12:18
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