Skip to content
Open
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
28 changes: 28 additions & 0 deletions .changeset/catalog-changeset-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'@gtbuchanan/cli': minor
---

Add `gtb changeset check`, a catalog-aware changeset gate.

`changeset status` maps changed files to packages, but a pnpm catalog moves
every dependency range to `pnpm-workspace.yaml` at the workspace root, which
belongs to no package. A catalog bump therefore reads as "no package changed",
while pnpm rewrites `catalog:` to the concrete range at pack time — so every
consumer's published manifest moves with no version bump behind it, and the new
range sits unreleased until some unrelated PR happens to bump the package.

The gate diffs the `catalog:` / `catalogs:` blocks between a base ref and HEAD,
maps each changed entry to the published packages declaring it in a runtime
dependency field, and fails when no changeset covers them. `devDependencies`,
bundled dependencies, private packages, and anything in the changesets config's
`ignore` are excluded, and an empty changeset does not count as coverage.

The command runs `changeset status` first, so one invocation covers both the
stock "a changeset exists" requirement and the catalog gate against a single
base ref instead of specifying it twice. `changeset-check.yml` is now one step,
behind the same `gtb-from-source` input `cd.yml` uses. The
`pnpm-resolve-pinned` + `pnpm dlx` indirection is gone: it existed only to
avoid an install the gtb bin needs anyway, and it already required
`@changesets/cli` to be a root devDependency to resolve from the lockfile.

`PackageCapabilities` gains a `catalogDependencies` field.
35 changes: 28 additions & 7 deletions .github/workflows/changeset-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,39 @@ jobs:

- uses: gtbuchanan/tooling/.github/actions/mise-setup@main

- id: changesets
uses: gtbuchanan/tooling/.github/actions/pnpm-resolve-pinned@main
with:
package: '@changesets/cli'
# `gtb changeset check` shells out to the installed `changeset` bin, and
# gtb needs node_modules either way — for its own dependencies when
# `gtb-from-source` runs it from workspace source, or for the bin itself
# when it doesn't.
- uses: gtbuchanan/tooling/.github/actions/pnpm-tasks@main
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- name: Verify changeset exists
run: pnpm dlx @changesets/cli@${{ steps.changesets.outputs.version }} status --since=origin/main
# Runs `changeset status` for the stock "a changeset exists" requirement,
# then the catalog gate for what it structurally cannot see: a pnpm
# catalog bump edits only the workspace root, so it reads as "no package
# changed" even though pnpm rewrites `catalog:` to the concrete range at
# pack time and every consumer's published manifest moves. One command so
# both resolve the same base ref.
- name: Verify changesets cover this PR
run: >-
${{ inputs.gtb-from-source && 'pnpm run gtb' || 'pnpm exec gtb' }}
changeset check --since=origin/main

name: Changeset

# The catalog gate runs through `gtb`, so the caller must depend on
# `@gtbuchanan/cli` (the same requirement `cd.yml` already carries).
on:
workflow_call: {}
workflow_call:
inputs:
gtb-from-source:
default: false
description: >-
Run gtb from the workspace source (`pnpm run gtb`) instead of the
installed bin (`pnpm exec gtb`). Set true only by the repo that
vendors `@gtbuchanan/cli` as a workspace package (tooling itself),
whose bin is not built in this job. Consumers leave it false and
run their installed, prebuilt bin.
type: boolean

permissions:
contents: read
4 changes: 4 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ jobs:
changeset:
name: Changeset
uses: ./.github/workflows/changeset-check.yml
# This repo vendors @gtbuchanan/cli as a workspace package, so the
# catalog gate runs gtb from source rather than an installed bin.
with:
gtb-from-source: true

