ci: route in-repo PRs' Linux test to self-hosted runner (#5620)

* ci: route in-repo PRs' Linux test to self-hosted runner

Send the required Linux Test job to the self-hosted ECS runner for PRs whose head branch lives in this repo (which implies the author had write access), while forks and push/merge_group runs stay on GitHub-hosted runners. This lets maintainer changes skip the shared hosted Linux queue without ever running untrusted fork code on self-hosted infrastructure. The MAINTAINER_ECS_RUNNER_DISABLED repo variable forces everything back to hosted if the ECS runner is unavailable.

* ci: reuse pre-installed Node on self-hosted runners, route Lint to ECS

The self-hosted ECS runners cannot reliably download the Node tarball from nodejs.org through the egress proxy (actions/setup-node aborts mid-download), which failed the Test job routed there. On self-hosted runners, reuse the machine's pre-installed Node instead of re-downloading it every run; hosted runners keep using actions/setup-node. Also route the Lint job to ECS for in-repo PRs — scripts/lint.js already skips installing actionlint/shellcheck/yamllint when they are on PATH, so no lint.js change is needed.

* ci: trim verbose runner-routing comments

* ci: run classify_pr gate on ECS for in-repo PRs

* ci: quote classify_pr/lint runs-on expressions

* ci: keep Lint on hosted runners (ECS proxy truncates linter downloads)

* ci: route Lint and review/triage gate jobs to self-hosted runner

Lint goes back to ECS (linters are now pre-installed on the runners, so scripts/lint.js skips downloading them). The pr-review gate jobs (authorize, review-config, delay-automatic-review, ack-review-request) and the triage authorize gate also move to ECS so the ECS heavy jobs (review-pr, tmux-testing) are no longer bottlenecked by the shared hosted queue. All respect the MAINTAINER_ECS_RUNNER_DISABLED kill-switch.
This commit is contained in:
易良 2026-06-22 20:28:27 +08:00 committed by GitHub
parent 8809c16b57
commit 7696d9a7d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 61 additions and 14 deletions

View file

@ -43,10 +43,12 @@ jobs:
classify_pr:
name: 'Classify PR'
if: "${{ github.event_name == 'pull_request' }}"
runs-on: 'ubuntu-latest'
# Gate runs on ECS for in-repo PRs too, else a busy hosted pool delays it and blocks the ECS-bound jobs. The kill-switch is read here, so flipping it reverts everything to hosted.
runs-on: "${{ (vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true' && github.event.pull_request.head.repo.full_name == github.repository) && fromJSON('[\"self-hosted\", \"linux\", \"x64\", \"ecs-qwen\"]') || fromJSON('[\"ubuntu-latest\"]') }}"
continue-on-error: true
outputs:
skip_ci: '${{ steps.release_sync.outputs.skip_ci }}'
ubuntu_runner: '${{ steps.pick_runner.outputs.ubuntu_runner }}'
steps:
- name: 'Detect release version-sync PR'
id: 'release_sync'
@ -84,11 +86,26 @@ jobs:
echo "skip_ci=${skip_ci}" >> "${GITHUB_OUTPUT}"
echo "skip_ci=${skip_ci}"
# In-repo PR (head branch in this repo => author has write access) runs the
# Linux Test on ECS; forks stay hosted. Disable via repo var MAINTAINER_ECS_RUNNER_DISABLED=true.
- name: 'Select Linux runner'
id: 'pick_runner'
env:
SAME_REPO: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
ECS_DISABLED: '${{ vars.MAINTAINER_ECS_RUNNER_DISABLED }}'
run: |-
ubuntu_runner='["ubuntu-latest"]'
if [[ "${ECS_DISABLED}" != "true" && "${SAME_REPO}" == "true" ]]; then
ubuntu_runner='["self-hosted", "linux", "x64", "ecs-qwen"]'
fi
echo "ubuntu_runner=${ubuntu_runner}" >> "${GITHUB_OUTPUT}"
echo "Selected Linux runner: ${ubuntu_runner}"
lint:
name: 'Lint'
needs: 'classify_pr'
if: "${{ !cancelled() && needs.classify_pr.outputs.skip_ci != 'true' }}"
runs-on: 'ubuntu-latest'
runs-on: "${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || '[\"ubuntu-latest\"]') }}"
steps:
- name: 'Checkout'
uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2
@ -96,12 +113,23 @@ jobs:
ref: '${{ github.event.inputs.branch_ref || github.ref }}'
fetch-depth: 0
- name: 'Set up Node.js 22.x'
# Self-hosted reuses the machine's Node (nodejs.org unreachable via proxy); linters are pre-installed on PATH so scripts/lint.js skips downloading them.
- name: 'Set up Node.js 22.x (hosted)'
if: "${{ runner.environment == 'github-hosted' }}"
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
with:
node-version: '22.x'
cache: 'npm'
- name: 'Use pre-installed Node.js (self-hosted)'
if: "${{ runner.environment == 'self-hosted' }}"
run: |-
if ! command -v node >/dev/null 2>&1; then
echo "::error::Node.js is not on PATH for this self-hosted runner. Provision Node 22.x or set MAINTAINER_ECS_RUNNER_DISABLED='true' to route PRs back to hosted runners."
exit 1
fi
echo "Using pre-installed Node $(node -v) / npm $(npm -v)"
- name: 'Install dependencies and run prepare'
run: 'npm ci'
@ -158,7 +186,7 @@ jobs:
# Stay running on release-sync PRs so the required Test contexts still
# report; the per-step skip_ci guards below make them no-op (pass) there.
if: '${{ !cancelled() }}'
runs-on: '${{ matrix.runner }}'
runs-on: '${{ fromJSON(matrix.runner) }}'
permissions:
contents: 'read'
checks: 'write'
@ -168,15 +196,15 @@ jobs:
matrix:
include:
- os: 'macos-latest'
runner: 'macos-latest'
runner: '["macos-latest"]'
node-version: '22.x'
upload-coverage: 'false'
- os: 'ubuntu-latest'
runner: 'ubuntu-latest'
runner: "${{ needs.classify_pr.outputs.ubuntu_runner || '[\"ubuntu-latest\"]' }}"
node-version: '22.x'
upload-coverage: 'true'
- os: 'windows-latest'
runner: 'windows-2022'
runner: '["windows-2022"]'
node-version: '22.x'
upload-coverage: 'false'
steps:
@ -184,8 +212,9 @@ jobs:
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2
- name: 'Set up Node.js ${{ matrix.node-version }}'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
# Self-hosted can't reach nodejs.org reliably; reuse the machine's Node.
- name: 'Set up Node.js ${{ matrix.node-version }} (hosted)'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment == 'github-hosted' }}"
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
with:
node-version: '${{ matrix.node-version }}'
@ -193,6 +222,18 @@ jobs:
cache-dependency-path: 'package-lock.json'
registry-url: 'https://registry.npmjs.org/'
- name: 'Use pre-installed Node.js (self-hosted)'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && runner.environment == 'self-hosted' }}"
run: |-
if ! command -v node >/dev/null 2>&1; then
echo "::error::Node.js is not on PATH for this self-hosted runner. Provision Node ${{ matrix.node-version }} or set the MAINTAINER_ECS_RUNNER_DISABLED repository variable to 'true' to route PRs back to hosted runners."
exit 1
fi
echo "Using pre-installed Node $(node -v) / npm $(npm -v)"
if [[ "$(node -p 'process.versions.node.split(".")[0]')" != "22" ]]; then
echo "::warning::Expected Node 22.x but found $(node -v); tests will run against the runner's Node."
fi
- name: 'Configure npm for rate limiting'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
run: |-

