ci: enforce changesets for affected packages on PRs#49
Conversation
|
| Filename | Overview |
|---|---|
| .github/scripts/check-changesets.mjs | New CI script that detects affected publishable packages and verifies changeset coverage; uses execFileSync throughout (no shell injection), excludes test/config files and spec-style tests correctly; edge case: entire package removal is not detected because pkgNameByDir is built from HEAD where the directory is already gone. |
| .github/workflows/changeset-check.yml | New workflow triggering on PRs to main; correctly skips release PRs, checks out the PR head SHA (not the merge commit), uses fetch-depth: 0 for full history, and passes base/head SHAs via env vars. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[PR opened / updated] --> B{head_ref starts with\nchangeset-release/?}
B -- yes --> C[Skip job]
B -- no --> D[Checkout PR head SHA\nfetch-depth: 0]
D --> E[git diff baseSha...headSha\n--diff-filter=ACMRTD]
E --> F[Build pkgNameByDir\nfrom packages/*/package.json at HEAD]
F --> G[For each changed file:\npackageForFile filter]
G --> H{Release-worthy file\nin a publishable package?}
H -- no: test/config/CHANGELOG/private --> I[Skip file]
H -- yes --> J[Add to affected set]
J --> K[git diff --diff-filter=AM\n.changeset/*.md]
K --> L[Parse frontmatter\nfrom each changeset file]
L --> M[Build covered set\nof package names]
M --> N{affected - covered = missing?}
N -- empty --> O[exit 0 ✓]
N -- non-empty --> P[Print missing packages\nexit 1 ✗]
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 1
.github/scripts/check-changesets.mjs:38-46
**Entire-package deletion silently bypasses the check**
`pkgNameByDir` is built by scanning the filesystem at checkout HEAD, so when a PR removes an entire package directory, that directory no longer exists on disk. Any deleted files from that package pass through `packageForFile` and return `null` (line 54 — package not found in the map), so the package never enters `affected`. A PR that fully removes a publishable package would pass the check without a changeset.
The fix is to also diff `packages/*/package.json` at the base ref to find packages that existed then but are absent now:
```js
// Also capture packages deleted entirely in this PR.
const deletedPkgFiles = git("diff", "--name-only", "--diff-filter=D", diffRange, "--", "packages/*/package.json")
.split("\n")
.filter(Boolean);
for (const f of deletedPkgFiles) {
const m = f.match(/^packages\/([^/]+)\/package\.json$/);
if (!m) continue;
try {
const pj = JSON.parse(git("show", `${baseSha}:${f}`));
if (!pj.private && pj.name) affected.add(pj.name);
} catch { /* file gone at both refs — skip */ }
}
```
Reviews (3): Last reviewed commit: "ci: enforce changesets for affected pack..." | Re-trigger Greptile
4d570d1 to
5b3680e
Compare
Adds a Changeset check workflow that runs on every PR and fails when a publishable package was modified without a corresponding new (or amended) .changeset/*.md entry. Test files, build/test config, CHANGELOG.md, and private packages are excluded. The auto-generated changeset-release/* PR is skipped via the workflow if condition. The check is implemented as a small Node script (no extra deps) that reads added/modified changeset files via git show, so it works regardless of whether checkout uses the merge commit or the head ref. address review: avoid shell, exclude .spec files, drop dead fallback - Switch all git invocations from execSync to execFileSync so paths from git diff output cannot be interpreted as shell metacharacters. Verified with a malicious .changeset/$(...).md filename: old version executed the embedded command, new version parses the file safely. - Exclude .spec.[jt]sx? in addition to .test.[jt]sx? to cover the other common test naming convention. - Remove the shallow-fetch fallback: the workflow always uses fetch-depth: 0 so both SHAs are reachable; the fallback was dead code.
5b3680e to
3abe493
Compare
|
[AGENT] Addressed greptile's review in 3abe493: P1 shell injection — fixed. All git calls now go through
Redundant shallow-fetch fallback — removed. The workflow uses |
Why
Several recent PRs (e.g. #41) modified publishable packages without a corresponding changeset, which means those fixes never get released. Catching this in code review is unreliable; let's enforce it in CI.
What this does
Adds a
Changeset checkworkflow that runs on every PR tomainand fails if any affected publishable package is missing a changeset entry.Affected = a release-worthy file under
packages/<pkg>/was added/modified/renamed/deleted in the PR diff. Excluded:**/test/**,**/__tests__/**,**/*.test.{ts,tsx,js,jsx}tsup.config.*,vitest.config.*,tsconfig*,biome*CHANGELOG.md(regenerated by changesets)"private": trueor nonameCovered = a
.changeset/*.mdfile (added or modified in this PR, excludingREADME.md) has a frontmatter line naming the package at any bump level. Modifications are accepted so a PR amending an earlier changeset on the same branch still passes.The auto-generated
changeset-release/*PR opened by the changesets action is skipped via the workflowif:.The workflow checks out the PR head SHA explicitly (not the synthetic merge commit) so
HEADandpull_request.head.shaagree, and usesfetch-depth: 0so the merge-base withbase.shais reachable. The script reads changeset content withgit show ${HEAD_SHA}:<path>so it's robust to checkout strategy.Verification
Replayed the script against historical PRs:
Plus a synthetic two-package scenario verifying multi-package detection and the amended-changeset path.
Notes
yarn installneeded in the job, so it's fast.