diff --git a/.github/workflows/0-test-changeset-coverage.yaml b/.github/workflows/0-test-changeset-coverage.yaml new file mode 100644 index 00000000..6d8a2d23 --- /dev/null +++ b/.github/workflows/0-test-changeset-coverage.yaml @@ -0,0 +1,41 @@ +name: 'Test: changeset/check-coverage' + +on: + workflow_call: + workflow_dispatch: # manual escape hatch — also lets us re-run after a workflow edit + pull_request: + paths: + - 'actions/changeset/check-coverage/**' + push: + paths: + - 'actions/changeset/check-coverage/**' + branches: ['v4', 'v4-beta'] + +jobs: + bats: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + # No `version:` — the repo's root package.json sets `packageManager`, + # and pnpm/action-setup errors if both are specified. + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install bats + run: sudo apt-get update && sudo apt-get install -y bats + + - name: Verify required tools are present + run: | + which jq yq pnpm bats + jq --version + yq --version + pnpm --version + bats --version + + - name: Run changeset/check-coverage bats suite + working-directory: actions/changeset/check-coverage + run: bats test/coverage.bats diff --git a/.github/workflows/node-simple-pnpm.yaml b/.github/workflows/node-simple-pnpm.yaml index b35d737b..455171c9 100644 --- a/.github/workflows/node-simple-pnpm.yaml +++ b/.github/workflows/node-simple-pnpm.yaml @@ -582,6 +582,55 @@ jobs: run: | pnpm changeset status --since="origin/${BRANCH_NAME}" + changeset-coverage: + name: changeset coverage (diagnostic) + runs-on: ubuntu-latest + # Diagnostic only: surface per-package changeset gaps as a red check, never block. + # The check-coverage step omits continue-on-error, so a gap (exit 1) fails this + # job and its check turns red. Job-level continue-on-error keeps the run green and + # the merge available. Separate from check-changesets so the native + # `changeset status` gate keeps blocking. Keep this job out of required checks. + continue-on-error: true + if: github.event_name == 'pull_request' || github.event_name == 'merge_group' + needs: + - metadata + steps: + - id: context + uses: milaboratory/github-ci/actions/context@v4 + + - uses: milaboratory/github-ci/actions/env@v4 + with: + inputs: ${{ inputs.env }} + secrets: ${{ secrets.env }} + + - uses: actions/checkout@v4 + with: + lfs: ${{ inputs.checkout-git-lfs }} + submodules: ${{ inputs.checkout-submodules }} + fetch-depth: '0' + + - name: Prepare environment for building a NodeJS application + uses: milaboratory/github-ci/actions/node/prepare-pnpm@v4 + env: + PNPM_VERSION: ${{ needs.metadata.outputs.pnpm-version }} + with: + node-version: ${{ inputs.node-version }} + cache-version: ${{ inputs.cache-version }} + pnpm-version: ${{ env.PNPM_VERSION || inputs.pnpm-version }} + cache-hashfiles-search-path: ${{ inputs.cache-hashfiles-search-path }} + npmrc-config: ${{ inputs.npmrc-config }} + + - name: Install NodeJS packages with pnpm + uses: milaboratory/github-ci/actions/shell@v4 + with: + run: | + pnpm install --frozen-lockfile --prefer-offline + + - name: Check changeset coverage + uses: milaboratory/github-ci/actions/changeset/check-coverage@v4 + with: + base-branch: ${{ inputs.changeset-default-branch }} + pre-calculated-build: name: pre-build runs-on: ${{ inputs.gha-runner-label }} diff --git a/actions/changeset/check-coverage/action.yaml b/actions/changeset/check-coverage/action.yaml new file mode 100644 index 00000000..07f97209 --- /dev/null +++ b/actions/changeset/check-coverage/action.yaml @@ -0,0 +1,30 @@ +name: Check changeset coverage +author: 'MiLaboratories' +description: | + Fail if a PR's changesets don't cover every workspace package it modifies. + + Catches two common gaps: + - editing files inside a workspace package (e.g. block code under + packages//) without adding that package to the changeset; + - bumping a catalog version in pnpm-workspace.yaml without a matching + bump for the workflow packages that consume it via `catalog:`. + + Runs after `pnpm install`. Requires the runner to have `pnpm`, `jq`, and + `yq` (mikefarah, v4+) on PATH — all pre-installed on GitHub-hosted + ubuntu-latest images. + +inputs: + base-branch: + description: Base branch to diff against (e.g. main). + required: true + +runs: + using: 'composite' + + steps: + - name: Check changeset coverage + shell: bash + env: + ACTION_PATH: ${{ github.action_path }} + BASE_BRANCH: ${{ inputs.base-branch }} + run: '${ACTION_PATH}/check-coverage.sh' diff --git a/actions/changeset/check-coverage/check-coverage.sh b/actions/changeset/check-coverage/check-coverage.sh new file mode 100755 index 00000000..a7ad8311 --- /dev/null +++ b/actions/changeset/check-coverage/check-coverage.sh @@ -0,0 +1,222 @@ +#!/usr/bin/env bash +# +# Verify the PR's changesets bump every workspace package the PR modifies. +# Exit 1 on a coverage gap; exit 2 on tooling failure; exit 0 otherwise. +# +# Two sources of "modified": +# +# 1. Direct edits to a workspace package's files, detected via +# `pnpm --filter '[]' list` — pnpm runs the per-package +# git-diff check itself. +# +# 2. Catalog version bumps in pnpm-workspace.yaml: for each touched +# catalog key, find workspace packages that consume it via +# `"": "catalog:..."` and require those packages to bump. +# +# Skips private (unpublished) workspace packages — they never appear in +# the changeset's release set. +# +# Runs from the repo root after `pnpm install`. + +set -o nounset +set -o errexit +set -o pipefail + +: "${BASE_BRANCH:?BASE_BRANCH required}" + +log() { printf '%s\n' "$*" >&2; } +err() { printf '::error::%s\n' "$*" >&2; } + +# Associative arrays (`declare -A`) require bash 4+. The GitHub-hosted +# ubuntu-latest runners ship bash 5+, so this guard is a no-op in CI today. +# It matters when running locally on macOS: /bin/bash is still 3.2, and +# without homebrew's bash earlier on PATH the script would otherwise abort +# at the first `declare -A` with a cryptic `invalid option`. Fail fast with +# a useful message instead. +if [ "${BASH_VERSINFO[0]:-0}" -lt 4 ]; then + err "check-coverage requires bash 4+ (found ${BASH_VERSION:-unknown})." + exit 2 +fi + +# Use cwd-relative paths for tmp files — changeset's --output mis-handles +# absolute paths on macOS (prepends cwd, causing ENOENT). Cwd-relative is +# safe on every platform. +status_json=".changeset-coverage-status-$$.json" +pkg_list_json=".changeset-coverage-pkgs-$$.json" + +# --------------------------------------------------------------------------- +# 1. Bumped set from `changeset status --output=...`. +# --------------------------------------------------------------------------- +trap 'rm -f "${status_json}" "${pkg_list_json}"' EXIT + +# Invoke the binary directly. `pnpm exec` and `pnpm