test(webview): make the syntaxTreeAvailable anti-masking gates load-robust #1429
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| pull_request: | |
| push: | |
| branches: [main] | |
| # Least-privilege GITHUB_TOKEN: this workflow only needs to read repo contents | |
| # (checkout + install + lint + test + build). No write scopes required. | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.ref }} | |
| # Cancel superseded PR runs, but let every main commit's run complete — | |
| # each main commit must keep a finished check for bisects/reverts. | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| steps: | |
| # All GitHub Actions are pinned to commit SHAs — supply-chain hardening. | |
| # The threat (tag remutation against a compromised maintainer / repo) applies | |
| # equally to first-party `actions/*` and third-party actions, so the same | |
| # policy covers both. When bumping, resolve the commit SHA with | |
| # `gh api repos/<owner>/<repo>/commits/<tag> --jq '.sha'` — this handles both | |
| # lightweight and annotated tags (e.g. `pnpm/action-setup` uses annotated tags, | |
| # and `git/refs/tags/<tag>` would return its tag-object SHA, not the commit). | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 | |
| with: | |
| version: 11.10.0 | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: '22' | |
| cache: 'pnpm' | |
| # Single root install — flat package layout since Slice 7C. | |
| - name: Install deps | |
| run: pnpm install --frozen-lockfile | |
| # PR/main security gate. Catches dependency regressions during review so | |
| # they don't reach the publish.yml release audit. Scans the bundled | |
| # runtime only — every package esbuild bakes into dist/extension.cjs / | |
| # dist/webview/index.js lives in `dependencies`, so `--prod` covers | |
| # exactly the surface that ships in the .vsix; pure tooling stays in | |
| # `devDependencies` and is intentionally excluded. See publish.yml for | |
| # the same rationale. Approved exceptions must be both documented in | |
| # .claude/docs/security-audit/YYYY-MM-DD-*.md and added to the | |
| # `auditConfig.ignoreGhsas` array in pnpm-workspace.yaml (pnpm 11's bulk | |
| # advisory endpoint returns GHSA ids, not CVEs). See | |
| # .claude/docs/security-audit/README.md. | |
| - name: Audit production dependencies (security gate) | |
| run: pnpm audit --prod --audit-level=high | |
| - name: Lint | |
| run: pnpm lint | |
| - name: Unit tests | |
| run: pnpm test:unit | |
| # Real-chromium layout gate (test/webview-browser via @vitest/browser). | |
| # happy-dom has no layout engine, so the list-hang pixel alignment + the | |
| # "hang base == real computed .cm-line padding" contract can only run here. | |
| # Cache the Playwright browser keyed on the installed version so a bump | |
| # invalidates it (mirrors the .vscode-test cache pattern below); the version | |
| # lives in the lockfile (single source of truth), extracted here so a bump | |
| # invalidates the cache automatically. | |
| - name: Resolve Playwright version | |
| id: pw-version | |
| run: | | |
| version=$(node -p "require('playwright/package.json').version") | |
| if [ -z "$version" ]; then | |
| echo "::error::Could not read playwright version" >&2 | |
| exit 1 | |
| fi | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: playwright-${{ runner.os }}-${{ steps.pw-version.outputs.version }} | |
| - name: Install Playwright chromium | |
| run: pnpm exec playwright install --with-deps chromium | |
| - name: Browser tests (@vitest/browser chromium) | |
| run: pnpm test:browser | |
| - name: Build (host + webview via esbuild) | |
| run: pnpm build | |
| # @vscode/test-electron downloads a ~150 MB VS Code build into | |
| # `.vscode-test/` on first run and skips the download when the | |
| # versioned install already exists (download.js keys the on-disk | |
| # dir on the requested version). Cache that dir keyed on the pinned | |
| # VS_CODE_VERSION so warm runs restore it and skip the download. | |
| # The version lives in test/extension/launch.ts (single source of | |
| # truth); extract it here rather than duplicating "1.94.0" in YAML, | |
| # so a version bump there invalidates the cache automatically. | |
| - name: Resolve pinned VS Code version | |
| id: vscode-version | |
| # Standalone assignment (not `echo "$(grep …)"`) so bash errexit | |
| # aborts on grep's exit 1 — a renamed constant / changed literal | |
| # fails the step loudly instead of masking it as an empty version | |
| # (which would degrade the key to `vscode-test-<os>-` and silently | |
| # break the "version bump invalidates the cache" invariant). | |
| run: | | |
| version=$(grep -oP 'VS_CODE_VERSION = "\K[^"]+' test/extension/launch.ts) | |
| if [ -z "$version" ]; then | |
| echo "::error::Could not extract VS_CODE_VERSION from test/extension/launch.ts — was the constant renamed?" >&2 | |
| exit 1 | |
| fi | |
| echo "version=$version" >> "$GITHUB_OUTPUT" | |
| - name: Cache E2E VS Code download | |
| uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 | |
| with: | |
| path: .vscode-test | |
| key: vscode-test-${{ runner.os }}-${{ steps.vscode-version.outputs.version }} | |
| # E2E suite needs dist/ on disk and a display. The Build step | |
| # above already produced dist/ (the test host loads | |
| # dist/extension.cjs via the extensionDevelopmentPath), so we | |
| # run `test:e2e:run` (tsc-emit + launch, no rebuild) instead of | |
| # `test:e2e` (which prepends its own `pnpm build`) — one build | |
| # per job. xvfb-run gives headless Ubuntu a virtual display so | |
| # VS Code's Electron can render. | |
| - name: E2E tests (xvfb-run) | |
| run: xvfb-run -a pnpm test:e2e:run | |
| # PR/main packaging gate. We package on every PR (not just | |
| # release tags) so .vscodeignore regressions surface during | |
| # review, not at publish time when rollback means yanking a | |
| # live Marketplace version. The audit step that follows is | |
| # the actual check — packaging here only produces the .vsix | |
| # it inspects. | |
| - name: Package .vsix | |
| run: pnpm exec vsce package --no-dependencies --pre-release -o extension.vsix | |
| - name: Audit .vsix contents | |
| run: node scripts/audit-vsix.mjs extension.vsix | |
| # Rehearse the RELEASE SBOM pipeline on every PR/main push. publish.yml runs | |
| # these same three steps at tag time — and only at tag time, which is why the | |
| # v0.1.66 empty SBOM went undetected until a tag was pushed (syft's | |
| # package.json cataloger is image-tagged, so the directory scan emitted an SBOM | |
| # with ZERO npm packages; the fail-closed verify step caught it and blocked | |
| # that publish, so nothing reached users — see .github/syft-release.yaml and | |
| # LEARNING.md 2026-08-05). No static check over our own YAML can catch an | |
| # UPSTREAM change like that; only running the real action can. Dependabot bumps | |
| # `anchore/sbom-action` (and the syft baked into it) weekly, so this job makes | |
| # that bump prove itself on its own PR. | |
| # | |
| # NOT path-gated: GitHub supports `paths:` only at the workflow trigger, which | |
| # would gate the `build` job too, and per-job path filtering needs a | |
| # third-party action (default-deny on new dependencies). The job is cheap — a | |
| # prod-only install plus a syft scan of ~23 packages, no `pnpm build`, no E2E — | |
| # and runs in parallel with `build`. | |
| sbom: | |
| runs-on: ubuntu-latest | |
| steps: | |
| # SHA-pinned per the repo-wide policy documented in the `build` job above. | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 | |
| - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 | |
| with: | |
| version: 11.10.0 | |
| # NO `cache: 'pnpm'` here, deliberately. setup-node's pnpm cache key comes | |
| # from the lockfile hash and does NOT include the job name, and Actions | |
| # caches are immutable per key — first save wins. This job finishes long | |
| # before `build`, so it would publish a PROD-ONLY pnpm store under the key | |
| # `build` restores from, making `build` re-download every devDependency | |
| # until the lockfile changes. The prod closure is ~23 packages; the cache | |
| # is not worth that regression. | |
| - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: '22' | |
| # The three steps below are textually identical to publish.yml's — asserted | |
| # by test/build/publish-workflow-sbom-config.test.ts. That identity IS the | |
| # feature: what passes here is what runs at tag time, so a change to either | |
| # file must be made in both. | |
| # | |
| # No root `pnpm install` is needed: the script installs into its own staging | |
| # dir, and verify-sbom-scope.mjs uses node builtins only. | |
| - name: Assemble shipped runtime dependency tree (SBOM source) | |
| run: scripts/assemble-sbom-staging.sh "${RUNNER_TEMP:?RUNNER_TEMP is not set}/sbom-src" | |
| - name: Generate SBOM (SPDX) | |
| uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0 | |
| with: | |
| path: ${{ runner.temp }}/sbom-src | |
| config: .github/syft-release.yaml | |
| format: spdx-json | |
| output-file: sbom.spdx.json | |
| upload-release-assets: false | |
| - name: Verify SBOM is scoped to the shipped runtime | |
| run: node scripts/verify-sbom-scope.mjs sbom.spdx.json --reconcile "${RUNNER_TEMP:?RUNNER_TEMP is not set}/sbom-src" |