Skip to content

Health Check

Health Check #1360

Workflow file for this run

name: Health Check
on:
push:
branches: [main, '*/wip/*']
pull_request:
merge_group:
# zizmor(excessive-permissions): an explicit permissions block is required
# instead of relying on the default GITHUB_TOKEN scope
permissions:
contents: read
jobs:
prepare:
timeout-minutes: 1
runs-on: ubuntu-latest
outputs:
versions: ${{ steps.read-gemstone-integration-versions.outputs.versions }}
oldest-version: ${{ steps.read-gemstone-integration-versions.outputs.oldest-version }}
steps:
# zizmor(unpinned-uses): actions must be pinned to a commit SHA, not
# a mutable tag like @v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: |
.github/workflows
scripts/lint-workflow-timeouts.sh
client/.gemstone-integration-releases.json
client/bin/gemstone-integration-versions.js
client/src/gemStoneVersion.js
sparse-checkout-cone-mode: false
# zizmor(artipacked): checkout must not persist the token past
# this job
persist-credentials: false
# Fails if any job lacks timeout-minutes, since a job without one
# silently inherits GitHub's 360-minute default instead of failing
# fast on a hang. Kept here rather than in lint-workflows: it's a
# cheap bash+yq script with no Docker pull, so it adds negligible
# latency to the matrix gate. Only the slower actionlint/zizmor
# checks were split into their own job, to avoid blocking the matrix
# on their Docker image pulls.
#
# Invoked as bash rather than through its `npm run lint:github:workflows`
# alias (which stays, for local use) so this job needs no setup-node at
# all: the repo's devEngines.packageManager floor (npm >=11.16, onFail:
# error) makes *any project-scoped* npm call — even a bare `npm run` —
# fail with EBADDEVENGINES on the runner's ambient npm 10, and installing
# a Node just to satisfy that would cost this 1-minute job the whole
# toolchain setup while the matrix waits on it. Nothing else here
# touches npm: the version reader below is dependency-free CJS that
# runs on any Node.
# Keep this in sync if the script is ever moved or renamed.
- name: Lint workflow timeouts
run: bash scripts/lint-workflow-timeouts.sh
- name: Read GemStone integration versions
id: read-gemstone-integration-versions
# actionlint/shellcheck(SC2086): quote $GITHUB_OUTPUT to prevent
# word splitting/globbing
run: |
echo "versions=$(node client/bin/gemstone-integration-versions.js)" >> "$GITHUB_OUTPUT"
echo "oldest-version=$(node client/bin/gemstone-integration-versions.js --oldest)" >> "$GITHUB_OUTPUT"
lint-workflows:
name: Lint GitHub Actions workflows
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
# zizmor(unpinned-uses): actions must be pinned to a commit SHA, not
# a mutable tag like @v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: |
.github/workflows
sparse-checkout-cone-mode: false
# zizmor(artipacked): checkout must not persist the token past
# this job
persist-credentials: false
- name: Run actionlint
# actionlint/shellcheck(SC2046): quote $(pwd) to prevent word
# splitting/globbing. Pinned to a digest, not a mutable tag, for the
# same supply-chain reason as the zizmor(unpinned-uses) actions above.
run: docker run --rm -v "$(pwd)":/repo --workdir /repo rhysd/actionlint@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667 -color # 1.7.12
- name: Run zizmor
uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2
with:
advanced-security: false
lint:
name: Lint & Format
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
# zizmor(unpinned-uses): actions must be pinned to a commit SHA, not
# a mutable tag like @v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# zizmor(artipacked): checkout must not persist the token past
# this job
persist-credentials: false
- name: Setup Node.js
# zizmor(unpinned-uses): actions must be pinned to a commit SHA,
# not a mutable tag like @v6
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Supply-chain config check
run: npm run lint:supply-chain
- name: GCI header version map
run: npm run lint:gci-headers
- name: Lockfile lint
run: npm run lint:lockfile
- name: Format check
run: npm run format:check
- name: Lint
run: npm run lint
- name: Audit registry signatures
run: npm audit signatures
health-check:
timeout-minutes: 5
name: GemStone S/64 ${{ matrix.gemstone-version }}${{ matrix.node-version && format(' (Node {0})', matrix.node-version) || '' }}
needs: prepare
runs-on: ubuntu-latest
strategy:
matrix:
gemstone-version: ${{ fromJson(needs.prepare.outputs.versions) }}
node-version: [''] # empty ⇒ dev jobs resolve Node from .nvmrc (node-version-file); floor include overrides
# node-version's declared axis value is only '' (which falls back to
# the .nvmrc Node version); '22.15.1' isn't one of the declared
# values, so GitHub can't merge this into an existing combination —
# it adds an additional job pinned to the oldest GemStone version
# and the Node floor, as a smoke test to verify tests run on the
# Node floor too. This is to support older VS Code versions.
include:
- gemstone-version: ${{ needs.prepare.outputs.oldest-version }}
node-version: '22.15.1' # pin the floor for the smoke-test
steps:
- name: Checkout code
# zizmor(unpinned-uses): actions must be pinned to a commit SHA,
# not a mutable tag like @v6
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# zizmor(artipacked): checkout must not persist the token past
# this job
persist-credentials: false
# Two setup-node steps, split on the same predicate as the matrix
# `include` below: the dev legs leave node-version empty and resolve
# Node from .nvmrc, while the floor leg pins it. They're separate steps
# rather than one parameterized step because the floor leg has to turn
# off *both* cache inputs (see its comment), and expressing that as a
# pair of coordinated ${{ }} ternaries on one step is easy to get
# subtly wrong.
- name: Setup Node.js
if: ${{ !matrix.node-version }}
# zizmor(unpinned-uses): actions must be pinned to a commit SHA,
# not a mutable tag like @v6
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
# The floor leg's Node 22 bundles npm 10, which can't satisfy this
# repo's devEngines.packageManager floor (npm >=11.16.0, onFail:
# error) — so *any project-scoped* npm invocation on it fails with
# EBADDEVENGINES, including the innocuous `npm config get cache` that
# setup-node runs itself to locate the cache directory. That happens
# during setup, before the "Pin npm to an exact version that owns the lockfile"
# step below can upgrade npm, so the cache lookup has to be suppressed
# outright.
#
# Both inputs are required. setup-node only consults
# package-manager-cache when `cache` is unset (`if (cache) ... else if
# (packagemanagercache) ...`), so `cache: 'npm'` silently overrides it;
# but leaving `cache` unset isn't enough either, because the auto-
# detection it falls back to reads devEngines.packageManager — which
# this repo now sets — and enables npm caching anyway.
#
# Dropping the cache only costs this one smoke-test leg a warm npm
# cache; the dev legs above keep theirs.
- name: Setup Node.js (Node floor smoke-test leg)
if: ${{ matrix.node-version }}
# zizmor(unpinned-uses): actions must be pinned to a commit SHA,
# not a mutable tag like @v6
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node-version }}
package-manager-cache: false
# package-lock.json is generated by npm 11, which prunes nested
# optional-peer entries (e.g. jsdom's @noble/hashes) that npm 10
# still expects. npm 10 walks the tree, doesn't find them, and fails
# `npm ci` with EUSAGE "Missing: ... from lock file". The floor
# include job's Node 22 bundles npm 10, so bring it up to an exact
# npm 11 that owns the lockfile. The `if` restricts this to the Node 22
# floor job: the dev jobs leave node-version empty, resolve Node from
# .nvmrc, and already ship npm 11. Update the guard when the floor
# moves to a new major — and drop the step entirely once the floor
# reaches npm >= 11.16 (no Node 22 release ever will: the 22 line
# caps at npm 10.9.8).
#
# zizmor(adhoc-packages): installing a package outside a lockfile is
# flagged as supply-chain surface, but no Node 22 release bundles npm
# 11 (the earliest that does is v24.0.0), so the floor job can't get
# it any other way. Pinned to an exact version rather than the `11`
# dist-tag so this leg installs the same toolchain on every run and a
# fresh npm release can't change it underneath us — bump it
# deliberately, together with the same pin in acceptance/Dockerfile.
- name: Pin npm to an exact version that owns the lockfile
if: startsWith(matrix.node-version, '22.')
run: npm install --global npm@11.17.0 # zizmor: ignore[adhoc-packages] -- npm 11 owns the lockfile and no Node 22 release bundles it
- name: Install dependencies
run: npm ci
- name: Compile
run: npm run compile
- name: Cache GemStone installation
# zizmor(unpinned-uses): actions must be pinned to a commit SHA,
# not a mutable tag like @v6
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: client/tmp/downloads
key: gemstone-download-${{ runner.os }}-${{ matrix.gemstone-version }}
- name: Setup test environment
# zizmor(template-injection): bind the untrusted matrix value to an
# env var instead of interpolating ${{ }} directly into the script
env:
GEMSTONE_VERSION: ${{ matrix.gemstone-version }}
run: npm run test:server:start -- "$GEMSTONE_VERSION"
- name: Run tests (without server plugin)
# zizmor(template-injection): bind the untrusted matrix values to an
# env var instead of interpolating ${{ }} directly into the script
env:
VITEST_JSON_OUTPUT: ${{ github.workspace }}/test-results/skips-${{ matrix.gemstone-version }}-${{ matrix.node-version || 'nvmrc' }}-bare.json
run: npm test
- name: Upload skip report (without server plugin)
if: always()
# zizmor(unpinned-uses): actions must be pinned to a commit SHA,
# not a mutable tag like @v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: skips-${{ matrix.gemstone-version }}-${{ matrix.node-version || 'nvmrc' }}-bare
path: test-results/skips-${{ matrix.gemstone-version }}-${{ matrix.node-version || 'nvmrc' }}-bare.json
if-no-files-found: ignore
- name: Install server plugin
run: npm run test:server:install-plugin
- name: Run tests (with server plugin)
# zizmor(template-injection): bind the untrusted matrix values to an
# env var instead of interpolating ${{ }} directly into the script
env:
VITEST_JSON_OUTPUT: ${{ github.workspace }}/test-results/skips-${{ matrix.gemstone-version }}-${{ matrix.node-version || 'nvmrc' }}-plugin.json
run: npm test
- name: Upload skip report (with server plugin)
if: always()
# zizmor(unpinned-uses): actions must be pinned to a commit SHA,
# not a mutable tag like @v7
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: skips-${{ matrix.gemstone-version }}-${{ matrix.node-version || 'nvmrc' }}-plugin
path: test-results/skips-${{ matrix.gemstone-version }}-${{ matrix.node-version || 'nvmrc' }}-plugin.json
if-no-files-found: ignore
package-check:
name: Verify vsce packaging
timeout-minutes: 5
needs: prepare
runs-on: ubuntu-latest
steps:
# zizmor(unpinned-uses): actions must be pinned to a commit SHA, not
# a mutable tag like @v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# zizmor(artipacked): checkout must not persist the token past
# this job
persist-credentials: false
- name: Setup Node.js
# zizmor(unpinned-uses): actions must be pinned to a commit SHA,
# not a mutable tag like @v6
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: '.nvmrc'
cache: 'npm'
- name: Install dependencies
run: npm ci
# Runs vsce's full packaging path (esbuild bundle + the production
# dependency walk `npm list --production --parseable --depth=99999`).
# An `overrides` entry resolved outside its declaring package's range
# marks that edge `invalid`, `npm list` exits non-zero, and packaging
# aborts — invisible to npm ci / lint / compile / test, and it has
# broken a release twice (see issue #351).
- name: Verify vsce packaging
run: npm run package
# Single stable check for branch protection / the merge queue to require.
# The matrix job's own check names embed the matrix values, so requiring
# `test` directly is brittle; this fans them in to one name instead.
# `if: always()` is essential: without it the job would be SKIPPED whenever a
# dependency fails, a required check would never report a conclusion, and the
# PR (or queue entry) would hang forever.
ci-complete:
name: ci-complete
if: always()
needs: [prepare, lint-workflows, lint, health-check, package-check]
runs-on: ubuntu-22.04
timeout-minutes: 1
steps:
- name: Verify all CI jobs succeeded
run: |
if [ "${{ needs.prepare.result }}" != "success" ] ||
[ "${{ needs.lint-workflows.result }}" != "success" ] ||
[ "${{ needs.lint.result }}" != "success" ] ||
[ "${{ needs.health-check.result }}" != "success" ] ||
[ "${{ needs.package-check.result }}" != "success" ]; then
echo "prepare=${{ needs.prepare.result }}"
echo "lint-workflows=${{ needs.lint-workflows.result }}"
echo "lint=${{ needs.lint.result }}"
echo "health-check=${{ needs.health-check.result }}"
echo "package-check=${{ needs.package-check.result }}"
exit 1
fi
# Report-only, first step toward a future gate: aggregates the per-suite-run
# JSON reports (one per GemStone version x bare/plugin run) to find tests that
# are skipped (ctx.skip / it.skip / .todo) in every suite run — i.e. never
# actually executed anywhere — and posts them to the job summary. Never fails
# the build; see summarize-skipped-tests.mjs. Deliberately independent of
# ci-complete: it's report-only and never fails, so branch protection / the
# merge queue neither waits for it nor cares about its outcome.
report-skips:
name: Skipped tests report
timeout-minutes: 1
needs: [prepare, health-check]
if: always()
runs-on: ubuntu-latest
# download-artifact needs actions:read to list/fetch artifacts from this
# run; the top-level contents:read block doesn't cover that
permissions:
contents: read
actions: read
steps:
- name: Checkout code
# zizmor(unpinned-uses): actions must be pinned to a commit SHA,
# not a mutable tag like @v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
sparse-checkout: |
scripts/summarize-skipped-tests.mjs
sparse-checkout-cone-mode: false
# zizmor(artipacked): checkout must not persist the token past
# this job
persist-credentials: false
- name: Download skip reports
# zizmor(unpinned-uses): actions must be pinned to a commit SHA,
# not a mutable tag like @v8
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: skips-*
path: test-results
merge-multiple: true
- name: Summarize skipped tests
run: node scripts/summarize-skipped-tests.mjs test-results