mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
Some checks are pending
* test(web-shell): add browser and lint harness * test(web-shell): harden browser smoke harness * fix(web-shell): guard mock daemon model state * test(web-shell): remove unused scenario harness * fix(web-shell): remove stale lint disables * test(web-shell): make matchMedia stub writable * fix(web-shell): exclude tests from package typecheck * test(web-shell): tighten mock daemon route contract * Update packages/web-shell/client/e2e/utils/mockDaemon.ts Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com> * test(web-shell): clear stale SSE connections * ci(web-shell): gate smoke on full CI profile --------- Co-authored-by: ermin.zem <ermin.zem@alibaba-inc.com> Co-authored-by: qwen-code-ci-bot <qwen-code-ci@service.alibaba.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com> Co-authored-by: 易良 <1204183885@qq.com>
684 lines
32 KiB
YAML
684 lines
32 KiB
YAML
# .github/workflows/ci.yml
|
|
|
|
name: 'Qwen Code CI'
|
|
|
|
on:
|
|
# No `push` trigger: every job here is gated to pull_request / merge_group, so
|
|
# a push to `main` ran nothing (CodeQL was the last push job and moved to its
|
|
# own scheduled codeql.yml). The merge queue validates the merged tree before
|
|
# it lands, so there is nothing left to run on the post-merge push.
|
|
pull_request:
|
|
branches:
|
|
- 'main'
|
|
- 'release/**'
|
|
merge_group:
|
|
workflow_dispatch:
|
|
inputs:
|
|
branch_ref:
|
|
description: 'Branch to run on'
|
|
required: true
|
|
default: 'main'
|
|
type: 'string'
|
|
|
|
concurrency:
|
|
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
|
cancel-in-progress: |-
|
|
${{ github.ref != 'refs/heads/main' && !startsWith(github.ref, 'refs/heads/release/') }}
|
|
|
|
permissions:
|
|
checks: 'write'
|
|
contents: 'read'
|
|
statuses: 'write'
|
|
|
|
defaults:
|
|
run:
|
|
shell: 'bash'
|
|
|
|
env:
|
|
ACTIONLINT_VERSION: '1.7.12'
|
|
SHELLCHECK_VERSION: '0.11.0'
|
|
YAMLLINT_VERSION: '1.35.1'
|
|
|
|
jobs:
|
|
classify_pr:
|
|
name: 'Classify PR'
|
|
if: "${{ github.event_name == 'pull_request' || github.event_name == 'merge_group' }}"
|
|
# Gate runs on ECS for in-repo PRs and for the merge queue (which runs in the
|
|
# base-repo context), 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 || github.event_name == ''merge_group'')) && 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'
|
|
env:
|
|
# Repository variables can override these defaults if release naming
|
|
# or the CI bot account changes.
|
|
HEAD_REPO: "${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name || '' }}"
|
|
HEAD_REF: "${{ github.event_name == 'pull_request' && github.head_ref || '' }}"
|
|
PR_TITLE: "${{ github.event_name == 'pull_request' && github.event.pull_request.title || '' }}"
|
|
RELEASE_SYNC_HEAD_PREFIX: "${{ vars.RELEASE_SYNC_HEAD_PREFIX || 'release/' }}"
|
|
RELEASE_SYNC_TITLE_PREFIX: "${{ vars.RELEASE_SYNC_TITLE_PREFIX || 'chore(release):' }}"
|
|
RELEASE_SYNC_ACTOR: "${{ vars.RELEASE_SYNC_ACTOR || 'qwen-code-ci-bot' }}"
|
|
run: |-
|
|
skip_ci=false
|
|
repo_match=false
|
|
actor_match=false
|
|
head_match=false
|
|
title_match=false
|
|
[[ "${HEAD_REPO}" == "${GITHUB_REPOSITORY}" ]] && repo_match=true
|
|
[[ "${GITHUB_ACTOR}" == "${RELEASE_SYNC_ACTOR}" ]] && actor_match=true
|
|
[[ "${HEAD_REF}" == "${RELEASE_SYNC_HEAD_PREFIX}"* ]] && head_match=true
|
|
[[ "${PR_TITLE}" == "${RELEASE_SYNC_TITLE_PREFIX}"* ]] && title_match=true
|
|
|
|
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" &&
|
|
"${repo_match}" == "true" &&
|
|
"${actor_match}" == "true" &&
|
|
"${head_match}" == "true" &&
|
|
"${title_match}" == "true" ]]; then
|
|
skip_ci=true
|
|
echo "Release sync PR detected: actor=${GITHUB_ACTOR}, head_ref=${HEAD_REF}, title=${PR_TITLE}"
|
|
else
|
|
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}"
|
|
fi
|
|
|
|
echo "skip_ci=${skip_ci}" >> "${GITHUB_OUTPUT}"
|
|
echo "skip_ci=${skip_ci}"
|
|
|
|
# In-repo PR (head branch in this repo => author has write access) and the
|
|
# merge queue (base-repo context) run the Linux jobs on ECS; fork PRs 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 }}'
|
|
EVENT_NAME: '${{ github.event_name }}'
|
|
run: |-
|
|
ubuntu_runner='["ubuntu-latest"]'
|
|
if [[ "${ECS_DISABLED}" != "true" && ( "${SAME_REPO}" == "true" || "${EVENT_NAME}" == "merge_group" ) ]]; then
|
|
ubuntu_runner='["self-hosted", "linux", "x64", "ecs-qwen"]'
|
|
fi
|
|
echo "ubuntu_runner=${ubuntu_runner}" >> "${GITHUB_OUTPUT}"
|
|
echo "Selected Linux runner: ${ubuntu_runner}"
|
|
|
|
#
|
|
# Test: Node
|
|
#
|
|
test:
|
|
name: 'Test (ubuntu-latest, Node 22.x)'
|
|
needs: 'classify_pr'
|
|
# 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.
|
|
# Not on push: the merge queue already tested the merged tree, so a
|
|
# post-merge re-run on `main` would be redundant.
|
|
if: "${{ !cancelled() && github.event_name != 'push' }}"
|
|
runs-on: '${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'') }}'
|
|
timeout-minutes: 60
|
|
outputs:
|
|
ci_profile: '${{ steps.ci_profile.outputs.ci_profile }}'
|
|
permissions:
|
|
contents: 'read'
|
|
checks: 'write'
|
|
pull-requests: 'write'
|
|
steps:
|
|
# On PRs, check out refs/pull/N/head (the immutable PR head, published the
|
|
# 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.
|
|
# 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@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
|
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 }}"
|
|
# 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
|
|
# chokes the squid egress proxy, flaking checkout. depth 1 is enough.
|
|
fetch-depth: 1
|
|
|
|
# Guard against a stale checkout (e.g. a caching egress proxy serving an old
|
|
# ref) silently testing the wrong tree. Cheap: one merge-base, sub-second.
|
|
# Also runs in the merge queue — now that the queue's Ubuntu checkout is on
|
|
# ECS/squid, a wrong-tree pass would merge bad code.
|
|
- name: 'Verify checkout includes expected head commit'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && (github.event_name == 'pull_request' || github.event_name == 'merge_group') }}"
|
|
env:
|
|
EXPECTED_SHA: "${{ github.event_name == 'merge_group' && github.event.merge_group.head_sha || github.event.pull_request.head.sha }}"
|
|
run: |-
|
|
if ! git merge-base --is-ancestor "${EXPECTED_SHA}" HEAD; then
|
|
echo "::error::Checked out ref does not contain expected head ${EXPECTED_SHA}."
|
|
git log --oneline --decorate -5
|
|
exit 1
|
|
fi
|
|
|
|
- name: 'Classify CI profile'
|
|
id: 'ci_profile'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
|
|
env:
|
|
GH_TOKEN: '${{ github.token }}'
|
|
PR_NUMBER: "${{ github.event_name == 'pull_request' && github.event.pull_request.number || '' }}"
|
|
IS_SAME_REPO_PR: "${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository }}"
|
|
run: |-
|
|
profile=full
|
|
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" && -n "${PR_NUMBER}" ]]; then
|
|
if [[ "${IS_SAME_REPO_PR}" == "true" ]]; then
|
|
changed_files="${RUNNER_TEMP}/changed-files.jsonl"
|
|
if gh api --paginate "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" --jq '.[] | {filename, status, previous_filename}' > "${changed_files}"; then
|
|
if ! profile="$(node .github/scripts/ci/classify-profile.mjs "${changed_files}")"; then
|
|
echo "::error::CI profile classifier exited non-zero; running full CI."
|
|
profile=full
|
|
fi
|
|
else
|
|
echo "::warning::Unable to list PR changed files; running full CI."
|
|
fi
|
|
else
|
|
echo "Fork PR detected; running full CI."
|
|
fi
|
|
fi
|
|
echo "ci_profile=${profile}" >> "${GITHUB_OUTPUT}"
|
|
echo "Selected CI profile: ${profile}"
|
|
|
|
- name: 'Docs-only CI'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'docs_only' }}"
|
|
run: 'echo "Docs-only change; full CI skipped."'
|
|
|
|
- name: 'GitHub CI helper checks'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'github_ci_only' }}"
|
|
timeout-minutes: 5
|
|
run: |-
|
|
# Keep this path dependency-free; script formatting is checked when those files hit full CI.
|
|
node scripts/lint.js --setup
|
|
node scripts/lint.js --actionlint
|
|
node scripts/lint.js --yamllint
|
|
node --test .github/scripts/pr-safety-precheck.test.mjs .github/scripts/ci/classify-profile.test.mjs .github/scripts/resolve-sandbox-image.test.mjs
|
|
|
|
# Self-hosted can't reach nodejs.org reliably; reuse the machine's Node.
|
|
- name: 'Set up Node.js 22.x (hosted)'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && runner.environment == 'github-hosted' }}"
|
|
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
|
|
with:
|
|
node-version: '22.x'
|
|
cache: 'npm'
|
|
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' && steps.ci_profile.outputs.ci_profile == 'full' && 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 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 persistent npm cache (self-hosted)'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && runner.environment == 'self-hosted' }}"
|
|
run: |-
|
|
cache_dir="${HOME}/.cache/qwen-code/npm"
|
|
mkdir -p "${cache_dir}"
|
|
echo "NPM_CONFIG_CACHE=${cache_dir}" >> "${GITHUB_ENV}"
|
|
echo "Using persistent npm cache at ${cache_dir}"
|
|
du -sh "${cache_dir}" 2>/dev/null || true
|
|
|
|
- name: 'Configure npm for rate limiting'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: |-
|
|
npm config set fetch-retry-mintimeout 20000
|
|
npm config set fetch-retry-maxtimeout 120000
|
|
npm config set fetch-retries 5
|
|
npm config set fetch-timeout 300000
|
|
|
|
- name: 'Install dependencies'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: |-
|
|
npm ci --prefer-offline --no-audit --progress=false
|
|
|
|
- name: 'Report npm cache usage (self-hosted)'
|
|
if: "${{ always() && needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && runner.environment == 'self-hosted' }}"
|
|
run: |-
|
|
cache_dir="${NPM_CONFIG_CACHE:-$(npm config get cache)}"
|
|
echo "npm cache: ${cache_dir}"
|
|
du -sh "${cache_dir}" 2>/dev/null || true
|
|
|
|
- name: 'Audit critical runtime dependencies'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: 'npm run audit:runtime:critical'
|
|
|
|
- name: 'Check lockfile'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: 'npm run check:lockfile'
|
|
|
|
- name: 'Check desktop workspace isolation'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: 'npm run check:desktop-isolation'
|
|
|
|
- name: 'Install linters'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: 'node scripts/lint.js --setup'
|
|
|
|
- name: 'Run ESLint'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: 'node scripts/lint.js --eslint'
|
|
|
|
- name: 'Run actionlint'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
timeout-minutes: 5
|
|
run: 'node scripts/lint.js --actionlint'
|
|
|
|
- name: 'Run shellcheck'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: 'node scripts/lint.js --shellcheck'
|
|
|
|
- name: 'Run yamllint'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: 'node scripts/lint.js --yamllint'
|
|
|
|
- name: 'Run Prettier'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: 'node scripts/lint.js --prettier'
|
|
|
|
- name: 'Run sensitive keyword linter'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: 'node scripts/lint.js --sensitive-keywords'
|
|
|
|
- name: 'Run i18n check'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: 'npm run check-i18n'
|
|
|
|
- name: 'Generate settings schema'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: 'npm run generate:settings-schema'
|
|
|
|
- name: 'Check settings schema is up-to-date'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: |-
|
|
if [[ -n $(git status --porcelain packages/vscode-ide-companion/schemas/settings.schema.json) ]]; then
|
|
echo "Error: settings.schema.json is out of date."
|
|
echo "Please run: npm run generate:settings-schema"
|
|
echo "Then commit the updated schema file."
|
|
git diff packages/vscode-ide-companion/schemas/settings.schema.json
|
|
exit 1
|
|
fi
|
|
echo "Settings schema is up-to-date"
|
|
|
|
# Keep this Linux-only PR gate explicit. macOS/Windows merge-queue jobs run
|
|
# npm run test:ci only, so they intentionally do not repeat this
|
|
# platform-independent bundle closure check.
|
|
- name: 'Check serve fast-path bundle closure'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
run: 'npm run check:serve-fast-path-bundle'
|
|
|
|
- name: 'Run tests and generate reports'
|
|
id: 'unit_tests'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
env:
|
|
NO_COLOR: true
|
|
HOME: '${{ runner.temp }}/qwen-ci-home'
|
|
USERPROFILE: '${{ runner.temp }}/qwen-ci-home'
|
|
OPENAI_API_KEY: ''
|
|
DASHSCOPE_API_KEY: ''
|
|
QWEN_API_KEY: ''
|
|
GEMINI_API_KEY: ''
|
|
QWEN_DEFAULT_AUTH_TYPE: ''
|
|
run: |-
|
|
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 }); }"
|
|
npm run test:ci
|
|
|
|
- name: 'Run no-AK integration smoke tests'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' && github.event_name == 'pull_request' }}"
|
|
run: 'npm run test:integration:no-ak:sandbox:none'
|
|
|
|
- name: 'Publish Test Report (for non-forks)'
|
|
if: |-
|
|
${{ 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) }}
|
|
uses: 'dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2' # ratchet:dorny/test-reporter@v3
|
|
with:
|
|
name: 'Test Results (ubuntu-latest, Node 22.x)'
|
|
path: 'packages/*/junit.xml'
|
|
reporter: 'java-junit'
|
|
fail-on-error: 'false'
|
|
|
|
- name: 'Upload Test Results Artifact (for forks)'
|
|
if: |-
|
|
${{ 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) }}
|
|
uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
|
|
with:
|
|
name: 'test-results-fork-22.x-ubuntu-latest'
|
|
path: 'packages/*/junit.xml'
|
|
|
|
- name: 'Upload coverage reports'
|
|
if: "${{ always() && needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
|
|
uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
|
|
with:
|
|
name: 'coverage-reports-22.x-ubuntu-latest'
|
|
path: 'packages/*/coverage'
|
|
|
|
web_shell_e2e_smoke:
|
|
name: 'web-shell E2E Smoke (ubuntu-latest, Node 22.x)'
|
|
needs:
|
|
- 'classify_pr'
|
|
- 'test'
|
|
if: |-
|
|
${{
|
|
!cancelled() &&
|
|
github.event_name == 'pull_request' &&
|
|
needs.classify_pr.outputs.skip_ci != 'true' &&
|
|
needs.test.outputs.ci_profile == 'full'
|
|
}}
|
|
runs-on: 'ubuntu-latest'
|
|
timeout-minutes: 20
|
|
permissions:
|
|
contents: 'read'
|
|
steps:
|
|
- name: 'Checkout'
|
|
uses: 'actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd' # v6.0.2
|
|
with:
|
|
ref: "${{ format('refs/pull/{0}/head', github.event.pull_request.number) }}"
|
|
fetch-depth: 1
|
|
|
|
- name: 'Set up Node.js 22.x'
|
|
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
|
|
with:
|
|
node-version: '22.x'
|
|
cache: 'npm'
|
|
cache-dependency-path: 'package-lock.json'
|
|
registry-url: 'https://registry.npmjs.org/'
|
|
|
|
- name: 'Configure npm for rate limiting'
|
|
run: |-
|
|
npm config set fetch-retry-mintimeout 20000
|
|
npm config set fetch-retry-maxtimeout 120000
|
|
npm config set fetch-retries 5
|
|
npm config set fetch-timeout 300000
|
|
|
|
- name: 'Install dependencies'
|
|
run: |-
|
|
npm ci --prefer-offline --no-audit --progress=false
|
|
|
|
- name: 'Install Playwright Chromium'
|
|
run: 'npx playwright install --with-deps chromium'
|
|
|
|
- name: 'Run web-shell browser smoke'
|
|
run: 'npm run test:e2e:smoke --workspace=packages/web-shell'
|
|
|
|
- name: 'Upload web-shell Playwright artifacts'
|
|
if: '${{ always() }}'
|
|
uses: 'actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a' # v7.0.1
|
|
with:
|
|
name: 'web-shell-e2e-smoke'
|
|
path: |-
|
|
packages/web-shell/client/e2e/test-results
|
|
packages/web-shell/client/e2e/playwright-report
|
|
if-no-files-found: 'ignore'
|
|
|
|
# macOS/Windows: slowest/costliest runners, rare platform regressions — run
|
|
# only in the merge queue. Skipped on PR (ubuntu is the fast PR signal) and on
|
|
# push (the queue already tested the merged tree, so a post-merge re-run is
|
|
# redundant). Two named jobs, not a matrix: a skipped matrix job reports one
|
|
# collapsed check name, never the per-OS required contexts, so PRs would sit
|
|
# "Expected" forever and never enter the queue. A skipped named job reports
|
|
# under its exact name and satisfies the required check (same as the
|
|
# Integration Tests job).
|
|
test_macos:
|
|
name: 'Test (macos-latest, Node 22.x)'
|
|
needs: 'classify_pr'
|
|
if: "${{ !cancelled() && github.event_name == 'merge_group' }}"
|
|
runs-on: 'macos-latest'
|
|
permissions:
|
|
contents: 'read'
|
|
steps:
|
|
# 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@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
|
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: 'Set up Node.js 22.x'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
|
|
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
|
|
with:
|
|
node-version: '22.x'
|
|
cache: 'npm'
|
|
cache-dependency-path: 'package-lock.json'
|
|
registry-url: 'https://registry.npmjs.org/'
|
|
|
|
- name: 'Configure npm for rate limiting'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
|
|
run: |-
|
|
npm config set fetch-retry-mintimeout 20000
|
|
npm config set fetch-retry-maxtimeout 120000
|
|
npm config set fetch-retries 5
|
|
npm config set fetch-timeout 300000
|
|
|
|
- name: 'Install dependencies'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
|
|
run: |-
|
|
npm ci --prefer-offline --no-audit --progress=false
|
|
|
|
- name: 'Run tests and generate reports'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
|
|
env:
|
|
NO_COLOR: true
|
|
HOME: '${{ runner.temp }}/qwen-ci-home'
|
|
USERPROFILE: '${{ runner.temp }}/qwen-ci-home'
|
|
OPENAI_API_KEY: ''
|
|
DASHSCOPE_API_KEY: ''
|
|
QWEN_API_KEY: ''
|
|
GEMINI_API_KEY: ''
|
|
QWEN_DEFAULT_AUTH_TYPE: ''
|
|
run: |-
|
|
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 }); }"
|
|
npm run test:ci
|
|
|
|
# Windows counterpart of test_macos (see that job's note). Runner is
|
|
# windows-2022; the check name keeps the windows-latest label so it matches
|
|
# the required-status-check context.
|
|
test_windows:
|
|
name: 'Test (windows-latest, Node 22.x)'
|
|
needs: 'classify_pr'
|
|
if: "${{ !cancelled() && github.event_name == 'merge_group' }}"
|
|
runs-on: 'windows-2022'
|
|
permissions:
|
|
contents: 'read'
|
|
steps:
|
|
- name: 'Checkout'
|
|
id: 'checkout'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
|
|
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
|
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: 'Set up Node.js 22.x'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
|
|
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
|
|
with:
|
|
node-version: '22.x'
|
|
cache: 'npm'
|
|
cache-dependency-path: 'package-lock.json'
|
|
registry-url: 'https://registry.npmjs.org/'
|
|
|
|
- name: 'Configure npm for rate limiting'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
|
|
run: |-
|
|
npm config set fetch-retry-mintimeout 20000
|
|
npm config set fetch-retry-maxtimeout 120000
|
|
npm config set fetch-retries 5
|
|
npm config set fetch-timeout 300000
|
|
|
|
- name: 'Install dependencies'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
|
|
run: |-
|
|
npm ci --prefer-offline --no-audit --progress=false
|
|
|
|
- name: 'Run tests and generate reports'
|
|
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' }}"
|
|
env:
|
|
NO_COLOR: true
|
|
HOME: '${{ runner.temp }}/qwen-ci-home'
|
|
USERPROFILE: '${{ runner.temp }}/qwen-ci-home'
|
|
OPENAI_API_KEY: ''
|
|
DASHSCOPE_API_KEY: ''
|
|
QWEN_API_KEY: ''
|
|
GEMINI_API_KEY: ''
|
|
QWEN_DEFAULT_AUTH_TYPE: ''
|
|
run: |-
|
|
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 }); }"
|
|
npm run test:ci
|
|
|
|
post_coverage_comment:
|
|
name: 'Post Coverage Comment'
|
|
runs-on: 'ubuntu-latest'
|
|
needs:
|
|
- 'classify_pr'
|
|
- 'test'
|
|
# !cancelled() not always(): don't let a cancelled run hold the concurrency slot here.
|
|
if: |-
|
|
${{
|
|
!cancelled() &&
|
|
needs.classify_pr.outputs.skip_ci != 'true' &&
|
|
needs.test.outputs.ci_profile == 'full' &&
|
|
github.event_name == 'pull_request' &&
|
|
github.event.pull_request.head.repo.full_name == github.repository
|
|
}}
|
|
continue-on-error: true
|
|
permissions:
|
|
contents: 'read' # For checkout
|
|
pull-requests: 'write' # For commenting
|
|
strategy:
|
|
matrix:
|
|
# Reduce noise by only posting the comment once
|
|
os:
|
|
- 'ubuntu-latest'
|
|
node-version:
|
|
- '22.x'
|
|
steps:
|
|
- name: 'Checkout'
|
|
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
|
|
|
- name: 'Download coverage reports artifact'
|
|
uses: 'actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c' # v8.0.1
|
|
with:
|
|
name: 'coverage-reports-${{ matrix.node-version }}-${{ matrix.os }}'
|
|
path: 'coverage_artifact' # Download to a specific directory
|
|
|
|
- name: 'Post Coverage Comment using Composite Action'
|
|
uses: './.github/actions/post-coverage-comment' # Path to the composite action directory
|
|
with:
|
|
cli_json_file: 'coverage_artifact/cli/coverage/coverage-summary.json'
|
|
core_json_file: 'coverage_artifact/core/coverage/coverage-summary.json'
|
|
cli_full_text_summary_file: 'coverage_artifact/cli/coverage/full-text-summary.txt'
|
|
core_full_text_summary_file: 'coverage_artifact/core/coverage/full-text-summary.txt'
|
|
node_version: '${{ matrix.node-version }}'
|
|
os: '${{ matrix.os }}'
|
|
github_token: '${{ secrets.GITHUB_TOKEN }}'
|
|
|
|
# Integration tests run only in the merge queue, not on every PR push.
|
|
# They are the suite that previously ran *only* in the nightly Release
|
|
# pipeline (`release.yml`), so regressions stayed hidden until release
|
|
# time. Gating them on `merge_group` catches the failure before the PR
|
|
# lands on `main`, while keeping the per-PR critical path fast. The
|
|
# `merge_group` event runs in the base-repo context, so the same model
|
|
# secrets used by the release jobs are available here.
|
|
#
|
|
# Until merge queue is enabled on `main` this job simply never triggers,
|
|
# so adding it is a no-op for existing PR/push runs. Reuses the exact
|
|
# `test:integration:cli:sandbox:none` script from `release.yml`.
|
|
integration_cli:
|
|
name: 'Integration Tests (CLI, No Sandbox)'
|
|
needs: 'classify_pr'
|
|
# Same ECS routing as the Ubuntu gate (via classify_pr): the merge queue runs
|
|
# in the base-repo context, so use the self-hosted ECS pool and keep the
|
|
# scarce hosted Linux runners free. Falls back to hosted if classify_pr is
|
|
# skipped or the ECS kill-switch is set.
|
|
if: "${{ !cancelled() && github.event_name == 'merge_group' }}"
|
|
runs-on: '${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'') }}'
|
|
permissions:
|
|
contents: 'read'
|
|
env:
|
|
OPENAI_API_KEY: '${{ secrets.OPENAI_API_KEY }}'
|
|
OPENAI_BASE_URL: '${{ secrets.OPENAI_BASE_URL }}'
|
|
OPENAI_MODEL: '${{ secrets.OPENAI_MODEL }}'
|
|
steps:
|
|
- name: 'Checkout'
|
|
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
|
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
|
|
|
|
# Same stale-checkout guard as the Ubuntu gate: this job now runs on ECS
|
|
# via classify_pr, so fail loud if the checkout lacks the merge-queue head
|
|
# rather than silently testing the wrong tree into a merge.
|
|
- name: 'Verify checkout includes expected head commit'
|
|
env:
|
|
EXPECTED_SHA: '${{ github.event.merge_group.head_sha }}'
|
|
run: |-
|
|
if ! git merge-base --is-ancestor "${EXPECTED_SHA}" HEAD; then
|
|
echo "::error::Checked out ref does not contain expected head ${EXPECTED_SHA}."
|
|
git log --oneline --decorate -5
|
|
exit 1
|
|
fi
|
|
|
|
# Hosted downloads Node; self-hosted ECS reuses its pre-installed Node 22
|
|
# (it can't reach nodejs.org reliably). Mirrors the Ubuntu gate.
|
|
- name: 'Setup Node.js (hosted)'
|
|
if: "${{ runner.environment == 'github-hosted' }}"
|
|
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
|
|
with:
|
|
node-version-file: '.nvmrc'
|
|
cache: 'npm'
|
|
cache-dependency-path: 'package-lock.json'
|
|
|
|
- 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 the MAINTAINER_ECS_RUNNER_DISABLED repository variable to 'true' to route the merge queue 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); integration tests will run against the runner's Node."
|
|
fi
|
|
|
|
- name: 'Configure persistent npm cache (self-hosted)'
|
|
if: "${{ runner.environment == 'self-hosted' }}"
|
|
run: |-
|
|
cache_dir="${HOME}/.cache/qwen-code/npm"
|
|
mkdir -p "${cache_dir}"
|
|
echo "NPM_CONFIG_CACHE=${cache_dir}" >> "${GITHUB_ENV}"
|
|
echo "Using persistent npm cache at ${cache_dir}"
|
|
du -sh "${cache_dir}" 2>/dev/null || true
|
|
|
|
- name: 'Install Dependencies'
|
|
env:
|
|
NPM_CONFIG_PREFER_OFFLINE: 'true'
|
|
run: |-
|
|
npm ci --no-audit --progress=false
|
|
|
|
- name: 'Report npm cache usage (self-hosted)'
|
|
if: "${{ always() && runner.environment == 'self-hosted' }}"
|
|
run: |-
|
|
cache_dir="${NPM_CONFIG_CACHE:-$(npm config get cache)}"
|
|
echo "npm cache: ${cache_dir}"
|
|
du -sh "${cache_dir}" 2>/dev/null || true
|
|
|
|
- name: 'Run CLI Integration Tests'
|
|
run: |-
|
|
npm run test:integration:cli:sandbox:none
|