View file

@ -70,7 +70,7 @@ jobs:
concurrency:
group: 'qwen-pr-ack-${{ github.event.issue.number || github.event.pull_request.number }}'
cancel-in-progress: false
runs-on: 'ubuntu-latest'
runs-on: "${{ vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true' && fromJSON('[\"self-hosted\", \"linux\", \"x64\", \"ecs-qwen\"]') || fromJSON('[\"ubuntu-latest\"]') }}"
timeout-minutes: 5
permissions:
pull-requests: 'write'
@ -112,7 +112,7 @@ jobs:
if: |-
github.event_name == 'pull_request_target' &&
github.event.action == 'review_requested'
runs-on: 'ubuntu-latest'
runs-on: "${{ vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true' && fromJSON('[\"self-hosted\", \"linux\", \"x64\", \"ecs-qwen\"]') || fromJSON('[\"ubuntu-latest\"]') }}"
permissions: {}
outputs:
bot_login: '${{ steps.values.outputs.bot_login }}'
@ -131,7 +131,7 @@ jobs:
github.event.pull_request.state == 'open' &&
!github.event.pull_request.draft &&
needs.authorize.outputs.should_review == 'true'
runs-on: 'ubuntu-latest'
runs-on: "${{ vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true' && fromJSON('[\"self-hosted\", \"linux\", \"x64\", \"ecs-qwen\"]') || fromJSON('[\"ubuntu-latest\"]') }}"
# Configured in repo settings with a 30-minute wait timer.
environment:
name: 'qwen-pr-review-delay'
@ -183,7 +183,7 @@ jobs:
startsWith(github.event.comment.body, '@qwen-code /review')) ||
(github.event_name == 'pull_request_review' &&
startsWith(github.event.review.body, '@qwen-code /review')))
runs-on: 'ubuntu-latest'
runs-on: "${{ vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true' && fromJSON('[\"self-hosted\", \"linux\", \"x64\", \"ecs-qwen\"]') || fromJSON('[\"ubuntu-latest\"]') }}"
timeout-minutes: 5
permissions:
contents: 'read'

View file

@ -51,7 +51,7 @@ jobs:
startsWith(github.event.comment.body, '@qwen-code /tmux '))) ||
(github.event_name == 'workflow_dispatch' &&
github.event.inputs.tmux_pr != ''))
runs-on: 'ubuntu-latest'
runs-on: "${{ vars.MAINTAINER_ECS_RUNNER_DISABLED != 'true' && fromJSON('[\"self-hosted\", \"linux\", \"x64\", \"ecs-qwen\"]') || fromJSON('[\"ubuntu-latest\"]') }}"
timeout-minutes: 5
permissions:
contents: 'read'

View file

@ -3608,7 +3608,13 @@ describe('Linux/macOS installer end-to-end', { timeout: 15000 }, () => {
const fakeBin = path.join(tmpDir, 'bin');
mkdirSync(fakeBin, { recursive: true });
writeFileSync(path.join(fakeBin, 'curl'), '#!/usr/bin/env sh\nexit 22\n');
// Shadow any system Node on PATH (e.g. /usr/bin/node on self-hosted
// runners) with a stub that fails version detection, so the npm fallback
// deterministically fails with "Unable to determine Node.js version"
// regardless of whether the host has Node installed in /usr/bin.
writeFileSync(path.join(fakeBin, 'node'), '#!/usr/bin/env sh\nexit 1\n');
chmodSync(path.join(fakeBin, 'curl'), 0o755);
chmodSync(path.join(fakeBin, 'node'), 0o755);
let failureMessage = '';
try {