From e4f37bdfe4069771996c5e26d1559fc96c4c7875 Mon Sep 17 00:00:00 2001 From: Daryl Collins Date: Mon, 20 Jul 2026 15:39:00 +0100 Subject: [PATCH] feat: add apps-package-check reusable workflow (publint gate) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New PR gate on the publish-readiness axis alongside apps-changeset-check and apps-codegen-drift-check: builds the workspace, then runs each publishable package's check:package script (publint) to validate the packed artifact — exports/main/types/bin resolution, ESM/CJS format, types condition order, and files-set completeness — none of which lint/typecheck/test can see. check:package is a per-package named-script contract invoked with --if-present, so the job is a fast green no-op for repos with no publishable package and the script can grow more artifact checks (e.g. attw) without renaming. Wired into the canonical apps-ci-trigger.yml as a third job and documented in the README, mirroring apps-codegen-drift-check. Claude-Session: https://claude.ai/code/session_01BNdm93FPjkK8G7NmuTEbXD --- .github/workflows/apps-ci-trigger.yml | 7 ++++ .github/workflows/apps-package-check.yml | 43 ++++++++++++++++++++++++ README.md | 25 ++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 .github/workflows/apps-package-check.yml diff --git a/.github/workflows/apps-ci-trigger.yml b/.github/workflows/apps-ci-trigger.yml index 66d1f6e..63dc830 100644 --- a/.github/workflows/apps-ci-trigger.yml +++ b/.github/workflows/apps-ci-trigger.yml @@ -23,3 +23,10 @@ jobs: # script anywhere in the workspace, so this job is safe to keep in the # canonical trigger even before any consumer has codegen. uses: ./.github/workflows/apps-codegen-drift-check.yml + + package-check: + # Publish-artifact hygiene (publint via each publishable package's + # `check:package` script) — same discrete-status rationale as + # drift-check, and the same no-op behaviour for repos with no + # publishable package. + uses: ./.github/workflows/apps-package-check.yml diff --git a/.github/workflows/apps-package-check.yml b/.github/workflows/apps-package-check.yml new file mode 100644 index 0000000..2b4e595 --- /dev/null +++ b/.github/workflows/apps-package-check.yml @@ -0,0 +1,43 @@ +name: Package check + +on: + workflow_call: + +permissions: + contents: read + +jobs: + package-check: + name: Package check + # changeset-release/* branches contain only version bumps and changelog + # entries; they can't change a package's file layout or exports. + if: "!startsWith(github.head_ref, 'changeset-release/')" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version-file: .nvmrc + check-latest: true + cache: pnpm + + - run: pnpm install --frozen-lockfile + + # `check:package` lints the *packed artifact* (publint packs the package + # and validates that `exports`/`main`/`types`/`bin` resolve to files that + # actually ship), so the compiled output must exist first. The root + # `build` script — where one exists — runs workspace packages in + # topological order; `--if-present` keeps this a no-op for repos with + # nothing to compile. + - run: pnpm run --if-present build + + # Same walk as codegen-drift-check: `check:package` is defined per + # publishable package (or at the root of a single-package repo), never + # only at the workspace root, so a bare root `pnpm run` would miss it. + # No-op for repos that ship no `check:package` script anywhere — a fast + # green check, safe to wire into the canonical CI trigger before every + # consumer has a publishable package. + - run: pnpm -r --include-workspace-root run --if-present check:package diff --git a/README.md b/README.md index 4289b68..9a851fe 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ in its own separate GitHub Actions job with its own runner. | `apps-ci.yml` | Lint, typecheck, and test on PRs | _(none)_ | | `apps-changeset-check.yml` | PR gate: requires a changeset; posts/deletes instructions comment | _(none)_ | | `apps-codegen-drift-check.yml` | PR gate: runs `pnpm -r --include-workspace-root run --if-present codegen-drift-check` (no-op when no package defines the script) | _(none)_ | +| `apps-package-check.yml` | PR gate: builds, then runs `pnpm -r --include-workspace-root run --if-present check:package` — publish-artifact hygiene via [publint](https://publint.dev) (no-op when no package defines the script) | _(none)_ | | `apps-npm-release.yml` | Changesets release pipeline (version bumps, npm publish, git tags) and on-demand snapshot publish via the `snapshot_tag` input | `CHANGESET_RELEASE_BOT_APP_ID`, `CHANGESET_RELEASE_BOT_APP_PRIVATE_KEY` | | `apps-docker-release.yml` | GCP image push on version tag (delegates to `gcp_pipeline_release_image.yaml`) | `build_params_gh_secret_keys` | | `apps-pr-labeler.yml` | Labels `.github/`-only PRs as `do-not-notify`; removes label when non-`.github/` changes are added | _(none)_ | @@ -107,6 +108,30 @@ The canonical `apps-ci-trigger.yml` already wires this in as a second job alongside `lint-typecheck-test` — adopting team-wide is a single trigger file in each consumer repo, not two. +### `apps-package-check.yml` + +A discrete PR gate for publish-artifact hygiene — the class of bug where a +package builds and tests green but ships broken: an `exports`/`main`/`types`/ +`bin` entry pointing at a file that isn't packed, wrong ESM/CJS format, +mis-ordered `types` conditions, or a `files` set that silently drops +`CHANGELOG.md`/`MIGRATION.md`. Runs `pnpm run --if-present build` (so the +compiled output the pack would ship actually exists), then +`pnpm -r --include-workspace-root run --if-present check:package`. + +The `check:package` convention: each **publishable** package defines the +script and it validates the packed artifact — currently +[publint](https://publint.dev) (`"check:package": "publint"` with `publint` +as a devDependency); more artifact checks (e.g. `attw`) can be appended +later without renaming the script. It is deliberately _not_ part of +`pnpm run lint`: publint packs the package, which is publish-readiness +work, not the fast pre-commit loop. Non-publishable packages simply omit +the script — `--if-present` keeps the job a fast green no-op, so it is +safe in the canonical CI trigger before any consumer has a publishable +package. + +See `apps-team-ts-template` (`packages/example-db`) for the canonical +consumer wiring. + ### `actions/check-dockerfile` Resolves a changeset tag to a workspace package directory and reports whether