ci:
name: CI
Expand Down
15 changes: 12 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ mise.toml — Pin dev-tool versions for local + CI; postinstall hoo
renovate.json — Repo-local Renovate config (extends the shared preset)
workflows/
cd.yml — Reusable: gtb version (changesets + manifest sync) then gtb publish (npm via OIDC + non-npm channels)
changeset-check.yml — Reusable: verify a changeset exists
changeset-check.yml — Reusable: gtb changeset check (changeset exists + catalog changes reaching published packages are released)
ci.yml — Reusable: build + slow + e2e + coverage
dependency-review.yml — Reusable: scan dep changes (vulns + licenses)
pr.yml — Pipeline (PR): ci + changeset + deps + pre-commit
Expand Down Expand Up @@ -367,8 +367,17 @@ through `package.json` scripts backed by `gtb` leaf commands.
`true` in `release.yml` to run gtb from source (`pnpm run gtb`), whose
bin these jobs don't build. This mirrors the `gtbPrefix` repo-shape
resolution used for generated scripts and mise tasks.
- **`changeset-check.yml`** — Verifies a changeset exists on every PR.
Use `pnpm changeset --empty` for PRs that don't need a version bump.
- **`changeset-check.yml`** — Runs `gtb changeset check`, which gates
releasability twice in one command: `changeset status` for the stock "a
changeset exists" requirement (use `pnpm changeset --empty` for PRs that
don't need a version bump), then a catalog gate for what `status`
structurally cannot see, since a catalog bump edits only the workspace root.
One command so both resolve the same base ref; see the `gtb-build-pipeline`
skill. The job installs — gtb needs `node_modules` either way, and it shells
out to the installed `changeset` bin. The caller must depend on
`@gtbuchanan/cli`; the `gtb-from-source` input (default `false`) selects
workspace-source execution (`pnpm run gtb`) over the installed bin
(`pnpm exec gtb`), exactly as in `cd.yml`.
- **`dependency-review.yml`** — Two PR gates on newly-changed deps.
`Dependency Review` runs `actions/dependency-review-action` (fails on
advisories at `fail-on-severity`, default `moderate`, and on
Expand Down
19 changes: 18 additions & 1 deletion packages/cli/skills/gtb-build-pipeline/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: gtb-build-pipeline
description: Build pipeline guidance for projects using @gtbuchanan/cli. Covers the Turborepo task graph, gtb sync and verify (including scoped runs), the gtb hk pre-commit runner, the gtb turbo wrapper (with the Android/Termux escape hatch), consumer script customization, and test-bucket strategy. Trigger keywords - @gtbuchanan/cli, @gtbuchanan/pnpm-termux-shim, turbo.json, gtb sync, gtb sync mise, gtb verify, gtb verify mise, gtb turbo, gtb task, gtb hk, hk:all, hk:base, mise.tasks.toml, compile:ts, pack:npm, deploy:skills, task graph, transit.
description: Build pipeline guidance for projects using @gtbuchanan/cli. Covers the Turborepo task graph, gtb sync and verify (including scoped runs), the gtb hk pre-commit runner, the gtb changeset catalog gate, the gtb turbo wrapper (with the Android/Termux escape hatch), consumer script customization, and test-bucket strategy. Trigger keywords - @gtbuchanan/cli, @gtbuchanan/pnpm-termux-shim, turbo.json, gtb sync, gtb sync mise, gtb verify, gtb verify mise, gtb turbo, gtb task, gtb hk, gtb changeset check, hk:all, hk:base, mise.tasks.toml, pnpm catalog, compile:ts, pack:npm, deploy:skills, task graph, transit.
---

# @gtbuchanan/cli build pipeline
Expand Down Expand Up @@ -201,6 +201,23 @@ An e2e suite won't catch this on your behalf. A harness that installs each works

Invoked via mise (`mise run hk:base`) so hk and its tools resolve from mise. The mise task resolves `gtb` itself per repo shape — see the `mise.tasks.toml` notes above.

## Changeset gate (`gtb changeset check`)

`gtb changeset check` is the PR releasability gate, and runs two checks against one base ref:

1. `changeset status`, delegated to changesets, which fails when a versionable package changed and no changeset exists at all. It runs first because its failure subsumes the second — with no changesets present, every catalog finding would be uncovered too.
1. The **catalog gate**, which fails when a pnpm catalog change reaches a published package with no changeset releasing it. It exists because `changeset status` maps changed **files** to packages, and a catalog moves every dependency range to `pnpm-workspace.yaml` at the workspace root — which belongs to no package. The bump therefore reads as "no package changed", while pnpm rewrites `catalog:` to the concrete range at pack time, so every consumer's published manifest moves with no version bump behind it.

- `gtb changeset check [--since <ref>] [--ignore <pkg>]` — the base ref defaults to `origin/main`. The catalog half diffs the `catalog:` / `catalogs:` blocks between that ref and HEAD, maps each changed entry to the published packages declaring it, and exits non-zero for any that no changeset covers.

Both halves share one command precisely so they cannot drift onto different base refs.

Both revisions are parsed and compared as maps, so reordering or reformatting `pnpm-workspace.yaml` reports nothing. Only runtime fields count (`dependencies`, `peerDependencies`, `optionalDependencies`) — publishing strips `devDependencies`, so a test-only bump never fires — and bundled dependencies are excluded, matching how `workspaceDependencies` is collected. Private packages and anything in the changesets config's `ignore` are skipped. An empty changeset is **not** coverage: it is the documented way to say "no release", which is the claim the gate exists to challenge. Removed catalog entries are skipped, since orphaning one requires a `package.json` edit that `changeset status` already sees.

The gate is quiet in practice because of Renovate's `rangeStrategy`: under npm's default an in-range bump touches only `pnpm-lock.yaml`, and the catalog is edited only when the new version falls outside the declared range — so a catalog edit is already a range-boundary crossing, exactly the consumer-visible set. Switching to `rangeStrategy: bump` would make every patch update rewrite the catalog and turn the gate into noise.

A repo with no catalog no-ops.

## Android-Termux setup

Two issues are caused by Termux's Node reporting `process.platform === 'android'`; a third (memory pressure) is unrelated and applies to any low-memory host. Native Android support upstream was declined in [vercel/turborepo#5616](https://github.com/vercel/turborepo/issues/5616), so `gtb turbo` ships the workaround instead.
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineCommand } from 'citty';
import { changeset } from './root/changeset.ts';
import { hk } from './root/hk.ts';
import { rootNames } from './root/names.ts';
import { prepare } from './root/prepare.ts';
Expand All @@ -19,6 +20,7 @@ export const main = defineCommand({
name: 'gtb',
},
subCommands: {
[rootNames.changeset]: changeset,
[rootNames.hk]: hk,
[rootNames.prepare]: prepare,
[rootNames.publish]: publish,
Expand Down
Loading
Loading