Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 11 additions & 10 deletions actions/changeset/check-coverage/action.yaml
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
name: Check changeset coverage
author: 'MiLaboratories'
description: |
Fail if a PR's changesets don't cover every workspace package it modifies.
Fail if a PR's changesets don't cover every workspace package whose own
files it edits — e.g. editing block code under packages/<pkg>/ without
adding that package to the changeset.

Catches two common gaps:
- editing files inside a workspace package (e.g. block code under
packages/<pkg>/) without adding that package to the changeset;
- bumping a catalog version in pnpm-workspace.yaml without a matching
bump for packages that consume it as a runtime dependency via
`catalog:` — build-tool bumps in devDependencies are ignored.
Dependency-version bumps are intentionally NOT flagged, including catalog
bumps in pnpm-workspace.yaml. A catalog entry always pins an external
package, and `pnpm changeset` never requires a release for an external
dependency bump — it propagates versions through the internal dependency
chain automatically at `changeset version` time. Flagging consumers of a
bumped catalog dependency would over-report relative to `pnpm changeset`.

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.
Runs after `pnpm install`. Requires the runner to have `pnpm` and `jq` on
PATH — both pre-installed on GitHub-hosted ubuntu-latest images.

inputs:
base-branch:
Expand Down
115 changes: 21 additions & 94 deletions actions/changeset/check-coverage/check-coverage.sh
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
#!/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.
# Verify the PR has a changeset for every workspace package whose own files
# it edits. Exit 1 on a coverage gap; exit 2 on tooling failure; exit 0
# otherwise.
#
# Two sources of "modified":
# "Modified" means a direct edit to a workspace package's own files, detected
# via `pnpm --filter '[<base>]' list` — pnpm runs the per-package git-diff
# check itself. Root-level paths (`.github/`, `docs/`, `pnpm-workspace.yaml`,
# `README.md`) live in no package directory, so they never trigger inclusion.
#
# 1. Direct edits to a workspace package's files, detected via
# `pnpm --filter '[<base>]' 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 as a runtime
# dependency (`dependencies` or `peerDependencies`) via
# `"<key>": "catalog:..."` and require those packages to bump.
#
# Runtime sections only — skip devDependencies and optionalDependencies.
# A build-tool bump (e.g. @platforma-sdk/block-tools in a block's model,
# @platforma-sdk/tengo-builder in its workflow) leaves the published
# artifact unchanged, and `pnpm changeset` ignores it too. Counting it
# flagged packages the PR never touched — e.g. a UI-only change that
# carried a shared build-tool bump.
# Dependency-version bumps never require a changeset — including catalog
# bumps in pnpm-workspace.yaml. A catalog entry always pins an *external*
# package (workspace packages are referenced via `workspace:*`, never
# `catalog:`). `pnpm changeset` ignores such a bump twice over: it never
# releases a package because an external dependency's version changed, and it
# propagates internal bumps through the dependency chain automatically at
# `changeset version` time. So requiring a hand-written changeset for a
# package that only "changed" via a dependency bump over-reports relative to
# `pnpm changeset` — see platforma-open/clonotype-space#95, where a
# `@platforma-sdk/workflow-tengo` catalog bump spuriously failed the
# untouched `.workflow` package.
#
# Skips private (unpublished) workspace packages — they never appear in
# the changeset's release set.
Expand Down Expand Up @@ -50,12 +50,11 @@ fi
# 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
trap 'rm -f "${status_json}"' EXIT

# Invoke the binary directly. `pnpm exec` and `pnpm <script>` spawn subshells
# that lose `node_modules/.bin` from PATH on repeated invocations. The action
Expand Down Expand Up @@ -118,13 +117,13 @@ require_pkg() {
required_reason["${pkg}"]="${required_reason[${pkg}]:-}${reason}; "
}

