diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4855e43164..cfdd33fe9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: |- diff --git a/.github/workflows/qwen-code-pr-review.yml b/.github/workflows/qwen-code-pr-review.yml index c4b3281ea0..82f019159e 100644 --- a/.github/workflows/qwen-code-pr-review.yml +++ b/.github/workflows/qwen-code-pr-review.yml @@ -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' diff --git a/.github/workflows/qwen-triage.yml b/.github/workflows/qwen-triage.yml index fa9c922329..56fcf16529 100644 --- a/.github/workflows/qwen-triage.yml +++ b/.github/workflows/qwen-triage.yml @@ -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' diff --git a/scripts/tests/install-script.test.js b/scripts/tests/install-script.test.js index ccbcf3eccc..8fac2982e4 100644 --- a/scripts/tests/install-script.test.js +++ b/scripts/tests/install-script.test.js @@ -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 {