Skip to content

Commit 8fd0342

Browse files
fix(review): strip CR and LF from registered-worktree skip warnings (#9748)
1 parent d6c6db6 commit 8fd0342

2 files changed

Lines changed: 91 additions & 3 deletions

File tree

.github/workflows/qwen-code-pr-review.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1936,15 +1936,16 @@ jobs:
19361936
# Registered paths come from leftover git metadata and are
19371937
# untrusted: the awk filter above matched by substring, so reject
19381938
# `..` traversal and re-anchor to the review prefix before the
1939-
# destructive remove.
1939+
# destructive remove. The skip warnings strip CR/LF from the
1940+
# path for the same reason the ladder's warnings do (above).
19401941
case "$worktree" in
19411942
*/../*|../*|*/..)
1942-
echo "::warning::skipping suspicious review worktree path: $worktree"
1943+
echo "::warning::skipping suspicious review worktree path: ${worktree//[$'\r\n']/ }"
19431944
continue
19441945
;;
19451946
"$GITHUB_WORKSPACE/.qwen/tmp/review-pr-"*) : ;;
19461947
*)
1947-
echo "::warning::skipping unexpected review worktree path: $worktree"
1948+
echo "::warning::skipping unexpected review worktree path: ${worktree//[$'\r\n']/ }"
19481949
continue
19491950
;;
19501951
esac

scripts/tests/review-worktree-cleanup-workflow.test.js

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,39 @@ const runRemoveReviewTree = (workspace, ...args) =>
182182
{ env: { ...process.env, GITHUB_WORKSPACE: workspace }, encoding: 'utf8' },
183183
);
184184

185+
// The skip-warning fixture executes the whole step body with `git`
186+
// stubbed to a function whose `worktree list --porcelain` returns
187+
// hostile registrations: the echoes under test sit in the loop, not in
188+
// git, and the stub keeps the fixture free of real worktree state.
189+
const runReviewCleanStep = (workspace, hostileRegistrations) =>
190+
spawnSync(
191+
'bash',
192+
[
193+
'-c',
194+
[
195+
'set -euo pipefail',
196+
'git() {',
197+
' case " $* " in',
198+
' *" worktree list "*) printf \'%s\\n\' "$HOSTILE_REGISTRATIONS" ;;',
199+
' esac',
200+
'}',
201+
reviewCleanStep,
202+
].join('\n'),
203+
'clean-review-worktrees',
204+
],
205+
{
206+
cwd: workspace,
207+
env: {
208+
...process.env,
209+
GITHUB_WORKSPACE: workspace,
210+
HOSTILE_REGISTRATIONS: hostileRegistrations
211+
.map((path) => `worktree ${path}`)
212+
.join('\n'),
213+
},
214+
encoding: 'utf8',
215+
},
216+
);
217+
185218
// existsSync follows the link: a dangling leftover reports as absent while
186219
// the link itself still survives, so link presence is asserted via lstat.
187220
const linkExists = (path) => {
@@ -342,6 +375,25 @@ describe('review worktree cleanup steps', () => {
342375
expect(
343376
reviewCleanCode.match(/\$\{abs\/\/\[\$'\\r\\n'\]\/ \}/g),
344377
).toHaveLength(2);
378+
// The registered-worktree loop's two skip warnings reach the same
379+
// stdout with an untrusted registered path, so the identical strip
380+
// protects them: a bare `$worktree` there injects a standalone
381+
// workflow-command line on the runner's stdout (executed by the
382+
// CR-bearing-registration fixture below).
383+
const skipWarningLines = reviewCleanCode
384+
.split('\n')
385+
.filter(
386+
(line) =>
387+
line.includes('skipping suspicious review worktree') ||
388+
line.includes('skipping unexpected review worktree'),
389+
);
390+
expect(skipWarningLines).toHaveLength(2);
391+
for (const line of skipWarningLines) {
392+
expect(line).not.toMatch(/\$worktree\b/);
393+
}
394+
expect(
395+
reviewCleanCode.match(/\$\{worktree\/\/\[\$'\\r\\n'\]\/ \}/g),
396+
).toHaveLength(2);
345397
expect(reviewCleanCode).toContain("awk 'NR==1 {print $3}'");
346398
// The failure warning carries the deciding state (sudo probe + owner),
347399
// and the function returns 0 unconditionally: even a failed warning
@@ -588,4 +640,39 @@ describe('review worktree cleanup steps', () => {
588640
}
589641
},
590642
);
643+
it.skipIf(!bashAvailable || !awkAvailable)(
644+
'skip warnings keep a CR-bearing registered path on one runner line',
645+
() => {
646+
const fixture = mkdtempSync(join(tmpdir(), 'review-skip-echo-fixture-'));
647+
try {
648+
// The step exits early without a checkout.
649+
mkdirSync(join(fixture, '.git'));
650+
const hostile = [
651+
// `..` routes to the suspicious-skip echo; the other two fail
652+
// the workspace prefix check and route to the unexpected-skip
653+
// echo.
654+
`${fixture}/.qwen/tmp/review-pr-1/../pwn\r::stop-commands::pwned`,
655+
`/elsewhere/.qwen/tmp/review-pr-2\r::endgroup::`,
656+
`/elsewhere/.qwen/tmp/review-pr-3\r::notice::forged/git`,
657+
];
658+
const out = runReviewCleanStep(fixture, hostile);
659+
expect(out.status).toBe(0);
660+
// The runner splits step stdout on bare CR as well as LF and
661+
// parses every line for workflow commands: the stripped path must
662+
// stay inside its warning line, never surface a standalone `::`
663+
// line.
664+
const lines = out.stdout.split(/[\r\n]/).filter((line) => line);
665+
expect(
666+
lines.filter((line) => line.startsWith('::warning::skipping')),
667+
).toHaveLength(3);
668+
expect(
669+
lines.filter(
670+
(line) => line.startsWith('::') && !line.startsWith('::warning::'),
671+
),
672+
).toEqual([]);
673+
} finally {
674+
rmSync(fixture, { recursive: true, force: true });
675+
}
676+
},
677+
);
591678
});

0 commit comments

Comments
 (0)