# 2a. Direct workspace-package edits.
# Direct workspace-package edits.
#
# `pnpm --filter '[<since>]' list` selects packages whose own files changed
# (pnpm runs the per-package `git diff` itself). Root-level paths like
# `.github/`, `docs/`, `pnpm-workspace.yaml`, or `README.md` are not in any
# package directory and so don't trigger inclusion — no manual ignore list
# needed.
# needed, and a catalog bump in pnpm-workspace.yaml never reaches this filter.
#
# The jq filter drops private packages here (they never appear in a
# changeset's release set), so `require_pkg` doesn't need to re-check.
Expand All @@ -137,78 +136,6 @@ done < <(

log "Direct-edit packages: ${#required_set[@]}"

# 2b. Catalog version bumps in pnpm-workspace.yaml.
#
# Only runs when the workspace yaml itself changed. Builds a full
# workspace map so we can find every consumer of each touched catalog key.
if git diff --name-only "origin/${BASE_BRANCH}...HEAD" | grep -qx 'pnpm-workspace.yaml'; then
pnpm -r list --depth -1 --json >"${pkg_list_json}"

# pnpm returns canonical absolute paths; strip the workspace root via
# `pwd -P` so the result matches the workspace's view on either platform.
workspace_root="$(pwd -P)"
declare -A pkg_name_to_dir=()
declare -A pkg_is_private=()

while IFS=$'\t' read -r pkg_name pkg_path pkg_private; do
[ -z "${pkg_name}" ] && continue # workspace root has no name
rel="${pkg_path#${workspace_root}/}"
[ "${rel}" = "${pkg_path}" ] && continue # workspace root itself
pkg_name_to_dir["${pkg_name}"]="${rel}"
pkg_is_private["${pkg_name}"]="${pkg_private}"
done < <(jq -r '
.[]
| select(.name != null and .name != "")
| [.name, .path, (.private // false | tostring)]
| @tsv
' "${pkg_list_json}")

# Extract catalog keys whose value changed (added, removed, or bumped).
# Parse both the base and head versions structurally with yq, flatten the
# default catalog and any named catalogs to `key=value` lines, then take
# entries unique to either side. Avoids the false-positive surface of a
# line-level regex on the raw diff.
cat_pairs() {
yq -e '
[
(.catalog // {} | to_entries[]),
(.catalogs // {} | to_entries[].value // {} | to_entries[])
] | .[] | .key + "=" + .value
' 2>/dev/null || true
}
old_pairs="$(git show "origin/${BASE_BRANCH}:pnpm-workspace.yaml" 2>/dev/null | cat_pairs || true)"
new_pairs="$(cat_pairs <pnpm-workspace.yaml || true)"
mapfile -t catalog_keys < <(
{ printf '%s\n' "${old_pairs}"; printf '%s\n' "${new_pairs}"; } \
| sed '/^$/d' \
| sort | uniq -u \
| sed -E 's/=.*//' \
| sort -u
)

if [ "${#catalog_keys[@]}" -gt 0 ]; then
log "Catalog keys touched: ${catalog_keys[*]}"
# For each catalog key, find workspace pkgs that depend on it with "catalog:".
for key in "${catalog_keys[@]}"; do
for name in "${!pkg_name_to_dir[@]}"; do
[ "${pkg_is_private[${name}]:-false}" = 'true' ] && continue
pj="${pkg_name_to_dir[${name}]}/package.json"
[ -f "${pj}" ] || continue
# Runtime sections only — a build-tool bump in devDependencies leaves
# the published artifact unchanged (see the header note).
if jq -e --arg k "${key}" '
[.dependencies?, .peerDependencies?]
| map(select(. != null) | to_entries) | add // []
| map(select(.key == $k and ((.value // "") | startswith("catalog:"))))
| length > 0
' "${pj}" >/dev/null 2>&1; then
require_pkg "${name}" "catalog dep '${key}' bumped"
fi
done
done
fi
fi

# ---------------------------------------------------------------------------
# 3. Compare required vs bumped.
# ---------------------------------------------------------------------------
Expand Down
118 changes: 36 additions & 82 deletions actions/changeset/check-coverage/test/coverage.bats
Original file line number Diff line number Diff line change
Expand Up @@ -67,68 +67,61 @@ setup() {
}

# ---------------------------------------------------------------------------
# Catalog bumps.
# Catalog / dependency bumps — intentionally NOT a coverage requirement.
#
# A catalog entry always pins an *external* package: workspace packages are
# referenced via `workspace:*`, never `catalog:`. `pnpm changeset` never adds
# a package to its release plan because an external dependency's version
# changed, and it propagates version bumps through the internal dependency
# chain automatically at `changeset version` time. So a catalog bump on its
# own requires no hand-written changeset — requiring one over-reports relative
# to `pnpm changeset`.
#
# Regression: platforma-open/clonotype-space#95. The `.workflow` package listed
# `@platforma-sdk/workflow-tengo` under `dependencies` as `catalog:`; the
# catalog bumped it (5.25.0 → 6.3.2) while the package's own files were
# untouched. The PR's changeset (model/ui/block) was complete per
# `pnpm changeset`, yet the old check failed the PR on `.workflow`.
# ---------------------------------------------------------------------------

@test "catalog bump without consumer bumps fails and names every consumer" {
@test "catalog bump alone requires no changeset (consumer files untouched)" {
# pkg-a and pkg-b both consume is-number via `dependencies: { is-number: catalog: }`
# — exactly the clonotype-space `.workflow` → workflow-tengo shape. Bumping
# the catalog without touching either package must pass.
bump_catalog 'is-number' '^7.0.1'
run_check
[ "${status}" -eq 1 ]
[[ "${output}" == *'@check-coverage-test/pkg-a'* ]]
[[ "${output}" == *'@check-coverage-test/pkg-b'* ]]
# pkg-c consumes is-string, not is-number — should not be flagged.
[[ "${output}" != *'@check-coverage-test/pkg-c'* ]]
[ "${status}" -eq 0 ]
}

@test "catalog bump with both consumers bumped passes" {
bump_catalog 'is-number' '^7.0.1'
add_changeset '"@check-coverage-test/pkg-a": patch
"@check-coverage-test/pkg-b": patch' 'bump is-number'
@test "runtime vs dev catalog dep is irrelevant — neither is flagged" {
# is-string is a runtime dep of pkg-c and a devDependency of pkg-dev. Bumping
# it flags neither: a dependency-version bump never requires a changeset.
bump_catalog 'is-string' '^1.0.8'
run_check
[ "${status}" -eq 0 ]
}

@test "catalog bump where only one consumer is bumped still fails for the other" {
@test "catalog bump alongside a direct edit only requires the edited package" {
# The simultaneous is-number catalog bump adds no requirements: only pkg-c,
# whose own files changed, must be covered. pkg-a/pkg-b consume is-number but
# were not edited.
touch_file 'packages/pkg-c/index.js'
bump_catalog 'is-number' '^7.0.1'
add_changeset '"@check-coverage-test/pkg-a": patch' 'bump is-number partial'
run_check
[ "${status}" -eq 1 ]
[[ "${output}" == *'@check-coverage-test/pkg-b'* ]]
[[ "${output}" != *'- @check-coverage-test/pkg-a'* ]]
}

@test "non-catalog YAML edits in pnpm-workspace.yaml don't trigger requirements" {
# An `overrides` entry that happens to share a name with a catalog key.
# Bumping it must NOT be treated as a catalog change — yq's structural
# parse ignores anything outside .catalog / .catalogs.
yq -i '.overrides."is-number" = "^7.0.5"' "${WORKSPACE}/pnpm-workspace.yaml"
git -C "${WORKSPACE}" commit --quiet -am 'bump override (not catalog)'
run_check
[ "${status}" -eq 0 ]
[[ "${output}" == *'@check-coverage-test/pkg-c'* ]]
[[ "${output}" != *'@check-coverage-test/pkg-a'* ]]
[[ "${output}" != *'@check-coverage-test/pkg-b'* ]]
}

@test "catalog bump for an unused key does not require any package" {
# Add then bump a catalog entry no one consumes.
yq -i '.catalog."unused-pkg" = "^1.0.0"' "${WORKSPACE}/pnpm-workspace.yaml"
git -C "${WORKSPACE}" commit --quiet -am 'add unused catalog key'
bump_catalog 'unused-pkg' '^1.0.1'
@test "direct edit covered by a changeset passes even with a concurrent catalog bump" {
touch_file 'packages/pkg-c/index.js'
bump_catalog 'is-number' '^7.0.1'
add_changeset '"@check-coverage-test/pkg-c": patch' 'edit pkg-c'
run_check
[ "${status}" -eq 0 ]
}

@test "catalog bump consumed only as a devDependency does not flag the consumer" {
# is-string is a runtime dep of pkg-c but only a devDependency of pkg-dev.
# Bumping it flags pkg-c and leaves pkg-dev alone — the block case where a
# build-tool catalog dep (block-tools, tengo-builder) bumps while the
# package stays untouched. Runtime-only scoping fixed this; pkg-dev was
# flagged before.
bump_catalog 'is-string' '^1.0.8'
run_check
[ "${status}" -eq 1 ]
[[ "${output}" == *'@check-coverage-test/pkg-c'* ]]
[[ "${output}" != *'@check-coverage-test/pkg-dev'* ]]
}

# ---------------------------------------------------------------------------
# Empty-changeset corner case.
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -178,42 +171,3 @@ setup() {
[ "${status}" -eq 2 ]
[[ "${output}" == *'changeset binary not found'* ]]
}

# ---------------------------------------------------------------------------
# Known limitations (skipped — document deferred follow-ups).
# ---------------------------------------------------------------------------

# Documents the deferred bug from PR #168: the catalog-key flatten in
# check-coverage.sh collapses `.catalogs.alpha.<key>` and `.catalogs.beta.<key>`
# into a single key=value stream, losing which named catalog owns each entry.
# When two named catalogs both pin the same package and only one bumps, the
# symmetric diff still emits the bare key — so consumers of the *unchanged*
# catalog get spuriously flagged. Un-skip once the script tracks the
# catalog-name discriminator.
@test "named-catalog change should not flag consumers of unchanged catalog" {
skip "Known limitation: catalog flatten loses named-catalog discriminator (PR #168 follow-up)"

yq -i '
.catalogs.alpha."is-number" = "^7.0.0" |
.catalogs.beta."is-number" = "^7.0.0"
' "${WORKSPACE}/pnpm-workspace.yaml"

# pkg-a → catalog:alpha; pkg-b → catalog:beta.
for pair in 'pkg-a:alpha' 'pkg-b:beta'; do
name="${pair%:*}"; cat="${pair#*:}"
pj="${WORKSPACE}/packages/${name}/package.json"
jq --arg c "catalog:${cat}" '.dependencies."is-number" = $c' "${pj}" >"${pj}.tmp"
mv "${pj}.tmp" "${pj}"
done
git -C "${WORKSPACE}" commit --quiet -am 'add named catalogs (alpha, beta)'

# Bump alpha only — beta is untouched.
yq -i '.catalogs.alpha."is-number" = "^7.0.1"' "${WORKSPACE}/pnpm-workspace.yaml"
git -C "${WORKSPACE}" commit --quiet -am 'bump alpha catalog only'

run_check
[ "${status}" -eq 1 ]
[[ "${output}" == *'@check-coverage-test/pkg-a'* ]]
# Currently fails: pkg-b is also flagged because of the catalog flatten.
[[ "${output}" != *'@check-coverage-test/pkg-b'* ]]
}
Loading