Health Check #1360
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: 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 |