Skip to content

ci: add changeset coverage check#168

Merged
PaulNewling merged 15 commits into
v4-betafrom
feat/changeset-coverage-check
May 28, 2026
Merged

ci: add changeset coverage check#168
PaulNewling merged 15 commits into
v4-betafrom
feat/changeset-coverage-check

Conversation

@PaulNewling
Copy link
Copy Markdown
Collaborator

@PaulNewling PaulNewling commented May 26, 2026

New composite action actions/changeset/check-coverage — fails when a PR's changesets don't bump every workspace package the PR modifies. Catches the catalog-software gap (a pnpm-workspace.yaml catalog version bumped without a matching changeset for the consuming workflow package).

Wired into the check-changesets job in node-simple-pnpm.yaml as a continue-on-error: true step. Reports missing packages in the job log. Never blocks merge today.

How "modified" is detected

  1. Direct package editspnpm -r --filter '[origin/<base>]' list selects packages whose own files changed. pnpm runs the per-package git diff itself, so no manual longest-prefix matching or ignore-list maintenance.
  2. Catalog bumps in pnpm-workspace.yamlyq flattens .catalog and .catalogs.* to key=value; uniq -u over old vs head gives the changed keys; jq then finds every workspace consumer that depends on the key via "catalog:...". Structural parse avoids the false-positive surface of a regex on the raw diff (e.g. an unrelated overrides.<name> bump no longer leaks into the catalog path).

Skips private (unpublished) packages — they never appear in a changeset's release set.

Tests

actions/changeset/check-coverage/test/ ships a bats suite (14 cases) against a minimal pnpm-workspace fixture. .github/workflows/0-test-changeset-coverage.yaml runs it on every PR that touches the action source.

Local: bats actions/changeset/check-coverage/test/coverage.bats. See test/README.md for fixture and runner deps.

Known follow-ups (out of scope here)

  • Named-catalog over-reporting. The consumer-match jq predicate uses startswith("catalog"), so it doesn't distinguish "catalog:" from "catalog:<name>". Not exercised today (no named catalogs in target repo). Tighten when .catalogs.* are introduced and add a fixture variant for them.
  • continue-on-error: true ramp. Stays warn-only through the canary. Tighten to blocking after the gate proves itself — same conditional as the surrounding preflight-* jobs fits: enforce on changeset-default-branch and merge_group, warn-only on PRs.
  • Pre-existing sed-scope bug in merge-beta.sh / fix-beta.sh. Flagged separately. The sed flips third-party refs too, leaving actions/checkout@v4-beta etc. unresolvable while the workflow lives on v4-beta. Not addressed in this PR; ticket open.

Greptile Summary

This PR introduces a new actions/changeset/check-coverage composite action that detects when a PR modifies workspace packages without a corresponding changeset bump, wired into the check-changesets job as a non-blocking (continue-on-error: true) step. It is accompanied by a bats test suite and a dedicated CI workflow.

  • check-coverage.sh: detects two categories of missed bumps — direct file edits inside a workspace package (via pnpm --filter '[origin/base]' list) and catalog version changes in pnpm-workspace.yaml (via structural yq diffing of .catalog/.catalogs); exits 1 on a gap, 2 on tooling failure, 0 on clean coverage.
  • coverage.bats + helpers.bash: thirteen bats tests covering the main paths (direct edits, catalog bumps, private packages, partial coverage, empty changesets) using a shared pnpm install base with per-test tar-copied workspaces for speed.
  • node-simple-pnpm.yaml: the coverage check is added after the existing "Check for Changesets" step with if: always() so it runs even when there are no changesets yet, surfacing every un-bumped package at once.

Confidence Score: 5/5

Safe to merge; the coverage check is non-blocking and the core detection logic is solid for the common single-catalog case.

The change is entirely additive and non-blocking. The detection logic is well-tested with a thorough bats suite. The two edge cases flagged have no impact on the critical path and won't cause data loss or incorrect merge decisions.

The cat_pairs section of check-coverage.sh (lines 163–178) is worth revisiting if the repo ever adopts multiple named catalogs with overlapping package keys.

Important Files Changed

