fix(durability): gate installHelper's exec-bit chmod on dryRun (#3736) (#3759)

installHelper's "already current" fast path ran chmodSync(helperPath,
0o755) unconditionally, before the dryRun check further down — so
`gbrain sources harden --dry-run` mutated the helper's permissions even
though dry-run is documented as a pure preview. This is the 5th
instance of the #3594 class (see #3692 for the sibling fix to the
step-1 pull, same function).

Gate the chmod with `if (!dryRun)`, mirroring the guard pattern already
used in installLocalHook's own "already current" branch. Non-dry-run
behavior is unchanged.

Adds a regression pair to test/brain-repo-durability.serial.test.ts:
one asserting dry-run leaves a drifted-permission helper untouched,
and a control asserting the real run still restores the exec bit.
This commit is contained in:
Masa
2026-08-03 19:37:18 +08:00
committed by GitHub
parent 2e554500e2
commit fa313f2969
2 changed files with 19 additions and 2 deletions
+3 -2
View File
@@ -394,8 +394,9 @@ function installHelper(repoPath: string, dryRun: boolean): { status: StepStatus;
const helperPath = join(repoPath, HELPER_REL);
const script = renderCommitPushHelper();
if (existsSync(helperPath) && readFileSync(helperPath, 'utf-8') === script) {
// Ensure exec bit even when content is current.
try { chmodSync(helperPath, 0o755); } catch { /* */ }
// Ensure exec bit even when content is current — but not in dry-run: a
// preview must not mutate permissions (#3736).
if (!dryRun) { try { chmodSync(helperPath, 0o755); } catch { /* */ } }
return { status: 'ok', detail: `${HELPER_REL} already current` };
}
if (dryRun) return { status: 'fixed', detail: `would write ${HELPER_REL} (dry-run)` };
+16
View File
@@ -169,6 +169,22 @@ describe('hardenBrainRepo', () => {
expect(commitCount(work)).toBe(before);
expect(existsSync(join(work, 'scripts', 'brain-commit-push.sh'))).toBe(false);
});
test('dry-run does not chmod an already-current helper (#3736)', async () => {
await harden(); // real run installs scripts/brain-commit-push.sh at 0o755
const helperPath = join(work, 'scripts', 'brain-commit-push.sh');
chmodSync(helperPath, 0o644); // simulate perms drifting away from +x, content unchanged
await harden({ dryRun: true });
expect(statSync(helperPath).mode & 0o777).toBe(0o644); // untouched — preview must not mutate
});
test('non-dry-run restores the exec bit on an already-current helper', async () => {
await harden();
const helperPath = join(work, 'scripts', 'brain-commit-push.sh');
chmodSync(helperPath, 0o644);
await harden();
expect(statSync(helperPath).mode & 0o111).toBeTruthy(); // exec bit restored
});
});
describe('unhardenBrainRepo', () => {