From 3dd1b6dd6c698524ee2490804ac56b3302c8e300 Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Tue, 25 Aug 2026 23:59:08 +0800 Subject: [PATCH 1/5] ci: point the windows test job's temp at a short-alias-free path --- .github/workflows/ci.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f2497c96994..c434cf0d35f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1074,6 +1074,22 @@ jobs: with: ref: "${{ github.event.inputs.branch_ref || (github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number)) || (github.event_name == 'merge_group' && github.event.merge_group.head_sha) || github.ref }}" + # The runner profile directory is exposed through an 8.3 short alias + # (C:\Users\RUNNER~1), so %TEMP% yields short-name paths while + # fs.realpath resolves the long form of the same directory — tests that + # compare path strings then fail (~200 cases). Point TEMP/TMP at a + # sibling of the workspace, which has no short alias, for every later + # step. Runs before setup-node / npm install so they use it too. + - name: 'Point temp at a short-alias-free directory' + if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}" + shell: 'powershell' + run: |- + $temp = Join-Path $env:RUNNER_WORKSPACE 'qwen-code-temp' + New-Item -ItemType Directory -Force -Path $temp | Out-Null + "TEMP=$temp" | Out-File -FilePath $env:GITHUB_ENV -Append + "TMP=$temp" | Out-File -FilePath $env:GITHUB_ENV -Append + Write-Host "TEMP set to $temp" + - name: 'Configure self-hosted Windows test environment' if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment == 'self-hosted' }}" uses: './.github/actions/configure-windows-runner' From 0cca0ce5ba5bdcaf8dae5315a6d23ca45b1ce726 Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Wed, 26 Aug 2026 17:11:53 +0800 Subject: [PATCH 2/5] fix(ci): preserve alias-free Windows temp paths --- .github/workflows/ci.yml | 21 ++++++++----- scripts/tests/no-ak-integration-ci.test.js | 34 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 255a0a50de3..ae49112204b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1025,26 +1025,26 @@ jobs: with: ref: "${{ github.event.inputs.branch_ref || (github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number)) || (github.event_name == 'merge_group' && github.event.merge_group.head_sha) || github.ref }}" + - name: 'Configure self-hosted Windows test environment' + if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment == 'self-hosted' }}" + uses: './.github/actions/configure-windows-runner' + # The runner profile directory is exposed through an 8.3 short alias # (C:\Users\RUNNER~1), so %TEMP% yields short-name paths while # fs.realpath resolves the long form of the same directory — tests that # compare path strings then fail (~200 cases). Point TEMP/TMP at a - # sibling of the workspace, which has no short alias, for every later - # step. Runs before setup-node / npm install so they use it too. + # sibling of the workspace after self-hosted tuning so this value wins, + # and before setup-node / npm install so every later step uses it. - name: 'Point temp at a short-alias-free directory' if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}" shell: 'powershell' run: |- $temp = Join-Path $env:RUNNER_WORKSPACE 'qwen-code-temp' New-Item -ItemType Directory -Force -Path $temp | Out-Null - "TEMP=$temp" | Out-File -FilePath $env:GITHUB_ENV -Append - "TMP=$temp" | Out-File -FilePath $env:GITHUB_ENV -Append + "TEMP=$temp" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "TMP=$temp" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append Write-Host "TEMP set to $temp" - - name: 'Configure self-hosted Windows test environment' - if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment == 'self-hosted' }}" - uses: './.github/actions/configure-windows-runner' - # Same stale-checkout guard as the Ubuntu gate: this job now runs on ECS, # so fail loud if the checkout lacks the merge-queue head rather than # silently testing the wrong tree into a merge. @@ -1075,6 +1075,11 @@ jobs: if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment == 'self-hosted' }}" uses: './.github/actions/self-hosted-node' + - name: 'Verify temp paths carry no short alias' + if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}" + run: |- + node -e "const fs=require('node:fs');for(const key of ['TEMP','TMP']){const value=process.env[key];const real=fs.realpathSync(value);if(real!==value){console.error(key+' carries a short alias: '+value+' -> '+real);process.exitCode=1}}" + - name: 'Configure persistent npm cache (self-hosted)' if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment == 'self-hosted' }}" run: |- diff --git a/scripts/tests/no-ak-integration-ci.test.js b/scripts/tests/no-ak-integration-ci.test.js index 50d260a8f62..0291f3f76d8 100644 --- a/scripts/tests/no-ak-integration-ci.test.js +++ b/scripts/tests/no-ak-integration-ci.test.js @@ -381,6 +381,23 @@ describe('no-AK integration CI wiring', () => { expect(configure).toContain( "uses: './.github/actions/configure-windows-runner'", ); + const redirectTemp = getWorkflowStep( + windowsJob, + 'Point temp at a short-alias-free directory', + ); + for (const key of ['TEMP', 'TMP']) { + expect(redirectTemp).toContain( + `"${key}=$temp" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append`, + ); + } + const verifyTemp = getWorkflowStep( + windowsJob, + 'Verify temp paths carry no short alias', + ); + expect(verifyTemp).toContain("for(const key of ['TEMP','TMP'])"); + expect(verifyTemp).toContain('fs.realpathSync(value)'); + expect(verifyTemp).toContain('if(real!==value)'); + expect(verifyTemp).toContain('process.exitCode=1'); const configureAction = readFileSync( path.join(ROOT, CONFIGURE_ACTION_PATH), 'utf8', @@ -406,6 +423,17 @@ describe('no-AK integration CI wiring', () => { const configureUseIndex = windowsJob.indexOf( "uses: './.github/actions/configure-windows-runner'", ); + const redirectTempIndex = windowsJob.indexOf( + "name: 'Point temp at a short-alias-free directory'", + ); + const hostedNodeIndex = windowsJob.indexOf('actions/setup-node@'); + const selfHostedNodeIndex = windowsJob.indexOf( + "uses: './.github/actions/self-hosted-node'", + ); + const verifyTempIndex = windowsJob.indexOf( + "name: 'Verify temp paths carry no short alias'", + ); + const installIndex = windowsJob.indexOf("name: 'Install dependencies'"); const guardUseIndex = windowsJob.indexOf( "uses: './.github/actions/verify-checkout-head'", ); @@ -413,6 +441,12 @@ describe('no-AK integration CI wiring', () => { expect(autocrlfIndex).toBeGreaterThanOrEqual(0); expect(autocrlfIndex).toBeLessThan(windowsCheckoutIndex); expect(configureUseIndex).toBeGreaterThan(windowsCheckoutIndex); + expect(redirectTempIndex).toBeGreaterThan(configureUseIndex); + expect(redirectTempIndex).toBeLessThan(hostedNodeIndex); + expect(redirectTempIndex).toBeLessThan(selfHostedNodeIndex); + expect(verifyTempIndex).toBeGreaterThan(hostedNodeIndex); + expect(verifyTempIndex).toBeGreaterThan(selfHostedNodeIndex); + expect(verifyTempIndex).toBeLessThan(installIndex); expect(guardUseIndex).toBeGreaterThan(windowsCheckoutIndex); expect(configureUseIndex).toBeLessThan(guardUseIndex); for (const line of [ From 90d1626bef51ee5780dec6ec1c3d720be907fcbe Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Wed, 26 Aug 2026 17:56:01 +0800 Subject: [PATCH 3/5] fix(ci): preserve self-hosted Windows temp path --- .github/workflows/ci.yml | 10 +++------- scripts/tests/no-ak-integration-ci.test.js | 6 ++++++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae49112204b..7982a8673db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1029,14 +1029,10 @@ jobs: if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment == 'self-hosted' }}" uses: './.github/actions/configure-windows-runner' - # The runner profile directory is exposed through an 8.3 short alias - # (C:\Users\RUNNER~1), so %TEMP% yields short-name paths while - # fs.realpath resolves the long form of the same directory — tests that - # compare path strings then fail (~200 cases). Point TEMP/TMP at a - # sibling of the workspace after self-hosted tuning so this value wins, - # and before setup-node / npm install so every later step uses it. + # Hosted runners expose TEMP through an 8.3 short alias. Self-hosted + # runners keep their configured, alias-free RUNNER_TEMP path. - name: 'Point temp at a short-alias-free directory' - if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}" + if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment != 'self-hosted' }}" shell: 'powershell' run: |- $temp = Join-Path $env:RUNNER_WORKSPACE 'qwen-code-temp' diff --git a/scripts/tests/no-ak-integration-ci.test.js b/scripts/tests/no-ak-integration-ci.test.js index 0291f3f76d8..c63f9782e67 100644 --- a/scripts/tests/no-ak-integration-ci.test.js +++ b/scripts/tests/no-ak-integration-ci.test.js @@ -385,6 +385,9 @@ describe('no-AK integration CI wiring', () => { windowsJob, 'Point temp at a short-alias-free directory', ); + expect(redirectTemp).toContain( + "if: \"${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment != 'self-hosted' }}\"", + ); for (const key of ['TEMP', 'TMP']) { expect(redirectTemp).toContain( `"${key}=$temp" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append`, @@ -394,6 +397,9 @@ describe('no-AK integration CI wiring', () => { windowsJob, 'Verify temp paths carry no short alias', ); + expect(verifyTemp).toContain( + 'if: "${{ needs.classify_pr.outputs.skip_ci != \'true\' }}"', + ); expect(verifyTemp).toContain("for(const key of ['TEMP','TMP'])"); expect(verifyTemp).toContain('fs.realpathSync(value)'); expect(verifyTemp).toContain('if(real!==value)'); From db3159c9c470843712debddddd72184280192ecc Mon Sep 17 00:00:00 2001 From: yiliang114 Date: Wed, 26 Aug 2026 19:54:59 +0900 Subject: [PATCH 4/5] fix(ci): compare Windows temp against its realpath case-insensitively MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The short-alias guard compared TEMP/TMP to fs.realpathSync with a strict !==. On Windows realpathSync returns the on-disk casing, so a drive-letter or directory-case difference — the same directory under one spelling — failed the job for a reason unrelated to the 8.3 alias. The guard runs on both lanes, so that false positive could red-wall the self-hosted lane against its own pre-existing RUNNER_TEMP. Compare case-insensitively instead: RUNNER~1 -> runneradmin differs by more than casing and still fails, while a casing-only difference warns and passes. An unset TEMP/TMP now reports itself instead of surfacing as realpathSync(undefined)'s TypeError. The test now extracts the script from the workflow and runs it against symlinked temp dirs, covering all three outcomes, rather than pinning the JS text — a substring pin cannot tell a working comparison from a reverted one. --- .github/workflows/ci.yml | 10 ++- scripts/tests/no-ak-integration-ci.test.js | 91 +++++++++++++++++++++- 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7982a8673db..eec7de4e380 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1071,10 +1071,18 @@ jobs: if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment == 'self-hosted' }}" uses: './.github/actions/self-hosted-node' + # Compare case-insensitively: Windows paths are case-insensitive and + # realpathSync returns the on-disk casing, so a drive-letter or + # directory-case difference is the SAME directory under one spelling, + # not the 8.3 alias this guards. A strict !== there would fail the + # self-hosted lane too — it runs this step against the runner's + # pre-existing RUNNER_TEMP — for a reason unrelated to the alias. The + # alias case (RUNNER~1 -> runneradmin) differs by more than casing and + # still fails; a casing-only difference warns so it stays visible. - name: 'Verify temp paths carry no short alias' if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}" run: |- - node -e "const fs=require('node:fs');for(const key of ['TEMP','TMP']){const value=process.env[key];const real=fs.realpathSync(value);if(real!==value){console.error(key+' carries a short alias: '+value+' -> '+real);process.exitCode=1}}" + node -e "const fs = require('node:fs'); for (const key of ['TEMP', 'TMP']) { const value = process.env[key]; if (!value) { console.error(key + ' is not set'); process.exitCode = 1; continue; } const real = fs.realpathSync(value); if (real === value) continue; if (real.toLowerCase() === value.toLowerCase()) { console.log('::warning::' + key + ' differs from its real path only by casing: ' + value + ' -> ' + real); continue; } console.error(key + ' carries a short alias: ' + value + ' -> ' + real); process.exitCode = 1; }" - name: 'Configure persistent npm cache (self-hosted)' if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment == 'self-hosted' }}" diff --git a/scripts/tests/no-ak-integration-ci.test.js b/scripts/tests/no-ak-integration-ci.test.js index c63f9782e67..ad376fc1188 100644 --- a/scripts/tests/no-ak-integration-ci.test.js +++ b/scripts/tests/no-ak-integration-ci.test.js @@ -10,7 +10,9 @@ import { mkdirSync, mkdtempSync, readFileSync, + realpathSync, rmSync, + symlinkSync, } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; @@ -400,10 +402,9 @@ describe('no-AK integration CI wiring', () => { expect(verifyTemp).toContain( 'if: "${{ needs.classify_pr.outputs.skip_ci != \'true\' }}"', ); - expect(verifyTemp).toContain("for(const key of ['TEMP','TMP'])"); expect(verifyTemp).toContain('fs.realpathSync(value)'); - expect(verifyTemp).toContain('if(real!==value)'); - expect(verifyTemp).toContain('process.exitCode=1'); + // The guard's decisions are asserted by executing it, below; pinning the + // JS text here only fixes its spelling in place. const configureAction = readFileSync( path.join(ROOT, CONFIGURE_ACTION_PATH), 'utf8', @@ -641,3 +642,87 @@ describe('no-AK integration CI wiring', () => { expect(webShellJob).toContain('--with-deps chromium'); }); }); + +// The 8.3 alias this guard exists for (`C:\Users\RUNNER~1` realpathing to +// `C:\Users\runneradmin`) cannot be reproduced off Windows, but every +// decision the guard makes is a comparison between an env value and its +// realpath — and a symlink reproduces each of those on any platform. Run the +// real script rather than pinning its text: the case-insensitive comparison +// below is the whole point of the step, and a substring pin cannot tell a +// working comparison from a reverted one. +describe('Windows temp short-alias guard', () => { + const workflow = readFileSync( + path.join(ROOT, '.github/workflows/ci.yml'), + 'utf8', + ); + const step = getWorkflowStep( + getWorkflowJob(workflow, 'test_windows'), + 'Verify temp paths carry no short alias', + ); + // The script is single-quoted throughout precisely so this stays a + // delimiter-safe extraction. + const match = /node -e "([^"]+)"/.exec(step); + const script = match?.[1]; + + // Invoked exactly as the workflow does: `node -e