basant307/AI_Governance_Project
048
1# .github/workflows/ci.yml2 3name: 'Qwen Code CI'4 5on:6 # No `push` trigger: every job here is gated to pull_request / merge_group, so7 # a push to `main` ran nothing (CodeQL was the last push job and moved to its8 # own scheduled codeql.yml). The merge queue validates the merged tree before9 # it lands, so there is nothing left to run on the post-merge push.10 pull_request:11 branches:12 - 'main'13 - 'release/**'14 merge_group:15 workflow_dispatch:16 inputs:17 branch_ref:18 description: 'Branch to run on'19 required: true20 default: 'main'21 type: 'string'22 23concurrency:24 group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'25 cancel-in-progress: |-26 ${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}27 28permissions:29 checks: 'write'30 contents: 'read'31 statuses: 'write'32 33defaults:34 run:35 shell: 'bash'36 37env:38 ACTIONLINT_VERSION: '1.7.12'39 SHELLCHECK_VERSION: '0.11.0'40 YAMLLINT_VERSION: '1.35.1'41 42jobs:43 classify_pr:44 name: 'Classify PR'45 if: "${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}"46 # Gate runs on ECS for in-repo PRs and for the merge queue (which runs in the47 # base-repo context), else a busy hosted pool delays it and blocks the48 # ECS-bound jobs. The kill-switch is read here, so flipping it reverts49 # everything to hosted.50 runs-on: '${{ (vars.MAINTAINER_ECS_RUNNER_DISABLED != ''true'' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == ''merge_group'')) && fromJSON(''["self-hosted", "linux", "x64", "ecs-qwen"]'') || fromJSON(''["ubuntu-latest"]'') }}'51 continue-on-error: true52 outputs:53 skip_ci: '${{ steps.release_sync.outputs.skip_ci }}'54 ubuntu_runner: '${{ steps.pick_runner.outputs.ubuntu_runner }}'55 steps:56 - name: 'Detect release version-sync PR'57 id: 'release_sync'58 env:59 # Repository variables can override these defaults if release naming60 # or the CI bot account changes.61 HEAD_REPO: "${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || '' }}"62 HEAD_REF: "${{ github.event_name == 'pull_request' && github.head_ref || '' }}"63 PR_TITLE: "${{ github.event_name == 'pull_request' && github.event.pull_request.title || '' }}"64 RELEASE_SYNC_HEAD_PREFIX: "${{ vars.RELEASE_SYNC_HEAD_PREFIX || 'release/' }}"65 RELEASE_SYNC_TITLE_PREFIX: "${{ vars.RELEASE_SYNC_TITLE_PREFIX || 'chore(release):' }}"66 RELEASE_SYNC_ACTOR: "${{ vars.RELEASE_SYNC_ACTOR || 'qwen-code-ci-bot' }}"67 run: |-68 skip_ci=false69 repo_match=false70 actor_match=false71 head_match=false72 title_match=false73 [[ "${HEAD_REPO}" == "${GITHUB_REPOSITORY}" ]] && repo_match=true74 [[ "${GITHUB_ACTOR}" == "${RELEASE_SYNC_ACTOR}" ]] && actor_match=true75 [[ "${HEAD_REF}" == "${RELEASE_SYNC_HEAD_PREFIX}"* ]] && head_match=true76 [[ "${PR_TITLE}" == "${RELEASE_SYNC_TITLE_PREFIX}"* ]] && title_match=true77 78 if [[ "${GITHUB_EVENT_NAME}" == "pull_request" &&79 "${repo_match}" == "true" &&80 "${actor_match}" == "true" &&81 "${head_match}" == "true" &&82 "${title_match}" == "true" ]]; then83 skip_ci=true84 echo "Release sync PR detected: actor=${GITHUB_ACTOR}, head_ref=${HEAD_REF}, title=${PR_TITLE}"85 else86 echo "Not a release sync PR: event=${GITHUB_EVENT_NAME}, actor=${GITHUB_ACTOR}, expected_actor=${RELEASE_SYNC_ACTOR}, repo_match=${repo_match}, head_match=${head_match}, title_match=${title_match}"87 fi88 89 echo "skip_ci=${skip_ci}" >> "${GITHUB_OUTPUT}"90 echo "skip_ci=${skip_ci}"91 92 # In-repo PR (head branch in this repo => author has write access) and the93 # merge queue (base-repo context) run the Linux jobs on ECS; fork PRs stay94 # hosted. Disable via repo var MAINTAINER_ECS_RUNNER_DISABLED=true.95 - name: 'Select Linux runner'96 id: 'pick_runner'97 env:98 SAME_REPO: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'99 ECS_DISABLED: '${{ vars.MAINTAINER_ECS_RUNNER_DISABLED }}'100 EVENT_NAME: '${{ github.event_name }}'101 run: |-102 ubuntu_runner='["ubuntu-latest"]'103 if [[ "${ECS_DISABLED}" != "true" && ( "${SAME_REPO}" == "true" || "${EVENT_NAME}" == "merge_group" ) ]]; then104 ubuntu_runner='["self-hosted", "linux", "x64", "ecs-qwen"]'105 fi106 echo "ubuntu_runner=${ubuntu_runner}" >> "${GITHUB_OUTPUT}"107 echo "Selected Linux runner: ${ubuntu_runner}"108 109 #110 # Test: Node111 #112 test:113 name: 'Test (ubuntu-latest, Node 22.x)'114 needs: 'classify_pr'115 # Stay running on release-sync PRs so the required Test contexts still116 # report; the per-step skip_ci guards below make them no-op (pass) there.117 # Not on push: the merge queue already tested the merged tree, so a118 # post-merge re-run on `main` would be redundant.119 if: "${{ !cancelled() && github.event_name != 'push' }}"120 runs-on: '${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'') }}'121 timeout-minutes: 60122 outputs:123 ci_profile: '${{ steps.ci_profile.outputs.ci_profile }}'124 permissions:125 contents: 'read'126 checks: 'write'127 pull-requests: 'write'128 steps:129 # On PRs, check out refs/pull/N/head (the immutable PR head, published the130 # instant the branch is pushed) instead of github.ref. github.ref is the131 # merge ref (refs/pull/N/merge), which GitHub rebuilds asynchronously and132 # can serve stale for minutes after a push, repeatedly flaking this gate.133 # Merge queue refs are ephemeral; check out the event head SHA directly so134 # slow hosted runners do not fail after the queue branch is removed.135 # Non-PR/non-queue events keep github.ref.136 - name: 'Checkout'137 id: 'checkout'138 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"139 uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3140 with:141 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 }}"142 # Shallow: nothing here walks git history (the verify guard below checks143 # head.sha == HEAD, schema/tests touch only the working tree). On the144 # in-repo ECS runner a full-history clone is the heaviest transfer and145 # chokes the squid egress proxy, flaking checkout. depth 1 is enough.146 fetch-depth: 1147 148 # Guard against a stale checkout (e.g. a caching egress proxy serving an old149 # ref) silently testing the wrong tree. Cheap: one merge-base, sub-second.150 # Also runs in the merge queue — now that the queue's Ubuntu checkout is on151 # ECS/squid, a wrong-tree pass would merge bad code.152 - name: 'Verify checkout includes expected head commit'153 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && (github.event_name == 'pull_request' || github.event_name == 'merge_group') }}"154 env:155 EXPECTED_SHA: "${{ github.event_name == 'merge_group' && github.event.merge_group.head_sha || github.event.pull_request.head.sha }}"156 run: |-157 if ! git merge-base --is-ancestor "${EXPECTED_SHA}" HEAD; then158 echo "::error::Checked out ref does not contain expected head ${EXPECTED_SHA}."159 git log --oneline --decorate -5160 exit 1161 fi162 163 - name: 'Classify CI profile'164 id: 'ci_profile'165 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"166 env:167 GH_TOKEN: '${{ github.token }}'168 PR_NUMBER: "${{ github.event_name == 'pull_request' && github.event.pull_request.number || '' }}"169 IS_SAME_REPO_PR: "${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}"170 run: |-171 profile=full172 if [[ "${GITHUB_EVENT_NAME}" == "pull_request" && -n "${PR_NUMBER}" ]]; then173 if [[ "${IS_SAME_REPO_PR}" == "true" ]]; then174 changed_files="${RUNNER_TEMP}/changed-files.jsonl"175 if gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --jq '.[] | {filename, status, previous_filename}' > "${changed_files}"; then176 if ! profile="$(node .github/scripts/ci/classify-profile.mjs "${changed_files}")"; then177 echo "::error::CI profile classifier exited non-zero; running full CI."178 profile=full179 fi180 else181 echo "::warning::Unable to list PR changed files; running full CI."182 fi183 else184 echo "Fork PR detected; running full CI."185 fi186 fi187 echo "ci_profile=${profile}" >> "${GITHUB_OUTPUT}"188 echo "Selected CI profile: ${profile}"189 190 - name: 'Docs-only CI'191 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'docs_only' }}"192 run: 'echo "Docs-only change; full CI skipped."'193 194 - name: 'GitHub CI helper checks'195 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'github_ci_only' }}"196 timeout-minutes: 5197 run: |-198 # Keep this path dependency-free; script formatting is checked when those files hit full CI.199 node scripts/lint.js --setup200 node scripts/lint.js --actionlint201 node scripts/lint.js --yamllint202 node --test .github/scripts/pr-safety-precheck.test.mjs .github/scripts/ci/classify-profile.test.mjs .github/scripts/resolve-sandbox-image.test.mjs203 204 # Self-hosted can't reach nodejs.org reliably; reuse the machine's Node.205 - name: 'Set up Node.js 22.x (hosted)'206 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && runner.environment == 'github-hosted' }}"207 uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0208 with:209 node-version: '22.x'210 cache: 'npm'211 cache-dependency-path: 'package-lock.json'212 registry-url: 'https://registry.npmjs.org/'213 214 - name: 'Use pre-installed Node.js (self-hosted)'215 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && runner.environment == 'self-hosted' }}"216 run: |-217 if ! command -v node >/dev/null 2>&1; then218 echo "::error::Node.js is not on PATH for this self-hosted runner. Provision Node 22.x or set the MAINTAINER_ECS_RUNNER_DISABLED repository variable to 'true' to route PRs back to hosted runners."219 exit 1220 fi221 echo "Using pre-installed Node $(node -v) / npm $(npm -v)"222 if [[ "$(node -p 'process.versions.node.split(".")[0]')" != "22" ]]; then223 echo "::warning::Expected Node 22.x but found $(node -v); tests will run against the runner's Node."224 fi225 226 - name: 'Configure persistent npm cache (self-hosted)'227 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && runner.environment == 'self-hosted' }}"228 run: |-229 cache_dir="${HOME}/.cache/qwen-code/npm"230 mkdir -p "${cache_dir}"231 echo "NPM_CONFIG_CACHE=${cache_dir}" >> "${GITHUB_ENV}"232 echo "Using persistent npm cache at ${cache_dir}"233 du -sh "${cache_dir}" 2>/dev/null || true234 235 - name: 'Configure npm for rate limiting'236 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"237 run: |-238 npm config set fetch-retry-mintimeout 20000239 npm config set fetch-retry-maxtimeout 120000240 npm config set fetch-retries 5241 npm config set fetch-timeout 300000242 243 - name: 'Install dependencies'244 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"245 run: |-246 npm ci --prefer-offline --no-audit --progress=false247 248 - name: 'Report npm cache usage (self-hosted)'249 if: "${{ always() && needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && runner.environment == 'self-hosted' }}"250 run: |-251 cache_dir="${NPM_CONFIG_CACHE:-$(npm config get cache)}"252 echo "npm cache: ${cache_dir}"253 du -sh "${cache_dir}" 2>/dev/null || true254 255 - name: 'Audit critical runtime dependencies'256 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"257 run: 'npm run audit:runtime:critical'258 259 - name: 'Check lockfile'260 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"261 run: 'npm run check:lockfile'262 263 - name: 'Check desktop workspace isolation'264 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"265 run: 'npm run check:desktop-isolation'266 267 - name: 'Install linters'268 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"269 run: 'node scripts/lint.js --setup'270 271 - name: 'Run ESLint'272 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"273 run: 'node scripts/lint.js --eslint'274 275 - name: 'Run actionlint'276 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"277 timeout-minutes: 5278 run: 'node scripts/lint.js --actionlint'279 280 - name: 'Run shellcheck'281 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"282 run: 'node scripts/lint.js --shellcheck'283 284 - name: 'Run yamllint'285 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"286 run: 'node scripts/lint.js --yamllint'287 288 - name: 'Run Prettier'289 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"290 run: 'node scripts/lint.js --prettier'291 292 - name: 'Run sensitive keyword linter'293 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"294 run: 'node scripts/lint.js --sensitive-keywords'295 296 - name: 'Run i18n check'297 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"298 run: 'npm run check-i18n'299 300 - name: 'Generate settings schema'301 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"302 run: 'npm run generate:settings-schema'303 304 - name: 'Check settings schema is up-to-date'305 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"306 run: |-307 if [[ -n $(git status --porcelain packages/vscode-ide-companion/schemas/settings.schema.json) ]]; then308 echo "Error: settings.schema.json is out of date."309 echo "Please run: npm run generate:settings-schema"310 echo "Then commit the updated schema file."311 git diff packages/vscode-ide-companion/schemas/settings.schema.json312 exit 1313 fi314 echo "Settings schema is up-to-date"315 316 # Keep this Linux-only PR gate explicit. macOS/Windows merge-queue jobs run317 # npm run test:ci only, so they intentionally do not repeat this318 # platform-independent bundle closure check.319 - name: 'Check serve fast-path bundle closure'320 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"321 run: 'npm run check:serve-fast-path-bundle'322 323 - name: 'Run tests and generate reports'324 id: 'unit_tests'325 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"326 env:327 NO_COLOR: true328 HOME: '${{ runner.temp }}/qwen-ci-home'329 USERPROFILE: '${{ runner.temp }}/qwen-ci-home'330 OPENAI_API_KEY: ''331 DASHSCOPE_API_KEY: ''332 QWEN_API_KEY: ''333 GEMINI_API_KEY: ''334 QWEN_DEFAULT_AUTH_TYPE: ''335 run: |-336 node -e "const fs = require('node:fs'); for (const key of ['HOME', 'USERPROFILE']) { const dir = process.env[key]; if (dir) fs.mkdirSync(dir, { recursive: true }); }"337 npm run test:ci338 339 - name: 'Run no-AK integration smoke tests'340 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && github.event_name == 'pull_request' }}"341 run: 'npm run test:integration:no-ak:sandbox:none'342 343 - name: 'Publish Test Report (for non-forks)'344 if: |-345 ${{ always() && needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && steps.unit_tests.outcome != 'skipped' && (github.event.pull_request.head.repo.full_name == github.repository) }}346 uses: 'dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2' # ratchet:dorny/test-reporter@v3347 with:348 name: 'Test Results (ubuntu-latest, Node 22.x)'349 path: 'packages/*/junit.xml'350 reporter: 'java-junit'351 fail-on-error: 'false'352 353 - name: 'Upload Test Results Artifact (for forks)'354 if: |-355 ${{ always() && needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}356 uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1357 with:358 name: 'test-results-fork-22.x-ubuntu-latest'359 path: 'packages/*/junit.xml'360 361 - name: 'Upload coverage reports'362 if: "${{ always() && needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"363 uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1364 with:365 name: 'coverage-reports-22.x-ubuntu-latest'366 path: 'packages/*/coverage'367 368 # macOS/Windows: slowest/costliest runners, rare platform regressions — run369 # only in the merge queue. Skipped on PR (ubuntu is the fast PR signal) and on370 # push (the queue already tested the merged tree, so a post-merge re-run is371 # redundant). Two named jobs, not a matrix: a skipped matrix job reports one372 # collapsed check name, never the per-OS required contexts, so PRs would sit373 # "Expected" forever and never enter the queue. A skipped named job reports374 # under its exact name and satisfies the required check (same as the375 # Integration Tests job).376 test_macos:377 name: 'Test (macos-latest, Node 22.x)'378 needs: 'classify_pr'379 if: "${{ !cancelled() && github.event_name == 'merge_group' }}"380 runs-on: 'macos-latest'381 permissions:382 contents: 'read'383 steps:384 # See the Ubuntu gate's checkout: PRs use the immutable refs/pull/N/head385 # and merge queue uses the event head SHA.386 - name: 'Checkout'387 id: 'checkout'388 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"389 uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3390 with:391 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 }}"392 393 - name: 'Set up Node.js 22.x'394 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"395 uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0396 with:397 node-version: '22.x'398 cache: 'npm'399 cache-dependency-path: 'package-lock.json'400 registry-url: 'https://registry.npmjs.org/'401 402 - name: 'Configure npm for rate limiting'403 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"404 run: |-405 npm config set fetch-retry-mintimeout 20000406 npm config set fetch-retry-maxtimeout 120000407 npm config set fetch-retries 5408 npm config set fetch-timeout 300000409 410 - name: 'Install dependencies'411 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"412 run: |-413 npm ci --prefer-offline --no-audit --progress=false414 415 - name: 'Run tests and generate reports'416 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"417 env:418 NO_COLOR: true419 HOME: '${{ runner.temp }}/qwen-ci-home'420 USERPROFILE: '${{ runner.temp }}/qwen-ci-home'421 OPENAI_API_KEY: ''422 DASHSCOPE_API_KEY: ''423 QWEN_API_KEY: ''424 GEMINI_API_KEY: ''425 QWEN_DEFAULT_AUTH_TYPE: ''426 run: |-427 node -e "const fs = require('node:fs'); for (const key of ['HOME', 'USERPROFILE']) { const dir = process.env[key]; if (dir) fs.mkdirSync(dir, { recursive: true }); }"428 npm run test:ci429 430 # Windows counterpart of test_macos (see that job's note). Runner is431 # windows-2022; the check name keeps the windows-latest label so it matches432 # the required-status-check context.433 test_windows:434 name: 'Test (windows-latest, Node 22.x)'435 needs: 'classify_pr'436 if: "${{ !cancelled() && github.event_name == 'merge_group' }}"437 runs-on: 'windows-2022'438 permissions:439 contents: 'read'440 steps:441 - name: 'Checkout'442 id: 'checkout'443 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"444 uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3445 with:446 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 }}"447 448 - name: 'Set up Node.js 22.x'449 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"450 uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0451 with:452 node-version: '22.x'453 cache: 'npm'454 cache-dependency-path: 'package-lock.json'455 registry-url: 'https://registry.npmjs.org/'456 457 - name: 'Configure npm for rate limiting'458 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"459 run: |-460 npm config set fetch-retry-mintimeout 20000461 npm config set fetch-retry-maxtimeout 120000462 npm config set fetch-retries 5463 npm config set fetch-timeout 300000464 465 - name: 'Install dependencies'466 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"467 run: |-468 npm ci --prefer-offline --no-audit --progress=false469 470 - name: 'Run tests and generate reports'471 if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"472 env:473 NO_COLOR: true474 HOME: '${{ runner.temp }}/qwen-ci-home'475 USERPROFILE: '${{ runner.temp }}/qwen-ci-home'476 OPENAI_API_KEY: ''477 DASHSCOPE_API_KEY: ''478 QWEN_API_KEY: ''479 GEMINI_API_KEY: ''480 QWEN_DEFAULT_AUTH_TYPE: ''481 run: |-482 node -e "const fs = require('node:fs'); for (const key of ['HOME', 'USERPROFILE']) { const dir = process.env[key]; if (dir) fs.mkdirSync(dir, { recursive: true }); }"483 npm run test:ci484 485 post_coverage_comment:486 name: 'Post Coverage Comment'487 runs-on: 'ubuntu-latest'488 needs:489 - 'classify_pr'490 - 'test'491 # !cancelled() not always(): don't let a cancelled run hold the concurrency slot here.492 if: |-493 ${{494 !cancelled() &&495 needs.classify_pr.outputs.skip_ci != 'true' &&496 needs.test.outputs.ci_profile == 'full' &&497 github.event_name == 'pull_request' &&498 github.event.pull_request.head.repo.full_name == github.repository499 }}500 continue-on-error: true501 permissions:502 contents: 'read' # For checkout503 pull-requests: 'write' # For commenting504 strategy:505 matrix:506 # Reduce noise by only posting the comment once507 os:508 - 'ubuntu-latest'509 node-version:510 - '22.x'511 steps:512 - name: 'Checkout'513 uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3514 515 - name: 'Download coverage reports artifact'516 uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # v8.0.1517 with:518 name: 'coverage-reports-${{ matrix.node-version }}-${{ matrix.os }}'519 path: 'coverage_artifact' # Download to a specific directory520 521 - name: 'Post Coverage Comment using Composite Action'522 uses: './.github/actions/post-coverage-comment' # Path to the composite action directory523 with:524 cli_json_file: 'coverage_artifact/cli/coverage/coverage-summary.json'525 core_json_file: 'coverage_artifact/core/coverage/coverage-summary.json'526 cli_full_text_summary_file: 'coverage_artifact/cli/coverage/full-text-summary.txt'527 core_full_text_summary_file: 'coverage_artifact/core/coverage/full-text-summary.txt'528 node_version: '${{ matrix.node-version }}'529 os: '${{ matrix.os }}'530 github_token: '${{ secrets.GITHUB_TOKEN }}'531 532 # Integration tests run only in the merge queue, not on every PR push.533 # They are the suite that previously ran *only* in the nightly Release534 # pipeline (`release.yml`), so regressions stayed hidden until release535 # time. Gating them on `merge_group` catches the failure before the PR536 # lands on `main`, while keeping the per-PR critical path fast. The537 # `merge_group` event runs in the base-repo context, so the same model538 # secrets used by the release jobs are available here.539 #540 # Until merge queue is enabled on `main` this job simply never triggers,541 # so adding it is a no-op for existing PR/push runs. Reuses the exact542 # `test:integration:cli:sandbox:none` script from `release.yml`.543 integration_cli:544 name: 'Integration Tests (CLI, No Sandbox)'545 needs: 'classify_pr'546 # Same ECS routing as the Ubuntu gate (via classify_pr): the merge queue runs547 # in the base-repo context, so use the self-hosted ECS pool and keep the548 # scarce hosted Linux runners free. Falls back to hosted if classify_pr is549 # skipped or the ECS kill-switch is set.550 if: "${{ !cancelled() && github.event_name == 'merge_group' }}"551 runs-on: '${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'') }}'552 permissions:553 contents: 'read'554 env:555 OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'556 OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'557 OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'558 steps:559 - name: 'Checkout'560 uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3561 with:562 ref: "${{ github.event.inputs.branch_ref || (github.event_name == 'merge_group' && github.event.merge_group.head_sha) || github.ref }}"563 # Shallow, mirroring the Ubuntu gate: nothing here walks git history,564 # and a full-history clone is the heaviest transfer on the ECS runner.565 fetch-depth: 1566 567 # Same stale-checkout guard as the Ubuntu gate: this job now runs on ECS568 # via classify_pr, so fail loud if the checkout lacks the merge-queue head569 # rather than silently testing the wrong tree into a merge.570 - name: 'Verify checkout includes expected head commit'571 env:572 EXPECTED_SHA: '${{ github.event.merge_group.head_sha }}'573 run: |-574 if ! git merge-base --is-ancestor "${EXPECTED_SHA}" HEAD; then575 echo "::error::Checked out ref does not contain expected head ${EXPECTED_SHA}."576 git log --oneline --decorate -5577 exit 1578 fi579 580 # Hosted downloads Node; self-hosted ECS reuses its pre-installed Node 22581 # (it can't reach nodejs.org reliably). Mirrors the Ubuntu gate.582 - name: 'Setup Node.js (hosted)'583 if: "${{ runner.environment == 'github-hosted' }}"584 uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0585 with:586 node-version-file: '.nvmrc'587 cache: 'npm'588 cache-dependency-path: 'package-lock.json'589 590 - name: 'Use pre-installed Node.js (self-hosted)'591 if: "${{ runner.environment == 'self-hosted' }}"592 run: |-593 if ! command -v node >/dev/null 2>&1; then594 echo "::error::Node.js is not on PATH for this self-hosted runner. Provision Node 22.x or set the MAINTAINER_ECS_RUNNER_DISABLED repository variable to 'true' to route the merge queue back to hosted runners."595 exit 1596 fi597 echo "Using pre-installed Node $(node -v) / npm $(npm -v)"598 if [[ "$(node -p 'process.versions.node.split(".")[0]')" != "22" ]]; then599 echo "::warning::Expected Node 22.x but found $(node -v); integration tests will run against the runner's Node."600 fi601 602 - name: 'Configure persistent npm cache (self-hosted)'603 if: "${{ runner.environment == 'self-hosted' }}"604 run: |-605 cache_dir="${HOME}/.cache/qwen-code/npm"606 mkdir -p "${cache_dir}"607 echo "NPM_CONFIG_CACHE=${cache_dir}" >> "${GITHUB_ENV}"608 echo "Using persistent npm cache at ${cache_dir}"609 du -sh "${cache_dir}" 2>/dev/null || true610 611 - name: 'Install Dependencies'612 env:613 NPM_CONFIG_PREFER_OFFLINE: 'true'614 run: |-615 npm ci --no-audit --progress=false616 617 - name: 'Report npm cache usage (self-hosted)'618 if: "${{ always() && runner.environment == 'self-hosted' }}"619 run: |-620 cache_dir="${NPM_CONFIG_CACHE:-$(npm config get cache)}"621 echo "npm cache: ${cache_dir}"622 du -sh "${cache_dir}" 2>/dev/null || true623 624 - name: 'Run CLI Integration Tests'625 run: |-626 npm run test:integration:cli:sandbox:none627 