Filename Overview
actions/changeset/check-coverage/check-coverage.sh Core coverage-check logic; two minor edge cases: named-catalog consumers can be over-flagged, and the ::error:: annotations lack file/line targeting claimed in the PR description.
.github/workflows/node-simple-pnpm.yaml Wires the new coverage-check step with continue-on-error: true; always() condition runs even on job cancellation — consider adding !cancelled().
.github/workflows/0-test-changeset-coverage.yaml New test workflow; installs bats explicitly but relies on pre-installed mikefarah/yq — the verify step provides a clear failure message if it's ever absent.
actions/changeset/check-coverage/action.yaml Minimal composite action definition; delegates entirely to the shell script via ACTION_PATH and BASE_BRANCH env vars.
actions/changeset/check-coverage/test/coverage.bats Good bats suite covering direct edits, catalog bumps, partial coverage, private packages, and empty-changeset corner cases.
actions/changeset/check-coverage/test/helpers.bash Clean test helpers; shared pnpm install with tar-based per-test workspace copies is a good pattern for speed.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[check-changesets job] --> B[pnpm install]
    B --> C[Check for Changesets\npnpm changeset status]
    C -->|always| D[check-coverage action]
    D --> E{run check-coverage.sh}
    E --> F[1. Build bumped_set\nchangeset status --output JSON]
    E --> G[2a. Build required_set\npnpm filter list direct edits]
    E --> H{pnpm-workspace.yaml\nchanged?}
    H -->|yes| I[2b. yq diff old vs new\ncatalog entries]
    I --> J[Find consumers of\ntouched catalog keys\nvia jq in package.json]
    J --> K[Add to required_set]
    G --> K
    F --> L{required_set minus\nbumped_set}
    K --> L
    L -->|empty| M[exit 0]
    L -->|non-empty| N[emit error annotations\nexit 1]
    N --> O[continue-on-error: true\njob continues]
Loading
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 3
.github/workflows/node-simple-pnpm.yaml:586-588
The `always()` condition will execute this step even when the job is cancelled mid-run (e.g. a manual cancel or a concurrency group eviction). Adding `!cancelled()` avoids wasting runner minutes on a dead workflow run.

```suggestion
        if: |
          always() && !cancelled() &&
          (github.event_name == 'pull_request' || github.event_name == 'merge_group')
```

### Issue 2 of 3
actions/changeset/check-coverage/check-coverage.sh:189-192
The filter `startswith("catalog")` matches any version spec that starts with the literal string "catalog". The pnpm protocol prefix is always `catalog:` (with a colon), so `startswith("catalog:")` is the more precise guard and avoids any hypothetical false match from a non-pnpm value that happened to begin with "catalog".

```suggestion
        if jq -e --arg k "${key}" '
            [.dependencies?, .devDependencies?, .peerDependencies?, .optionalDependencies?]
            | map(select(. != null) | to_entries) | add // []
            | map(select(.key == $k and ((.value // "") | startswith("catalog:"))))
```

### Issue 3 of 3
actions/changeset/check-coverage/check-coverage.sh:163-178
**Named-catalog changes can over-flag unrelated consumers**

`cat_pairs` flattens both `.catalog` and all named `.catalogs` into a single stream that loses which named catalog each entry belongs to. When two named catalogs both declare the same package at different pinned versions and only one pin changes, `sort | uniq -u` still emits the bare package name as "touched." The downstream consumer search then requires bumps from packages that reference the *unchanged* named catalog too. Repos using multiple named catalogs with overlapping package names will see persistent false-positive coverage-gap annotations.

Reviews (2): Last reviewed commit: "Revert "Fix incomplete revert: flip rema..." | Re-trigger Greptile

New composite action `actions/changeset/check-coverage` that fails when a
PR's changesets don't bump every workspace package the PR modifies.
Catches the catalog-software gap (pnpm-workspace.yaml catalog version
bumped without a matching changeset for the consuming workflow package).

Wired into the existing `check-changesets` job in node-simple-pnpm.yaml
as a `continue-on-error: true` step on PR / merge_group events. Emits
`::error file=…,line=…::` annotations inline on the PR diff; never
blocks merge.
TODO: REVERT before merging this PR — flip
@feat/changeset-coverage-check back to @v4-beta. The action only
lives on this branch right now; consumers pinning the workflow at
this branch otherwise resolve the nested uses: against v4-beta and
fail to find the action.
The over-broad sed in merge-beta.sh/fix-beta.sh flips @v4 → @v4-beta on
every ref, including third-party actions. actions/checkout and
aws-actions/configure-aws-credentials have no @v4-beta tag upstream,
so the workflow fails to load when a consumer pins this branch.

