Skip to content

Commit 2a1bcab

Browse files
hotlongclaude
andauthored
ci(changeset): PR 层闸门禁止新增空 frontmatter changeset,并把 skills/** 的正解写进处方 (#5471, #5947) (#6059)
新增 scripts/check-empty-changeset.mjs,由 pr-automation.yml 的 Check Changeset 作业调用:PR 新引入的空 frontmatter changeset 直接红并点名文件,报错指向 `skip-changeset` 标签(路线 2)。豁免按 diff 计算 —— 只判本 PR 引入的声明, base 上的 182 份存量因此天然豁免,无需任何会腐烂的清单。 同时把处方文本改口收口(#5947):路线 3 标记为 CLOSED,路线 2 显式列入 `skills/**` 并写明「没有包可署名 ⇒ 标签,不是空文件」,让 git log 考古者与 处方读者拿到同一个答案。 不碰发布机器、changesets 配置与 Release 行为;#5620 的 allow-major 步骤逐字未动。 Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3 Co-authored-by: Claude <noreply@anthropic.com>
1 parent be59695 commit 2a1bcab

3 files changed

Lines changed: 579 additions & 21 deletions

File tree

.github/workflows/pr-automation.yml

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -251,15 +251,23 @@ jobs:
251251
# Diffing against BASE_SHA ignores that residue and sees only what the
252252
# PR itself introduced.
253253
#
254-
# An empty-frontmatter changeset still COUNTS here, and that is
255-
# deliberate and unchanged — it remains a legal "this PR releases
256-
# nothing" declaration. What #5292 corrected is the PRESCRIPTION, not
257-
# the count: this comment used to call it "on par with the
258-
# skip-changeset label", and the failure message below used to offer
259-
# the two as equals. They are not equal downstream. The label is a
260-
# gate-level exemption that produces no input for changesets/action;
261-
# an empty changeset is a real input to it. See the message for the
262-
# consequence.
254+
# An empty-frontmatter changeset still COUNTS here — this step counts
255+
# files, and that is deliberately unchanged. What has changed is that
256+
# counting is no longer the last word: the step BELOW rejects an empty
257+
# changeset this PR newly introduces (#5471), so satisfying this count
258+
# with an empty file now trades one red for another.
259+
#
260+
# The history is worth keeping straight. #5292 corrected the
261+
# PRESCRIPTION, not the count: this comment used to call an empty
262+
# changeset "on par with the skip-changeset label" and the message
263+
# below used to offer the two as equals. They are not equal
264+
# downstream — the label is a gate-level exemption that produces no
265+
# input for changesets/action, an empty changeset is a real input to
266+
# it. #5471 then measured that the prose alone did not hold (empty
267+
# files kept accruing after PR #5467 merged) and ruled the route shut
268+
# for new files. Splitting it across two steps is what keeps THIS
269+
# step's failure mode ("no changeset at all") distinct from that one's
270+
# ("the changeset you added declares nothing").
263271
ADDED=$(git diff --name-only --diff-filter=A "$BASE_SHA" HEAD -- '.changeset/*.md' \
264272
| grep -v '/README\.md$' | wc -l | tr -d '[:space:]')
265273
if [ "$ADDED" -eq 0 ]; then
@@ -271,37 +279,97 @@ jobs:
271279
# strips that much from every line, so `MSG` lands in column 0 of
272280
# the generated script. Do not re-indent it.
273281
cat <<'MSG'
274-
This PR adds no changeset. There are three ways forward, and they are NOT
275-
equivalent. Pick by what the PR actually releases:
282+
This PR adds no changeset. There are TWO ways forward -- route 3 used to be
283+
a third and is now closed, see below. Pick by what the PR actually releases:
276284
277285
1. It releases something
278286
-> run 'pnpm changeset' and name the packages it releases.
279287
280-
2. It releases nothing (.github/, .claude/, docs/, content/, examples/,
281-
tests-only, and the like)
288+
2. It releases nothing (.github/, .claude/, skills/, docs/, content/,
289+
examples/, tests-only, and the like)
282290
-> apply the 'skip-changeset' label. <<< PREFERRED
283291
The label is a gate-level exemption. It produces NO input for
284292
changesets/action, so it cannot affect a release.
285293
286-
3. An empty-frontmatter changeset also satisfies this gate and stays
287-
legal -- but it is a LAST RESORT, not the quick way past a red check.
288-
Unlike the label it is a REAL INPUT to changesets/action: when every
289-
pending changeset is empty, the action takes its
294+
'skills/**' is on that list, and it is spelled out because the git
295+
log says otherwise (#5947). Changes to PUBLISHED skills have
296+
repeatedly shipped with an empty changeset instead -- #4607, #5130,
297+
#5451 / PR #5799 -- on the reasoning "skills/ ships with no npm
298+
package, so there is no package to name". That premise is true:
299+
skills/ is not a workspace member and no package's 'files' field
300+
includes it. The conclusion does not follow. Naming no package is
301+
precisely what the LABEL is for; an empty changeset names no package
302+
EITHER, and pays #4898 for the privilege. Take the label.
303+
304+
3. (CLOSED) An empty-frontmatter changeset. Still present in the
305+
repository's history and still counted by this step, but the step
306+
below now REJECTS any that a PR newly adds (#5471). It was never worth
307+
taking: it names no package, so its body reaches no CHANGELOG, and it
308+
buys nothing the label does not. What it uniquely buys is risk --
309+
unlike the label it is a REAL INPUT to changesets/action, and when
310+
every pending changeset is empty the action takes its
290311
"hasChangesets && !hasNonEmptyChangesets" branch, prints
291312
"All changesets are empty; not creating PR", and returns in 0 seconds
292313
-- no version PR, no publish, and the Release run still goes GREEN.
293-
That is #4898, which silently stalled 17.0.0-rc.2. It also buys you
294-
nothing the label does not: an empty changeset names no package, so
295-
its body reaches no CHANGELOG.
314+
That is #4898, which silently stalled 17.0.0-rc.2. The empty
315+
changesets already on main are exempt and stay where they are; only
316+
newly introduced ones are rejected.
296317
297318
If you are unsure, take route 2. A wrong 'skip-changeset' label is caught by
298319
review; a wrong empty changeset is caught by nobody.
299320
MSG
300-
echo "::error::This PR adds no changeset. If it releases nothing, apply the 'skip-changeset' label (preferred); otherwise run 'pnpm changeset' and name the packages. An empty-frontmatter changeset also passes this gate, but it is NOT equivalent to the label -- it is a real input to changesets/action, and an all-empty set stalls the release silently and greenly (#4898). Full comparison in this step's log."
321+
echo "::error::This PR adds no changeset. If it releases nothing (including any 'skills/**' change -- see #5947), apply the 'skip-changeset' label; otherwise run 'pnpm changeset' and name the packages. An empty-frontmatter changeset is NOT a third option any more: the step below rejects newly added ones (#5471), because it is a real input to changesets/action and an all-empty set stalls the release silently and greenly (#4898). Full comparison in this step's log."
301322
exit 1
302323
fi
303324
echo "This PR adds $ADDED changeset(s)."
304325
326+
# #5471: an empty-frontmatter changeset is rejected when this PR is the one
327+
# introducing it. Ruled 2026-08-06 after the #5292 / PR #5467 prose route
328+
# failed to hold -- empty files kept accruing at roughly ten a day while
329+
# the workflow text called them a LAST RESORT, and the `skills/**`
330+
# precedent chain (#4607 / #5130 / #5451 -> PR #5799) kept copying the
331+
# downgraded route out of `git log`, where the prescription is invisible.
332+
#
333+
# A SEPARATE step, not more logic inside the step above, for two reasons.
334+
# The two failures are different facts and deserve different messages
335+
# ("no changeset at all" vs "the changeset you added declares nothing"),
336+
# and a script can be self-tested where an inline shell block cannot.
337+
#
338+
# Scope, and the two things this deliberately does NOT do:
339+
# - The EXISTING empty changesets (182 at efedd289f) are exempt. The gate
340+
# judges the PR's diff, never the directory, so the exemption needs no
341+
# roster -- a 182-name list would be a high-water mark that rots on the
342+
# first merge. Nothing here cleans them up; that is deferred to the
343+
# next `changeset pre exit`, by the same ruling.
344+
# - Nothing about the release machinery, `.changeset/config.json` or
345+
# changesets/action's behaviour changes. This is a PR-layer gate and
346+
# reverts in one commit.
347+
#
348+
# The `--self-test` is chained AHEAD of the real run on purpose (the repo
349+
# convention for `check-*.mjs` gates): a checker whose own fixtures are
350+
# never executed is a phantom check, and this one's fixtures are the only
351+
# place the red and green directions are pinned. It builds real temp git
352+
# repositories and costs well under a second.
353+
#
354+
# Label handling: this step carries the same `steps.labels.outputs.skip`
355+
# guard as every step above it, so it honours the LIVE label re-read
356+
# (#5580 / #5625) and a rerun after labelling converges. That leaves one
357+
# cell open and it is recorded rather than implied: a PR carrying BOTH the
358+
# `skip-changeset` label AND a new empty changeset is not caught, because
359+
# the whole job is exempt. Closing it would mean running this step outside
360+
# the job's exemption, where it would fire only when the label arrived
361+
# after the event fired -- a gate that reds one PR and greens an identical
362+
# one. A consistent exemption beats a nondeterministic gate, and the case
363+
# is empty of motive anyway: an author who already has the label gains
364+
# nothing by adding the file.
365+
- name: Reject an empty-frontmatter changeset added by this PR
366+
if: steps.labels.outputs.skip != 'true'
367+
env:
368+
BASE_SHA: ${{ github.event.pull_request.base.sha }}
369+
run: |
370+
node scripts/check-empty-changeset.mjs --self-test
371+
node scripts/check-empty-changeset.mjs --base "$BASE_SHA"
372+
305373
- name: Guard against accidental major bumps (launch window)
306374
# Every publishable package is in one Changesets "fixed" (lockstep) group,
307375
# so a single `major` bump promotes the ENTIRE monorepo to a new major

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"check:objectui-changeset": "node scripts/objectui-changeset-digest.mjs --self-test && node scripts/objectui-range.mjs --self-test",
5555
"check:objectui-pin-fresh": "node scripts/check-objectui-pin-fresh.mjs --self-test && node scripts/check-objectui-pin-fresh.mjs",
5656
"check:prerelease-pins": "node scripts/check-prerelease-pin-watch.mjs --self-test && node scripts/check-prerelease-pin-watch.mjs",
57+
"check:empty-changeset": "node scripts/check-empty-changeset.mjs --self-test && node scripts/check-empty-changeset.mjs",
5758
"check:release-notes": "node scripts/check-release-notes.mjs",
5859
"check:release-body": "node scripts/release-github-releases.mjs --self-test",
5960
"check:node-version": "node scripts/check-node-version.mjs",

0 commit comments

Comments
 (0)