Skip to content

fix(webview): pair the parse-frontier gate with a structural-reparse check in the bounded block fields #1467

fix(webview): pair the parse-frontier gate with a structural-reparse check in the bounded block fields

fix(webview): pair the parse-frontier gate with a structural-reparse check in the bounded block fields #1467

Workflow file for this run

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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with:
version: 11.10.0
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
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@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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"