mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-10 09:15:24 +00:00
* feat(web-shell): auto-post visual previews (screenshots + flow GIFs) on PRs PRs that touch the web-shell UI now get an auto-updated comment with light/dark screenshots of key views (transcript, slash menu, model/theme dialogs, permission panel) and short GIF recordings of common flows, rendered against the existing mock daemon — no real backend, no secrets. Split into two workflows for security, since capture runs untrusted PR code: - web-shell-visuals.yml (pull_request): checks out the PR head, builds and renders it with Playwright, captures PNGs + webm, converts webm->GIF with ffmpeg, and uploads an artifact. `contents: read` only, references no secrets — fork PRs run with a read-only token and no secrets. - web-shell-visuals-publish.yml (workflow_run): downloads the artifact, binds it to its real PR by requiring the PR head SHA to equal the run's authenticated head SHA, hosts the images on a per-PR `pr-assets/*` branch (referenced by immutable commit SHA), and posts/updates one inline comment. Never checks out or runs PR code; the write token lives only here. Capture infra is self-contained in packages/web-shell (playwright.visuals.config.ts + client/e2e/visuals/*), reusing the mock daemon harness. Run locally with: `npm run test:e2e:visuals --workspace=packages/web-shell`. * fix(web-shell): guard empty gh api response in visuals publish Addresses review feedback on #6880: if `gh api` returns empty (network error / rate limit), jq on empty stdin errors and `set -e` kills the publish job. Skip gracefully instead. * fix(web-shell): address review nits on visuals capture - harness recordFlow: wrap video saveAs/delete in try/catch so a video I/O error (e.g. drive failed before navigation) can't mask the real driveError. - capture workflow: drop the unused head_sha.txt artifact field; the publish job binds to the authenticated workflow_run.head_sha, and an artifact-sourced SHA would be untrusted. * fix(web-shell): address second review round on visuals capture - context.close() in recordFlow's finally is now best-effort (try/catch) so a close/crash error can't mask the real driveError. - add a flows spec that asserts a throwing drive propagates its own error. - trigger the capture workflow on playwright.visuals.config.ts changes too. * fix(web-shell): address third review round on visuals capture - harness: log (don't silently swallow) a video save/null when drive succeeded; keep masking-suppression only when driveError is set. - publish: HTML-escape interpolated values in the comment builder (defense in depth, independent of the upstream filename sanitization); fix the stale 'single pr-assets branch' comment and key concurrency on source repo+branch so different PRs (incl. same-named fork branches) parallelize. - capture: bump checkout to v6.0.3 (repo standard); surface ffmpeg's stderr on GIF-conversion failure instead of discarding it. * fix(web-shell): harden visuals publish/capture (review round 4) Publish (privileged workflow_run): - CRITICAL: capture basename before `tr` so its trailing newline isn't turned into `_` (which broke the .png/.gif filter -> empty preview). - dedup only against the bot's OWN comment (author + marker), not any marker-bearing comment a participant can post. - bound the pr-assets branch: force-push a single orphan snapshot per run (previous snapshot GC'd) instead of appending unbounded untrusted content; this also removes the rebase/retry path. - cap EXAMINED candidates (not just accepted) before validation; tighten per-file (3MiB) and accepted-image (14) caps. - re-validate PR open + head-SHA immediately before the comment write (TOCTOU); retry the comment listing and abort rather than POST a duplicate when listing fails. - esc() the runUrl for consistency with the self-defending HTML. Capture (pull_request): - upload raw recordings as a SEPARATE artifact the publisher never downloads, so an untrusted multi-GB video can't exhaust the privileged job. - also trigger on packages/webui/src and packages/sdk-typescript/src (the visuals dev server aliases them). - create screenshots/gifs dirs before the metadata counts (defensive). Harness recordFlow: - track drive failure with an explicit boolean (handles `throw undefined`); discard the recording on failure so a failed flow leaves no bogus webm. * refactor(web-shell): extract + unit-test the visuals publish staging/comment Addresses the review's testability gap (the class of bug that let the filename sanitizer break the whole preview slip through green CI). The image validation (magic bytes, filename sanitization, examined/accepted/size caps) and the comment builder (light/dark pairing, flow labels, HTML escaping) move from inline workflow bash/node into .github/scripts/web-shell-visuals-publish .mjs, covered by web-shell-visuals-publish.test.mjs (run in ci.yml's node --test line). The publish workflow sparse-checks-out and calls the script instead. Behaviour is unchanged; it just gained a test surface. * fix(web-shell): retry the visuals asset force-push; drop stale comment Round-4 switched hosting to a force-push but left a comment referencing a 'push-retry loop' that no longer existed, and the force-push was a single call that set -e would abort on a transient failure. Add a bounded retry and correct the comment. * fix(web-shell): harden visuals publish/capture (review round 6) Script (unit-tested): - flow labels: own-property lookup so `toString.gif`/`constructor.gif` can't leak Object.prototype members into the comment. - per-kind image caps (screenshots vs gifs) so a large screenshot set can't silently starve the flow GIFs from the preview. - tests for both, plus the per-kind cap. Publish: - bind the artifact PR number to the run's authenticated head repo+branch (not just head SHA), rejecting a sibling PR that shares the same commit. - re-validate before the force-push and again right before the comment write (close the download/stage/lookup TOCTOU windows). Capture: - bound artifact contents before upload (drop oversized / excess files) so an untrusted spec can't bloat the published or video artifact. - trigger on the capture workflow file itself. - new close-trigger cleanup workflow deletes a PR's asset branch on close, so pr-assets/* refs don't accumulate without bound. - single-source the capture viewport (constants.ts) shared by config + harness. - model-switch flow asserts the daemon model request actually fired. * fix(web-shell): stricter visuals error handling (review round 7) Harness recordFlow: - when the drive SUCCEEDS, a failed context.close() or video.saveAs() (or a missing recording) now FAILS the flow instead of a swallowed console.warn — a silent pass with no .webm makes the downstream GIF step fail confusingly. A drive FAILURE still discards the partial video and rethrows the original error (unchanged). Publish: - validate_pr distinguishes a transient API failure (empty after retries -> exit 1, re-triggerable) from a genuine invalid state (closed / head mismatch -> skip), via a `gate` wrapper used at all three checkpoints. - add a 2s backoff between comment-listing retries (matching the push retry). --------- Co-authored-by: wenshao <wenshao@example.com>
743 lines
35 KiB
YAML
743 lines
35 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'
|
|
linux_runner:
|
|
description: 'Linux runner to use for manual validation'
|
|
required: true
|
|
default: 'self-hosted'
|
|
type: 'choice'
|
|
options:
|
|
- 'self-hosted'
|
|
- 'hosted'
|
|
|
|
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' || github.event_name == 'workflow_dispatch' }}"
|
|
# 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 }}'
|
|
DISPATCH_LINUX_RUNNER: '${{ github.event.inputs.linux_runner }}'
|
|
run: |-
|
|
ubuntu_runner='["ubuntu-latest"]'
|
|
if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then
|
|
if [[ "${ECS_DISABLED}" != "true" && "${DISPATCH_LINUX_RUNNER}" == "self-hosted" ]]; then
|
|
ubuntu_runner='["self-hosted", "linux", "x64", "ecs-qwen"]'
|
|
fi
|
|
elif [[ "${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 .github/scripts/web-shell-visuals-publish.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' || github.event_name == 'workflow_dispatch') &&
|
|
needs.classify_pr.outputs.skip_ci != 'true' &&
|
|
needs.test.outputs.ci_profile == 'full'
|
|
}}
|
|
runs-on: '${{ fromJSON(needs.classify_pr.outputs.ubuntu_runner || ''["ubuntu-latest"]'') }}'
|
|
timeout-minutes: 20
|
|
permissions:
|
|
contents: 'read'
|
|
steps:
|
|
- name: 'Checkout'
|
|
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 }}"
|
|
fetch-depth: 1
|
|
|
|
- name: 'Verify checkout includes expected head commit'
|
|
if: "${{ github.event_name == 'pull_request' }}"
|
|
env:
|
|
EXPECTED_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
|
|
|
|
# Self-hosted can't reach nodejs.org reliably; reuse the machine's Node.
|
|
- 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'
|
|
cache-dependency-path: 'package-lock.json'
|
|
registry-url: 'https://registry.npmjs.org/'
|
|
|
|
- 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 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); web-shell smoke 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: '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 (hosted)'
|
|
if: "${{ runner.environment == 'github-hosted' }}"
|
|
run: 'npx playwright install --with-deps chromium'
|
|
|
|
- name: 'Install Playwright Chromium (self-hosted)'
|
|
if: "${{ runner.environment == 'self-hosted' }}"
|
|
# Self-hosted ECS runners already include system deps; --with-deps can race apt locks.
|
|
run: 'npx playwright install chromium'
|
|
|
|
- name: 'Choose web-shell Playwright port'
|
|
run: |-
|
|
port="$(node -e "const net=require('node:net');const server=net.createServer();server.listen(0,'127.0.0.1',()=>{console.log(server.address().port);server.close();});")"
|
|
echo "PLAYWRIGHT_PORT=${port}" >> "${GITHUB_ENV}"
|
|
echo "Using web-shell Playwright port ${port}"
|
|
|
|
- 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
|