Only this file is fixed — the minimum needed to canary-test the new
coverage check. The same bug affects 27 other files on v4-beta and
needs a separate sed-scope fix in merge-beta.sh / fix-beta.sh.
Same pre-existing sed-scope bug as the previous commit, now hit in the
composite actions check-changesets transitively calls. Restores
actions/setup-node, pnpm/action-setup, and actions/cache to @v4.
@PaulNewling PaulNewling marked this pull request as ready for review May 26, 2026 23:09
The pre-existing sed-scope bug in merge-beta.sh / fix-beta.sh will be
handled in a separate ticket. Reverts d5f16a1 and 335ff48.
Comment thread actions/changeset/check-coverage/check-coverage.sh Outdated
Comment thread actions/changeset/check-coverage/check-coverage.sh Outdated
…s suite

Refactor check-coverage.sh (267 → 213 lines):
- step 4a now uses `pnpm -r --filter '[origin/<base>]' list` instead of
  hand-rolled longest-prefix matching over the changed-files list and the
  manual ignore patterns. Root-level paths (.github/, docs/, README) aren't
  in any package directory, so pnpm's filter naturally excludes them.
- step 4b parses pnpm-workspace.yaml structurally with yq (flat key=value
  pairs from both .catalog and .catalogs.*) instead of a line-level regex
  over the diff. Avoids the regex's false-positive surface — e.g. an
  unrelated `overrides.<name>` bump no longer spuriously requires
  catalog-consumer bumps.
- workspace map is now built lazily, only when pnpm-workspace.yaml changed.
- drops the `::error file=,line=::` per-file annotation plumbing. Devs land
  on .changeset/ when fixing this anyway; the summary block still names
  every missing package.

Add bats suite under test/ with a minimal pnpm-workspace fixture and a CI
workflow (0-test-changeset-coverage.yaml) that runs the suite on PRs that
touch the action. 14 cases cover direct edits, catalog miss/hit/partial,
private packages, structural-vs-regex catalog parsing, and the no-
changesets corner.
Drop the workflow-self path from the trigger filter — the suite should
only run when the action it tests actually changes. Add workflow_dispatch
as a manual escape hatch for re-running after a workflow edit.
declare -A is bash 4+. macOS ships 3.2 at /bin/bash; without homebrew's
bash earlier on PATH the script aborts at the first declare with a
cryptic 'invalid option'. Detect at start and emit a useful error
instead.
Note that ubuntu-latest runners ship bash 5+ — so the guard is a no-op
in CI; it exists to help local runs on macOS, whose /bin/bash is still
3.2 and would otherwise produce a cryptic 'invalid option' error.
@PaulNewling
Copy link
Copy Markdown
Collaborator Author

@greptileai

- check-coverage.sh: tighten catalog detection to `startswith("catalog:")`
  (was `startswith("catalog")`) — pnpm's protocol prefix is the literal
  `catalog:`.

- node-simple-pnpm.yaml: add `!cancelled()` to the coverage-check
  conditional so the step skips on cancelled jobs (manual cancel,
  concurrency-group eviction) instead of burning runner minutes.

- test/helpers.bash: harden `_build_base_workspace` — `set -e` inside
  the subshell, capture output to a log file, dump only on failure.
  Without this, a silently failing `pnpm install` corrupts the base
  workspace and every test then fails with a confusing error pointing
  at the script under test, not the install.

- test/coverage.bats: three new tests.
  - BASE_BRANCH input: production callers pass `v4` (or `master`),
    not `main`. Five `origin/${BASE_BRANCH}` references in the script
    could regress to hardcoded `main`; this guards against it.
  - exit 2 on missing changeset binary: locks down the tooling-failure
    contract distinct from exit 1 (coverage gap).
  - named-catalog over-flagging: skip-marked, documents the deferred
    bug from the PR body with a runnable fixture for the follow-up.
…e primary case

The prior description led with the catalog-version gap, but the more
common case the action catches is the simple one: edit files inside a
workspace package, forget to add that package to the changeset. List
both, with the direct-edit case first.
@PaulNewling PaulNewling merged commit 9c30b0e into v4-beta May 28, 2026
1 check passed
@PaulNewling PaulNewling deleted the feat/changeset-coverage-check branch May 28, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant