Skip to content

Detect and handle non-100644 file modes (symlinks, executables) in push_signed_commits#26259

Merged
pelikhan merged 4 commits intomainfrom
copilot/fix-non-100644-file-modes
Apr 14, 2026
Merged

Detect and handle non-100644 file modes (symlinks, executables) in push_signed_commits#26259
pelikhan merged 4 commits intomainfrom
copilot/fix-non-100644-file-modes

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 14, 2026

The GitHub GraphQL createCommitOnBranch mutation only supports mode 100644: symlinks are silently converted to regular files containing the link target path, and executable bits are silently dropped.

Changes

  • Switch diff command: Replace git diff --name-status ${sha}^ ${sha} with git diff-tree -r --raw ${sha} to capture destination file mode per changed file
  • Pre-scan all commits before any GraphQL mutations: File-change collection and mode checks now run as a single pass over all commits before the first createCommitOnBranch call. If a symlink is found, the function throws before any commit is signed, ensuring the remote branch is never left in a diverged state where the git push fallback would be rejected as non-fast-forward.
  • Symlink handling (mode 120000): emit core.warning and throw to trigger the existing git push fallback — symlinks cannot be represented via GraphQL
  • Executable handling (mode 100755): emit core.warning that the executable bit will be lost, but continue with the signed commit (data-loss warning, not corruption)
  • Copy vs Rename fix: Copy (C) entries no longer incorrectly delete the source path — only Rename (R) entries add a deletion for the source
  • Defensive parsing: validate raw diff lines have ≥5 space-separated fields before extracting dstMode/status; validate that renamedPath/copiedPath are present before use; skip malformed lines with a warning

Raw diff format parsed:

:<srcMode> <dstMode> <srcHash> <dstHash> <status>[score]\t<path>[<\t><newPath>]

Lines not starting with : (e.g. the commit SHA header emitted by git diff-tree) are skipped.

Test changes

  • Added 3 integration tests: symlink → fallback with warning, executable → warning + GraphQL proceeds, regular file → no warnings
  • Fixed create_pull_request.test.cjs mock: the copilot-assignee tests were accidentally relying on fs.readFileSync(undefined) throwing under the old diff command. Updated the mock to stub git rev-list as empty so pushSignedCommits exits early, decoupling these tests from the signed-commit path.

Changeset

  • Type: patch
  • Description: Detects non-100644 file modes in signed commit pushes, warns about executable-bit loss, safely falls back for symlinks before GraphQL mutations, and corrects copy-vs-rename handling/defensive raw diff parsing.

Warning

⚠️ Firewall blocked 2 domains

The following domains were blocked by the firewall during workflow execution:

  • ab.chatgpt.com
  • chatgpt.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"
    - "chatgpt.com"

See Network Configuration for more information.

Generated by Changeset Generator for issue #26259 ·



✨ PR Review Safe Output Test - Run 24417642151

💥 [THE END] — Illustrated by Smoke Claude · ● 170.6K ·

…_signed_commits.cjs

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/727c7685-88df-4f8b-8a4e-f8f835e30ceb

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix warnings for non-100644 file modes in push_signed_commits.cjs Detect and handle non-100644 file modes (symlinks, executables) in push_signed_commits Apr 14, 2026
Copilot AI requested a review from pelikhan April 14, 2026 17:05
@pelikhan pelikhan marked this pull request as ready for review April 14, 2026 17:06
Copilot AI review requested due to automatic review settings April 14, 2026 17:06
@github-actions github-actions bot mentioned this pull request Apr 14, 2026
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

Updates pushSignedCommits to detect non-100644 file modes (symlinks/executables) from Git diff metadata so the GraphQL signed-commit path can warn or safely fall back to git push when mode information can’t be represented.

Changes:

  • Switches commit file-change detection to git diff-tree -r --raw <sha> and parses destination modes/statuses.
  • Adds behavior for symlinks (warn + trigger fallback) and executables (warn about mode loss, continue).
  • Adds integration tests for symlink/executable/regular file modes and adjusts a create_pull_request test mock to avoid coupling to signed-commit behavior.
Show a summary per file
File Description
actions/setup/js/push_signed_commits.cjs Parse raw diff-tree output to detect dst file modes and handle symlink/executable cases.
actions/setup/js/push_signed_commits.test.cjs Adds integration tests covering symlink fallback, executable warning, and regular-file no-warning behavior.
actions/setup/js/create_pull_request.test.cjs Makes the exec mock return an empty rev-list result so copilot-assignee tests exit early from pushSignedCommits.

Copilot's findings

Tip

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

  • Files reviewed: 3/3 changed files
  • Comments generated: 3

Comment on lines +150 to +153
} else if (status && (status.startsWith("R") || status.startsWith("C"))) {
// Rename or Copy: filePath = old path, renamedPath = new path
deletions.push({ path: filePath });
if (dstMode === "120000") {
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

The current handling treats copy (status starting with "C") the same as rename by unconditionally adding a deletion for the source path. For a true copy, deleting the source path will remove the original file in the signed commit, which is incorrect. Only add a deletion for renames ("R"), and for copies ("C") keep the source path and only add the new path as an addition.

Copilot uses AI. Check for mistakes.
Comment on lines +153 to +167
if (dstMode === "120000") {
core.warning(`pushSignedCommits: symlink ${renamedPath} cannot be pushed as a signed commit, falling back to git push`);
throw new Error("symlink file mode requires git push fallback");
}
if (dstMode === "100755") {
core.warning(`pushSignedCommits: executable bit on ${renamedPath} will be lost in signed commit (GitHub GraphQL does not support mode 100755)`);
}
const content = fs.readFileSync(path.join(cwd, renamedPath));
additions.push({ path: renamedPath, contents: content.toString("base64") });
} else {
// Added or Modified
const content = fs.readFileSync(path.join(cwd, parts[1]));
additions.push({ path: parts[1], contents: content.toString("base64") });
if (dstMode === "120000") {
core.warning(`pushSignedCommits: symlink ${filePath} cannot be pushed as a signed commit, falling back to git push`);
throw new Error("symlink file mode requires git push fallback");
}
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

Throwing on a symlink inside the per-commit loop can occur after earlier commits have already been pushed via GraphQL (signed commits), and then the catch block falls back to a plain git push. At that point the remote branch head will typically have diverged from local HEAD (remote has extra signed commits), so git push origin <branch> is likely to be rejected as non-fast-forward. To keep fallback reliable, detect unsupported modes for the entire commit range before performing any GraphQL mutations (and go directly to git push), or otherwise ensure the fallback updates the remote safely (e.g., avoid partially pushing signed commits before falling back).

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +161
const paths = pathPart.split("\t");
const filePath = paths[0];
const renamedPath = paths[1]; // only for renames/copies

if (status === "D") {
deletions.push({ path: parts[1] });
} else if (status.startsWith("R") || status.startsWith("C")) {
// Rename or Copy: parts[1] = old path, parts[2] = new path
deletions.push({ path: parts[1] });
const content = fs.readFileSync(path.join(cwd, parts[2]));
additions.push({ path: parts[2], contents: content.toString("base64") });
deletions.push({ path: filePath });
} else if (status && (status.startsWith("R") || status.startsWith("C"))) {
// Rename or Copy: filePath = old path, renamedPath = new path
deletions.push({ path: filePath });
if (dstMode === "120000") {
core.warning(`pushSignedCommits: symlink ${renamedPath} cannot be pushed as a signed commit, falling back to git push`);
throw new Error("symlink file mode requires git push fallback");
}
if (dstMode === "100755") {
core.warning(`pushSignedCommits: executable bit on ${renamedPath} will be lost in signed commit (GitHub GraphQL does not support mode 100755)`);
}
const content = fs.readFileSync(path.join(cwd, renamedPath));
additions.push({ path: renamedPath, contents: content.toString("base64") });
Copy link

Copilot AI Apr 14, 2026

Choose a reason for hiding this comment

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

For rename/copy entries, renamedPath is assumed to exist, but if the raw diff line is malformed (or a path cannot be parsed) this will become undefined and readFileSync(path.join(cwd, renamedPath)) will throw. Since this code is aiming for defensive parsing, add a check that filePath is non-empty and that renamedPath is present for rename/copy statuses; otherwise emit a warning and skip the line.

Copilot uses AI. Check for mistakes.
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.

This has been addressed in the updated implementation — renamedPath/copiedPath are now validated with explicit null checks, and a core.warning is emitted with continue to skip malformed lines gracefully.

📰 BREAKING: Report filed by Smoke Copilot · ● 638.3K

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 80/100

Excellent

Metric Value
New/modified tests analyzed 3
✅ Design tests (behavioral contracts) 3 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 3 (100%)
Duplicate test clusters 0
Test inflation detected ⚠️ Yes — marginal (104 / 51 ≈ 2.04 : 1)
🚨 Coding-guideline violations None

Note: create_pull_request.test.cjs has 8 lines of change but no new test functions — it's a mock-infrastructure fix to make existing tests work with the updated pushSignedCommits behaviour. It is excluded from scoring.


Test Classification Details

Test File Classification Issues Detected
should fall back to git push and warn when commit contains a symlink push_signed_commits.test.cjs:577 ✅ Design None — real git repo, verifies fallback path end-to-end
should warn about executable bit loss but continue with GraphQL signed commit push_signed_commits.test.cjs:624 ✅ Design None — verifies warning emitted and GraphQL payload content
should not warn for regular files (mode 100644) push_signed_commits.test.cjs:657 ✅ Design None — negative assertion confirms no spurious warnings

Flagged Tests — Requires Review

No tests require review. All three new tests are high-value design tests.


Test Inflation — Minor Note

push_signed_commits.test.cjs added 104 lines against 51 production lines (~2.04 : 1 ratio, threshold is 2 : 1). This is marginally over the threshold and is expected for integration tests that must set up real git repositories, create symlinks and executable files, push to bare remotes, and verify remote state. The extra setup code adds genuine assurance. No action required, but worth noting.


Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 0 tests — no Go test files changed in this PR
  • 🟨 JavaScript (*.test.cjs, *.test.js): 3 tests (vitest)

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). All three tests exercise real git repository interactions and verify observable, user-visible behaviour: the symlink fallback path, the executable-bit warning continuation path, and the no-warning baseline for regular files.


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

🧪 Test quality analysis by Test Quality Sentinel · ● 576.5K ·

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: 80/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). All three new integration tests verify real behavioral contracts using actual git repositories and observable outputs.

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments

…ename, add null checks

Agent-Logs-Url: https://github.com/github/gh-aw/sessions/896611d9-098f-40f3-8f4f-f695e6c2e04a

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

Copilot AI commented Apr 14, 2026

@copilot review all comments

Addressed all three review comments in bb1469f:

  1. Copy vs Rename: Copy (C) no longer adds a deletion for the source path — only Rename (R) does.
  2. Symlink timing: The entire file-change collection (including mode checks) is now a pre-scan that runs before any GraphQL mutations. If a symlink is found, it throws before the first createCommitOnBranch call, so the remote branch is never left in a diverged state.
  3. renamedPath/copiedPath null check: Both are validated before use; malformed lines emit a warning and are skipped.

@github-actions
Copy link
Copy Markdown
Contributor

Hey @Copilot 👋 — great work fixing the silent data-loss bug in push_signed_commits around non-standard file modes! This is a solid, well-scoped fix that addresses a real correctness issue with the GitHub GraphQL createCommitOnBranch mutation.

The PR looks ready for maintainer review. Highlights:

  • Focused — cleanly scoped to file-mode detection in the signed-commit push path, with the create_pull_request.test.cjs mock fix properly decoupled as a related housekeeping item.
  • Well-tested — 3 new integration tests cover the symlink fallback, the executable-bit warning path, and the no-warning happy path for regular files.
  • Clear description — the body explains the root cause, the changes, and the test rationale in detail.

This one looks good to go! 🚀

Generated by Contribution Check · ● 1.3M ·

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

✅ All tools validated successfully! Agent Container Smoke Test confirms agent container is ready.

@github-actions
Copy link
Copy Markdown
Contributor

📰 BREAKING: Smoke Copilot is now investigating this pull request. Sources say the story is developing...

@github-actions github-actions bot removed the smoke label Apr 14, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

✨ The prophecy is fulfilled... Smoke Codex has completed its mystical journey. The stars align. 🌟

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

🎬 THE ENDSmoke Claude MISSION: ACCOMPLISHED! The hero saves the day! ✨

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 14, 2026

⚠️ Smoke Gemini failed. Gemini encountered unexpected challenges...

@github-actions
Copy link
Copy Markdown
Contributor

Agent Container Tool Check

Tool Status Version
bash 5.2.21
sh available
git 2.53.0
jq 1.7
yq v4.52.5
curl 8.5.0
gh 2.89.0
node v20.20.2
python3 3.12.3
go 1.24.13
java 21.0.10
dotnet 10.0.201

Result: 12/12 tools available ✅

Overall Status: PASS

🔧 Tool validation by Agent Container Smoke Test · ● 191.4K ·

@github-actions
Copy link
Copy Markdown
Contributor

Smoke test (Codex, run 24417642180)
PRs: #26260 "chore: remove schedule triggers from smoke-* workflows"; #26258 "[slides] Fix toolsets default comment in MCP Servers slide"
GitHub MCP: ✅
Serena MCP: ✅
Playwright: ✅
Web Fetch MCP: ❌ (tool unavailable)
File write: ✅
Bash cat: ✅
Build: ✅
Overall: FAIL

Warning

⚠️ Firewall blocked 2 domains

The following domains were blocked by the firewall during workflow execution:

  • ab.chatgpt.com
  • chatgpt.com

To allow these domains, add them to the network.allowed list in your workflow frontmatter:

network:
  allowed:
    - defaults
    - "ab.chatgpt.com"
    - "chatgpt.com"

See Network Configuration for more information.

🔮 The oracle has spoken through Smoke Codex ·

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.

PR #26259 pre-scans all commits for unsupported file modes (symlinks 120000, executables 100755) before starting any GraphQL mutations — correctly addressing the non-fast-forward fallback issue. Copy handling (C status) correctly avoids deleting the source path. Null checks on renamedPath/copiedPath add good defensive parsing. Overall a solid, well-structured fix.

📰 BREAKING: Report filed by Smoke Copilot · ● 638.3K

// fallback would be rejected as non-fast-forward.
//
// The GitHub GraphQL createCommitOnBranch mutation only supports regular file mode 100644:
// - Symlinks (120000) would be silently converted to regular files containing the link target path
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.

Good approach pre-scanning all commits before starting any GraphQL mutations. This prevents the partial-push problem where a symlink found mid-loop after some commits were already signed would leave the remote in a diverged state.

if (tabIdx === -1) continue;

const modeFields = line.slice(1, tabIdx).split(" "); // strip leading ':'
if (modeFields.length < 5) {
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.

The copy handling (status.startsWith("C")) correctly avoids adding the source path to deletions — only the new destination path is added as an addition. This fixes the original issue where copies were incorrectly treated as renames.

@github-actions
Copy link
Copy Markdown
Contributor

📰 VERDICT: Smoke Copilot has concluded. All systems operational. This is a developing story. 🎤

@github-actions
Copy link
Copy Markdown
Contributor

Commit pushed: f843626

Generated by Changeset Generator

@github-actions
Copy link
Copy Markdown
Contributor

💥 Smoke Test 24417642151 — Claude engine nominal!

Core #1–12: ✅✅✅✅✅✅✅✅✅✅✅✅
PR Review #13–19: ✅✅✅⚠️✅✅⚠️

Overall: ✅ PASS (2 skipped: thread resolve, close PR)

💥 [THE END] — Illustrated by Smoke Claude · ● 170.6K ·

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.

💥 Automated smoke test review - all systems nominal! (Run 24417642151)

💥 [THE END] — Illustrated by Smoke Claude · ● 170.6K

for (const sha of shas) {
/** @type {Array<{path: string, contents: string}>} */
const additions = [];
/** @type {Array<{path: string}>} */
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.

Smoke Test Review (Run 24417642151): The pre-scan approach for detecting unsupported file modes before any GraphQL mutations is a good defensive pattern — it prevents partial signed-commit pushes that would leave the remote in a diverged state.

@@ -574,4 +574,108 @@ describe("push_signed_commits integration tests", () => {
expect(callArg.message.body).toBeUndefined();
});
});
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.

Smoke Test Review (Run 24417642151): New test suite for file mode handling looks comprehensive — covers symlink fallback, executable-bit warning, and the regular-file baseline. Good coverage of the three cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[plan] Warn and handle non-100644 file modes (executables, symlinks) in push_signed_commits.cjs

3 participants