diff --git a/.github/scripts/qwen-triage-workflow.test.mjs b/.github/scripts/qwen-triage-workflow.test.mjs index 22cb58192d..4590ee7b6f 100644 --- a/.github/scripts/qwen-triage-workflow.test.mjs +++ b/.github/scripts/qwen-triage-workflow.test.mjs @@ -1014,14 +1014,52 @@ describe('qwen-triage: flakiness gate (#9125)', () => { // executes, so it is a changed test file exactly like M. assert.match( recordStep.run, - /^\s*git -c core\.quotePath=false diff -z --name-only --diff-filter=ACMRT "\$BASE_OID" HEAD \\\n\s*> "\$GATE_HOME\/files-all"$/m, + /^\s*\/usr\/bin\/git -c core\.quotePath=false diff -z --name-only --diff-filter=ACMRT "\$BASE_OID" HEAD \\\n\s*> "\$\{RUNNER_TEMP:\?\}\/flake-record-files-all"$/m, 'the NUL diff must flow straight into its file — $( ) strips NUL bytes, a pipeline swallows the exit status', ); assert.match( recordStep.run, - /^\s*BASE_OID="\$\(cat "\$\{RUNNER_TEMP:\?\}\/verify-base-oid"\)"$/m, + /^\s*rm -rf -- "\$\{RUNNER_TEMP:\?\}\/flake-record-files-all"\n\s*\/usr\/bin\/git -c core\.quotePath=false diff -z/m, + 'the staging path must be unlinked immediately before the redirect — a planted symlink or directory there makes root write through it or hard-fail the record step', + ); + assert.match( + recordStep.run, + /^\s*BASE_OID="\$\(\/usr\/bin\/cat "\$\{RUNNER_TEMP:\?\}\/verify-base-oid"\)"$/m, 'the record step must diff against the base OID captured while .git was root-owned, not re-resolve HEAD^1', ); + assert.match( + recordStep.run, + /^\s*case "\$BASE_OID" in$/m, + 'the base OID must be shape-validated in the parent arm before the diff', + ); + assert.match( + recordStep.run, + /^\s*\[0-9a-f\]\[0-9a-f\]\[0-9a-f\]\[0-9a-f\]\[0-9a-f\]\[0-9a-f\]\[0-9a-f\]\[0-9a-f\]\*\) ;;/m, + 'the base OID shape must be an 8+-hex prefix — a planted valid OID would yield an empty diff and starve the gate into n/a', + ); + assert.match( + recordStep.run, + /^\s*cp "\$\{RUNNER_TEMP:\?\}\/flake-record-files-all" "\$GATE_HOME\/files-all"$/m, + 'the scrubbed child must copy the parent-recorded diff, never re-run git under env -i', + ); + const recordDiffAt = recordStep.run.search(/^\s*\/usr\/bin\/git -c core\.quotePath=false diff -z/m); + const recordReExecAt = recordStep.run.search(/exec \/usr\/bin\/env -i/); + const recordCpAt = recordStep.run.search(/^\s*cp "\$\{RUNNER_TEMP:\?\}\/flake-record-files-all" "\$GATE_HOME\/files-all"$/m); + const recordInstallAt = recordStep.run.search(/^\s*install -d -m 0700 -o root -g root "\$GATE_HOME"$/m); + assert.ok( + recordDiffAt !== -1 && recordReExecAt !== -1 && recordCpAt !== -1 && recordInstallAt !== -1 && + recordDiffAt < recordReExecAt && recordReExecAt < recordInstallAt && recordInstallAt < recordCpAt, + 'the diff must be recorded in the parent arm before the env -i re-exec, and copied into the recreated root-only home', + ); + // The scrubbed child must never re-run git under env -i: the ordering + // pin uses first-match semantics, so it cannot by itself forbid a + // second git in the child. Strip comments first (the child's own docs + // name `git diff` when describing what NOT to do) before asserting. + assert.doesNotMatch( + recordStep.run.slice(recordReExecAt).replace(/^\s*#.*$/gm, '').replace(/\\\n/g, ' '), + /\bgit\b[^\n]*\b(diff|log|show|whatchanged)\b/, + 'the scrubbed child must never re-run git under env -i — that is the failure shape of run 32227155960', + ); assert.ok( recordStep.run.includes( "grep -zE '\\.(test|spec)\\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$'", diff --git a/.github/workflows/qwen-triage.yml b/.github/workflows/qwen-triage.yml index 0e9f9493f5..68dc9b6a23 100644 --- a/.github/workflows/qwen-triage.yml +++ b/.github/workflows/qwen-triage.yml @@ -2903,6 +2903,25 @@ jobs: /usr/bin/printf '::error::flake-gate record: step script changed between open and re-exec snapshot — refusing to run\n' exit 1 fi + # Record the changed-test list in the PARENT's normal environment, + # before the env -i re-exec: the scrubbed child cannot read the + # shallow merge-ref objects — git's `safe.directory` global config + # lives under HOME, which `env -i` strips, so git refuses to read + # the base commit object and the diff fails with + # "Could not access ". The diff reads git metadata only + # and executes no PR code, so running it here is safe, and the + # "Pin agent inputs" step already proved this environment reads + # the base. Write the NUL-delimited list under RUNNER_TEMP, which + # the re-exec forwards; the clean child copies it into the gate + # home below. + BASE_OID="$(/usr/bin/cat "${RUNNER_TEMP:?}/verify-base-oid")" + case "$BASE_OID" in + [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]*) ;; + *) /usr/bin/printf '::error::No trusted base OID recorded; refusing to record the flakiness-gate file list.\n'; exit 1 ;; + esac + rm -rf -- "${RUNNER_TEMP:?}/flake-record-files-all" + /usr/bin/git -c core.quotePath=false diff -z --name-only --diff-filter=ACMRT "$BASE_OID" HEAD \ + > "${RUNNER_TEMP:?}/flake-record-files-all" LD_PRELOAD= LD_AUDIT= LD_LIBRARY_PATH= exec /usr/bin/env -i \ PATH="$PATH" RUNNER_TEMP="${RUNNER_TEMP:-}" \ GITHUB_OUTPUT="${GITHUB_OUTPUT:-}" GITHUB_STEP_SUMMARY="${GITHUB_STEP_SUMMARY:-}" \ @@ -2975,21 +2994,11 @@ jobs: # T (typechange) included: a symlink->regular flip changes what # the runner executes, so it is a changed test file exactly # like M — excluding it silently drops the file from the gate. - # Diff against the base OID the "Pin agent inputs" step recorded - # while .git was still root-owned, never `HEAD^1` again: this - # step runs in the env -i scrubbed child, and resolving the `^1` - # parent there intermittently fails with "Could not access - # 'HEAD^1'" on the persistent pool — the shallow merge-ref object - # store is left in a state prior --depth=2 fetches made - # unreadable. The OID is content-addressed and needs no parent - # walk, so the diff works from the already-captured value. - BASE_OID="$(cat "${RUNNER_TEMP:?}/verify-base-oid")" - case "$BASE_OID" in - [0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]*) ;; - *) echo "::error::No trusted base OID recorded; refusing to record the flakiness-gate file list."; exit 1 ;; - esac - git -c core.quotePath=false diff -z --name-only --diff-filter=ACMRT "$BASE_OID" HEAD \ - > "$GATE_HOME/files-all" + # The diff was already computed in the parent (before the env -i + # re-exec, where git can read the shallow merge-ref objects) and + # staged under RUNNER_TEMP; this scrubbed child only copies it + # into the root-only gate home. + cp "${RUNNER_TEMP:?}/flake-record-files-all" "$GATE_HOME/files-all" # .mts/.cts included: vitest's default include set collects them. # Only a no-match (status 1) may yield an empty list: a grep # error (status 2, e.g. ENOSPC opening the output) is