ci: stabilize actionlint on self-hosted runners (#6113)

This commit is contained in:
易良 2026-07-01 17:25:42 +08:00 committed by GitHub
parent 15d30ddc64
commit 1287ac10ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 109 additions and 8 deletions

View file

@ -128,14 +128,15 @@ jobs:
# instant the branch is pushed) instead of github.ref. github.ref is the
# merge ref (refs/pull/N/merge), which GitHub rebuilds asynchronously and
# can serve stale for minutes after a push, repeatedly flaking this gate.
# The merge queue (integration_cli on merge_group) validates the merged
# result. Non-PR events keep github.ref.
# Merge queue refs are ephemeral; check out the event head SHA directly so
# slow hosted runners do not fail after the queue branch is removed.
# Non-PR/non-queue events keep github.ref.
- name: 'Checkout'
id: 'checkout'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2
with:
ref: "${{ github.event.inputs.branch_ref || (github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number)) || github.ref }}"
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 }}"
# Shallow: nothing here walks git history (the verify guard below checks
# head.sha == HEAD, schema/tests touch only the working tree). On the
# in-repo ECS runner a full-history clone is the heaviest transfer and
@ -318,14 +319,14 @@ jobs:
permissions:
contents: 'read'
steps:
# See the Ubuntu gate's checkout: on PRs use the immutable refs/pull/N/head
# to avoid merge-ref rebuild lag; other events keep github.ref.
# See the Ubuntu gate's checkout: PRs use the immutable refs/pull/N/head
# and merge queue uses the event head SHA.
- name: 'Checkout'
id: 'checkout'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2
with:
ref: "${{ github.event.inputs.branch_ref || (github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number)) || github.ref }}"
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: 'Set up Node.js 22.x'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
@ -380,7 +381,7 @@ jobs:
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2
with:
ref: "${{ github.event.inputs.branch_ref || (github.event_name == 'pull_request' && format('refs/pull/{0}/head', github.event.pull_request.number)) || github.ref }}"
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: 'Set up Node.js 22.x'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
@ -495,6 +496,7 @@ jobs:
- name: 'Checkout'
uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2
with:
ref: "${{ github.event.inputs.branch_ref || (github.event_name == 'merge_group' && github.event.merge_group.head_sha) || github.ref }}"
# Shallow, mirroring the Ubuntu gate: nothing here walks git history,
# and a full-history clone is the heaviest transfer on the ECS runner.
fetch-depth: 1

View file

@ -7,6 +7,7 @@
*/
import { execSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
@ -15,7 +16,37 @@ const ACTIONLINT_VERSION = '1.7.12';
const SHELLCHECK_VERSION = '0.11.0';
const YAMLLINT_VERSION = '1.35.1';
const TEMP_DIR = join(tmpdir(), 'qwen-code-linters');
function sanitizePathPart(value) {
return value.replace(/[^A-Za-z0-9._-]/g, '_');
}
export function getLinterTempDir({
cwd = process.cwd(),
env = process.env,
} = {}) {
const baseDir = env.RUNNER_TEMP || tmpdir();
const runId = env.GITHUB_RUN_ID;
if (runId) {
return join(
baseDir,
'qwen-code-linters',
[
sanitizePathPart(runId),
sanitizePathPart(env.GITHUB_RUN_ATTEMPT || '1'),
sanitizePathPart(env.GITHUB_JOB || 'job'),
].join('-'),
);
}
const workspaceHash = createHash('sha256')
.update(cwd)
.digest('hex')
.slice(0, 16);
return join(baseDir, 'qwen-code-linters', `local-${workspaceHash}`);
}
const TEMP_DIR = getLinterTempDir();
function getPlatformArch() {
const platform = process.platform;
@ -65,6 +96,7 @@ const LINTERS = {
run: `
actionlint \
-color \
-shellcheck= \
-ignore 'SC2002:' \
-ignore 'SC2016:' \
-ignore 'SC2129:' \

View file

@ -0,0 +1,67 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
describe('getLinterTempDir', () => {
const originalArgv = process.argv;
beforeEach(() => {
process.argv = ['node', 'scripts/lint.js', '--test-import'];
});
afterEach(() => {
process.argv = originalArgv;
});
it('isolates GitHub Actions linter installs by run and job', async () => {
const { getLinterTempDir } = await import('../lint.js');
const first = getLinterTempDir({
cwd: '/runner/_work/qwen-code/qwen-code',
env: {
RUNNER_TEMP: '/runner/_work/_temp',
GITHUB_RUN_ID: '28501834362',
GITHUB_RUN_ATTEMPT: '1',
GITHUB_JOB: 'test',
},
});
const second = getLinterTempDir({
cwd: '/runner/_work/qwen-code/qwen-code',
env: {
RUNNER_TEMP: '/runner/_work/_temp',
GITHUB_RUN_ID: '28501834363',
GITHUB_RUN_ATTEMPT: '1',
GITHUB_JOB: 'integration_cli',
},
});
expect(first).toBe(
'/runner/_work/_temp/qwen-code-linters/28501834362-1-test',
);
expect(second).toBe(
'/runner/_work/_temp/qwen-code-linters/28501834363-1-integration_cli',
);
expect(first).not.toBe(second);
});
it('isolates local linter installs by workspace', async () => {
const { getLinterTempDir } = await import('../lint.js');
const first = getLinterTempDir({
cwd: '/tmp/qwen-code-a',
env: {},
});
const second = getLinterTempDir({
cwd: '/tmp/qwen-code-b',
env: {},
});
expect(first).toMatch(/\/qwen-code-linters\/local-[a-f0-9]{16}$/);
expect(second).toMatch(/\/qwen-code-linters\/local-[a-f0-9]{16}$/);
expect(first).not.toBe(second);
});
});