-
Notifications
You must be signed in to change notification settings - Fork 5
1117 lines (1061 loc) · 65.1 KB
/
Copy pathchangeset-release.yml
File metadata and controls
1117 lines (1061 loc) · 65.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: Changeset Release
# ══════════════════════════════════════════════════════════════════════════════
# TWO LANES, ONE FILE: THE VERSION PR REFRESHES ON A CLOCK, npm PUBLISHES ON A
# MERGE. (objectstack#10850)
# ══════════════════════════════════════════════════════════════════════════════
#
# Maintainer ruling, 2026-08-21 (verbatim, do not translate):
#
# 「我每周才发一次版本,changeset 每个pr 都跑有必要吗」
# 「立卡吧,version 工作流改成发版前手动触发」
# 「包括 objectui 仓库」
#
# The sister half landed first in `../objectstack` as its `release.yml`
# `version-pr` job (objectstack#11233 / PR #11238, maintainer 2026-08-23): a
# 6-hourly `schedule` plus an on-demand dispatch input, which is the later and
# more specific spelling of "手动触发" and the one mirrored here. Per-PR changeset
# VALIDATION is explicitly untouched by the ruling — `changeset-guard.yml` and
# `changeset-presence.yml` still run on every pull request.
#
# WHY THE REFRESH LEFT THE PUSH TRIGGER
# -------------------------------------
# With pending changesets this file's changesets step recomputed and FORCE-PUSHED
# the standing "chore: release packages" PR (objectui#5400) on every landing, and
# `main` takes ~18 merges a working day (the same cadence the concurrency note
# below measures). The PR therefore never held still long enough for its own
# branch CI to finish. Releases are weekly, so every one of those refreshes was a
# CI run spent on bookkeeping nobody reads until release day. Between refreshes
# `changeset-release/main` is now a static branch whose CI can converge.
#
# The staleness window is the whole cost, and it is bounded on demand: dispatch
# with `refresh_version_pr` when you want the PR current NOW (immediately before
# a release, say). The bookkeeping is not time-critical — the changesets are
# already committed on `main`; objectui#5400 is only their rendering.
#
# WHY THE SPLIT NEEDS A SEPARATE `lane` JOB, AND NOT JUST AN `if:` ON THE STEP
# ---------------------------------------------------------------------------
# ⚠️ Unlike objectstack's `release.yml` — which has a version job and a publish
# job and could simply give each one its own trigger — this repository publishes
# from THE SAME changesets step that refreshes the PR. Which of the two it does
# is not ours to choose per invocation: the action decides from repository state.
# Its v1 source (`changesets/action`, `src/index.ts`) dispatches on exactly two
# facts, `hasChangesets` and whether a `publish:` input was given:
#
# case !hasChangesets && !hasPublishScript: -> nothing
# case !hasChangesets && hasPublishScript: -> runPublish (npm + tags)
# case hasChangesets && !hasNonEmptyChangesets: -> nothing
# case hasChangesets: -> runVersion (force-push PR)
#
# Two consequences drive everything below, and they are NOT symmetric:
#
# - Omitting `publish:` makes publishing UNREACHABLE BY CONSTRUCTION. There is
# no state in which the action publishes without that input.
# - Omitting `version:` does NOT make the refresh unreachable. `runVersion`
# falls back to plain `changeset version` when the input is absent, so a step
# invoked on a push that still carries pending changesets force-pushes
# objectui#5400 no matter how it is configured.
#
# So the refresh lane is closed by construction, and the publish lane can only be
# closed by NOT INVOKING THE ACTION on a push that carries pending changesets.
# That question — "does `main` carry pending changesets?" — has to be answered
# before the expensive steps, or the saving the ruling asked for is not made: the
# job would still install and build on all ~18 daily landings just to decide to
# do nothing. Hence one cheap job that answers it from a sparse checkout, and one
# job-level `if:` that states the policy. A second workflow FILE was ruled out
# (no new lanes to maintain, and the publish invariants would be duplicated where
# they can drift apart).
#
# THE LANES
# ---------
# push to main, version NOT on npm -> PUBLISH what this commit declares.
# Normally that is the version-PR merge
# itself. It is also the retry: any
# later push re-attempts a release whose
# own run failed, which is how
# @object-ui/*@17.5.0 eventually shipped
# 6h50m late (see the `pnpm test` note
# further down) — except that it is no
# longer luck, because it no longer
# depends on `.changeset/` happening to
# still be empty (objectui#5442).
# push to main, version already on npm-> NOTHING. This is every ordinary
# landing: a merge that is not a release
# does not move the manifest version, so
# there is nothing to publish and the
# expensive job is skipped.
# schedule (6-hourly) -> REFRESH ONLY. The changesets step is
# or a dispatch with invoked WITHOUT a `publish:` script
# `refresh_version_pr` and WITHOUT npm credentials, so the
# publish branch is unreachable by
# construction rather than by an `if:`
# someone can get wrong.
# dispatch without the input -> NOTHING, loudly. See the input's note.
#
# THE REFRESH LANE VALIDATES WHAT IT IS ABOUT TO RENDER (objectui#5397)
# ---------------------------------------------------------------------
# The release PR runs no CI and cannot be made to — 849 `ci.yml` runs on
# `changeset-release/main`, every recent one `action_required`, created and
# parked with nothing executed — so the release commit is the only commit that
# reaches `main` without passing the merge queue. Before the refresh step
# invokes the action, this job therefore renders the post-version tree itself,
# validates the surfaces the version step can move, and puts the tree back. The
# ruling, the measurements that chose that scope over a 40-minute `pnpm test`,
# and why the restore is load-bearing are at "POST-VERSION VALIDATION" below.
#
# WHY THE PUBLISH LANE IS KEYED ON npm AND NOT ON `.changeset/`
# -------------------------------------------------------------
# It used to be keyed on "does this commit carry pending changesets?", and that
# predicate answers a question about BOOKKEEPING while being read as if it
# answered a question about the REGISTRY. The two come apart exactly where it
# costs a release (objectui#5442, maintainer ruling 2026-08-22, Option B):
#
# The version PR is cut from `main` at T and merged at T+n. `main` takes ~18
# merges a working day, so by T+n it has almost always accumulated changesets
# that the merge does not remove. Those changesets belong to the NEXT version;
# the version this commit declares is finished and unpublished. Keyed on
# changesets, that release is skipped, and the next version PR bumps straight
# past it.
#
# Measured, and still true at the commit this paragraph was written on: of the 90
# versions `packages/core/CHANGELOG.md` declares, 16 never reached npm, and
# `packages/core/package.json` says 17.6.0 while `dist-tags.latest` says 17.5.0.
# `@object-ui/react` and `@object-ui/types` are missing the exact same 16, which
# is the counter-probe that matters: whole RELEASES never published, rather than
# some package failing to.
#
# So the predicate is now "is the version this commit declares already on npm?".
# It is answered in the `lane` job by one read of `registry.npmjs.org`, and it is
# better than its predecessor on both axes at once:
#
# - CORRECTNESS. Pending changesets stop being the publish lane's business.
# What this commit declares gets published, whatever the next version is
# accumulating.
# - COST. It is STRICTLY cheaper than "no pending changesets", not a trade
# against it. An ordinary landing does not move the manifest version, so it
# answers "already on npm" and skips the expensive job — where the old
# predicate ran the job in full on every landing that happened to find
# `.changeset/` empty, only to publish nothing.
#
# The saving objectstack#10850 asked for is therefore kept, and kept for a
# sounder reason: a landing pays for the release job when it has a release to
# make, rather than when a directory happens to be empty.
#
# ⚠️ The predicate cannot reach the action on its own. `changesets/action@v1`
# does not take a "publish" instruction — it picks a branch from repository
# state, and `hasChangesets` is the discriminator (algorithm quoted above). On a
# release-merge commit carrying the next version's changesets it takes
# `runVersion` and publishes nothing. So the publish lane hands it a tree with
# nothing pending; see the step that does it, which explains why that is safe.
#
# ⛔ A refresh lane that could publish would be a NEW capability, not a moved
# one: this repository's release act is the version-PR MERGE, a human action. A
# scheduled tick that reached npm would publish with nobody having merged
# anything. That is why the publish half is denied to it twice over — no
# `publish:` input and no `NPM_TOKEN`/`NODE_AUTH_TOKEN` in its env — and why the
# publish half keeps its trigger exactly as it was. This card changed WHEN the
# version PR refreshes and nothing else: no gate was added, and none removed.
#
# ⚠️ Scheduled runs are queued, not guaranteed on the minute — GitHub delays or
# drops them under load, and disables them entirely after 60 days of repository
# inactivity. Both are acceptable HERE and would not be on a publishing lane: a
# refresh that arrives late leaves objectui#5400 stale, which is visible on the
# PR and fixable with one dispatch. It is a second reason the publish lane must
# never be reachable from `schedule`.
on:
# The PUBLISH lane (and only it — see the header). Kept exactly as it was: the
# merge of the version PR is the release act in this repository.
push:
branches:
- main
# The REFRESH lane. GitHub runs `schedule` on the DEFAULT BRANCH exclusively,
# which is the only ref this workflow may regenerate the version PR from, so
# the trigger cannot reach a ref the lane is not meant to touch.
schedule:
- cron: '0 */6 * * *'
workflow_dispatch:
inputs:
# The on-demand half of the schedule. Its ONLY effect is to select the
# refresh lane; it can start nothing the 6-hourly tick does not start, so
# it cannot widen anything.
#
# ⚠️ It defaults to FALSE to match objectstack's spelling of the same
# input, which leaves "Run workflow" with default inputs doing NOTHING in
# this repository — objectstack's bare dispatch is its publish REPAIR lane,
# and this repository has no such lane (publishing here is not gated on an
# environment approval, so there is nothing to re-approve). Rather than
# invent one under this card, the `lane` job below prints a `::notice::`
# saying why nothing ran, so a bare dispatch reports instead of vanishing.
refresh_version_pr:
description: >-
Regenerate the "chore: release packages" PR (objectui#5400) now instead
of waiting for the next 6-hourly refresh. Runs the bookkeeping lane
ONLY: nothing is published to npm. Leave unchecked and this workflow
does nothing at all.
required: false
default: false
type: boolean
# One concurrency group per COMMIT — never one shared by every push to `main`.
#
# A shared group does not queue. GitHub keeps at most ONE pending run per group and
# cancels the previous pending run whenever a newer one arrives, so on a branch that
# takes ~18 merges a day a long-running release job discards most of what follows it.
# Measured over runs #3712-#3911 (2026-08-17 .. 2026-08-20, 200 runs): 93 were
# `conclusion: cancelled` with an EMPTY jobs array — not one step ever ran — while the
# 106 that did execute held the group for a median of 33m22s. Median wait for a run
# that survived: 5m16s (objectui#5404, method and figures from objectui#5395).
#
# A push run of this workflow can publish to npm, so a discarded run is a discarded
# publish. `cancel-in-progress: true` is NOT the fix: it keeps one pending slot by
# killing the run that may be mid-`changeset publish`, which is strictly worse than
# any wait.
#
# ⚠️ "A push run" is where that sentence used to say "this workflow", and the
# distinction is new with objectstack#10850: since the refresh lane arrived, `push`
# is no longer the only event here, and `github.sha` is no longer unique per run.
# A `schedule` tick and a `workflow_dispatch` both read the head of `main`, so two
# of them between merges land in the SAME group and the older pending one is
# evicted. That is harmless, and provably so rather than by luck:
#
# - Only refresh runs can be evicted. A push run at `<sha>` is always the FIRST
# run in its group — the sha did not exist before that push — so it is never
# the pending one, and `cancel-in-progress: false` means it is never cancelled
# once started. The publish lane keeps exactly the protection this key was
# added for.
# - An evicted refresh is nothing lost. The refresh regenerates objectui#5400
# from scratch, so the newest run's result is the one that was wanted anyway —
# the same reasoning `../objectstack`'s `version-pr` job records for its own
# concurrency group.
#
# Keyed by `github.sha` the group holds exactly one run, so nothing is ever discarded.
# Serialisation is not given up — it moves into the "Wait for older release runs" step
# below, which queues runs for real and in order, and which can only ever delay a run,
# never drop one.
concurrency:
group: ${{ github.workflow }}-${{ github.sha }}
cancel-in-progress: false
permissions:
# `actions: read` lets the queue step below list this workflow's own runs. Declaring
# any `permissions:` block sets every unlisted scope to `none`, so it has to be named.
actions: read
contents: write
pull-requests: write
id-token: write
jobs:
# ══════════════════════════════════════════════════════════════════════════
# Which lane is this run in? (objectstack#10850)
# ══════════════════════════════════════════════════════════════════════════
# The header explains why this cannot be an `if:` on the changesets step: the
# publish lane is only safe on a commit where `.changeset/` is empty, and that
# fact has to be known BEFORE install and build, or every one of `main`'s ~18
# daily landings still pays for a job that will decide to do nothing.
#
# This job answers the FACT ("does this commit carry pending changesets?") and
# nothing else. The POLICY lives in the release job's `if:` below, in workflow
# expression language where it can be read in one place — a shell script that
# emitted "the lane" would move the split somewhere no reviewer looks.
lane:
name: Decide lane
runs-on: ubuntu-latest
# Narrower than the workflow-level block deliberately: this job reads files
# and writes nothing. Declaring any `permissions:` sets every unlisted scope
# to `none`, which is the point — a new job on a workflow that can publish
# should not inherit the publish credentials' scopes.
permissions:
contents: read
outputs:
manifest_version: ${{ steps.npm.outputs.manifest_version }}
version_on_npm: ${{ steps.npm.outputs.version_on_npm }}
steps:
# Two directories, because the lane now turns on two facts: `.changeset/`
# answers "are changesets pending?" and `packages/core/` carries the fixed
# group's version anchor, which answers "is that version already on npm?".
# Still far short of a full checkout of this repository — ~15s that every
# landing would otherwise pay.
- name: Check out the lane inputs
uses: actions/checkout@v7
with:
sparse-checkout: |
.changeset
packages/core
# This step no longer PREDICTS anything: it reports the lane's inputs and
# says why a bare "Run workflow" did nothing. That is all it does.
#
# It used to also mirror, in bash and before any install, the file scan
# that `changesets/action` performs through `readChangesetState`, and
# publish the result as a `pending_changesets` output gating the release
# job's "Clear pending changesets" step. objectui#6081 DELETED that mirror.
#
# ⛔ Do not reintroduce it. The thing it mirrored has no stable referent,
# which is why no amount of care could keep it honest: `changesets/action`
# is used at a MOVING TAG, its bundled `@changesets/read@^0.6.7` is a
# different major from the `1.0.0` this repository installs, the two
# disagree in OPPOSITE directions (`AGENTS.md` / `CLAUDE.md` / `GEMINI.md`
# count for one and not the other; `.changeset/pre/*.md` the other way),
# and the bundled copy is a minified chunk that nothing here installs — so
# no test could execute it and no lockfile recorded it. A retag of `v1` in
# place changed what the mirror was mirroring and NOTHING in this
# repository failed. On the release lane, where objectui#5442 already
# produced one silent lost release, that is the worst available property.
#
# What replaced it is not a better prediction but the absence of one: the
# clear step is IDEMPOTENT and a measured no-op on the very input where
# this output used to skip it — `Removed 0 pending changeset file(s)`,
# exit 0, byte-identical tree, and a rerun after a real clear also removes
# 0. A step that does nothing when there is nothing to do does not need
# something else to predict whether there is anything to do.
#
# ⛔ Nor should anyone reintroduce it to "save" the clear step a run. It
# costs a directory glob and some `rm -f` on a job whose other steps take
# ~90s, and the price of buying that back is a hand-maintained copy of
# another project's minified file scan.
#
# ⚠️ `.changeset` stays in the sparse-checkout above: the `npm` step below
# reads `.changeset/config.json` for the `fixed` group. It is no longer
# read for changeset FILES.
- name: Report the lane
env:
REFRESH_VERSION_PR: ${{ inputs.refresh_version_pr }}
run: |
set -euo pipefail
{
echo '### Changeset Release lane'
echo
echo "- event: \`${GITHUB_EVENT_NAME}\`"
echo "- commit: \`${GITHUB_SHA}\`"
} >> "$GITHUB_STEP_SUMMARY"
# A bare "Run workflow" reports instead of vanishing — see the input's
# note in the `on:` block above.
if [ "${GITHUB_EVENT_NAME}" = 'workflow_dispatch' ] && [ "${REFRESH_VERSION_PR}" != 'true' ]; then
echo "::notice::Nothing to do. Check 'refresh_version_pr' to regenerate the version PR; publishing to npm happens when that PR is merged, not from a dispatch."
fi
# ══════════════════════════════════════════════════════════════════════
# THE PUBLISH PREDICATE: is the version this commit declares on npm?
# (objectui#5442 — the ruled Option B, argued in this file's header)
# ══════════════════════════════════════════════════════════════════════
# `@object-ui/core` is an ANCHOR, not a special case: every package in the
# `fixed` group of `.changeset/config.json` moves as one version, so any
# member answers for the whole release. That membership is ASSERTED rather
# than assumed — if `@object-ui/core` ever leaves the group it stops
# answering for the release, and the publish lane must fail rather than key
# itself on a package that no longer represents one.
#
# LOUD on every answer that is not a clean yes or no. 200 is present, 404
# is absent, and anything else — 5xx, a proxy, no network — is neither.
# Guessing "absent" publishes on a registry hiccup; guessing "present"
# skips a real release. Neither is worth making silently, which is the same
# reason the detector above refuses to guess a lane.
- name: Is this commit's version already on npm?
id: npm
env:
ANCHOR_PKG: '@object-ui/core'
ANCHOR_MANIFEST: packages/core/package.json
ATTEMPTS: '5'
BACKOFF_SECONDS: '5'
run: |
set -euo pipefail
if ! jq -e --arg p "$ANCHOR_PKG" '[.fixed[]?[]?] | index($p)' \
.changeset/config.json > /dev/null; then
echo "::error::${ANCHOR_PKG} is not in the 'fixed' group of .changeset/config.json, so it no longer answers for the release version. Point this step at a package that is still in the group (objectui#5442)."
exit 1
fi
manifest_version=$(jq -r '.version // empty' "$ANCHOR_MANIFEST")
if [ -z "$manifest_version" ]; then
echo "::error::Could not read a version from ${ANCHOR_MANIFEST} (objectui#5442)."
exit 1
fi
# The registry's per-version endpoint, which answers 200 for a version
# it has published and 404 for one it has never seen. `/` in the scope
# has to be percent-encoded.
encoded=$(printf '%s' "$ANCHOR_PKG" | sed 's|/|%2F|')
url="https://registry.npmjs.org/${encoded}/${manifest_version}"
on_npm=''
for attempt in $(seq 1 "$ATTEMPTS"); do
code=$(curl -sS -o /dev/null -w '%{http_code}' \
-H 'Cache-Control: no-cache' --max-time 20 "$url" 2>/dev/null) || code='000'
case "$code" in
200) on_npm=true; break ;;
404) on_npm=false; break ;;
*) echo "attempt ${attempt}/${ATTEMPTS}: registry answered ${code} for ${url}" ;;
esac
# An explicit `if`, not `[ ... ] && sleep`: bash exempts AND-lists
# from `set -e`, so the short form works, but it works for a reason
# a reader has to already know.
if [ "${attempt}" -lt "${ATTEMPTS}" ]; then sleep "$BACKOFF_SECONDS"; fi
done
if [ -z "$on_npm" ]; then
echo "::error::Could not establish whether ${ANCHOR_PKG}@${manifest_version} is on npm after ${ATTEMPTS} attempts, so the publish lane has no predicate to run on. Refusing to guess (objectui#5442)."
exit 1
fi
echo "manifest_version=${manifest_version}" >> "$GITHUB_OUTPUT"
echo "version_on_npm=${on_npm}" >> "$GITHUB_OUTPUT"
{
echo "- declared version: \`${ANCHOR_PKG}@${manifest_version}\`"
echo "- already on npm: \`${on_npm}\`"
} >> "$GITHUB_STEP_SUMMARY"
if [ "$on_npm" = false ] && [ "${GITHUB_EVENT_NAME}" != 'push' ]; then
# Visible, but not a failure here: this lane cannot publish, and the
# next push to `main` closes the gap. The failure lives on the push
# lane, where it is actionable — see "Verify the release reached npm".
echo "::notice::${ANCHOR_PKG}@${manifest_version} is not on npm. The next push to main will publish it (objectui#5442)."
fi
release:
name: Changeset Release
needs: lane
# The whole lane split, in one place (objectstack#10850). Each half names its
# own event rather than leaning on the complement of the other:
#
# push + version not yet on npm -> PUBLISH what this commit declares.
# push + version already on npm -> skipped; nothing to release.
# schedule -> REFRESH.
# dispatch WITH the input -> the same refresh, on demand.
# dispatch WITHOUT it -> skipped; the `lane` job says why.
#
# ⚠️ The push half reads `version_on_npm`, NOT a count of pending
# changesets. That is the whole of objectui#5442: pending changesets belong
# to the next version and say nothing about whether THIS one shipped. Since
# objectui#6081 nothing counts them at all — `.changeset/` reaches the
# release only through the step that CLEARS it for the action, never through
# a decision about whether there is a release to make.
#
# `inputs.refresh_version_pr` is guarded by its event test rather than read
# bare: the `inputs` context exists only on `workflow_dispatch`, so on a
# `schedule` run it is null — falsy, and therefore the right answer by
# accident. Say which event we are on, so the guard states the lane split
# instead of leaning on a context's emptiness.
if: >-
(github.event_name == 'push' && needs.lane.outputs.version_on_npm == 'false') ||
github.event_name == 'schedule' ||
(github.event_name == 'workflow_dispatch' && inputs.refresh_version_pr)
runs-on: ubuntu-latest
steps:
# The queue the `concurrency:` key above cannot provide. GitHub offers exactly two
# behaviours for a contended group — cancel the running one, or discard the pending
# ones — and neither is acceptable on a workflow that publishes. So the ordering is
# done here: a run waits until every release run older than itself has finished, then
# proceeds. Ordering is by `run_number`, a total order, so the oldest run is always
# free to proceed and this cannot deadlock.
#
# It is deliberately FAIL-OPEN. Every exit path is `exit 0`, and the step additionally
# carries `continue-on-error: true`: if the API cannot be read, or an older run is
# wedged past the cap, this step gives up waiting and lets the release proceed. A run
# that never runs is the defect this queue exists to remove — degrading to today's
# unserialised behaviour is the acceptable failure, blocking a publish is not.
- name: Wait for older release runs to finish
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RUN_NUMBER: ${{ github.run_number }}
# A release job is ~90s of work; this cap is a wedged-run escape hatch, not a
# normal wait.
MAX_WAIT_SECONDS: '900'
POLL_SECONDS: '20'
run: |
set -uo pipefail
# A rename of this file makes the path 404, `gh` exits non-zero, and the step
# fails open below — it does not silently stop serialising while looking green.
runs_path="repos/${GITHUB_REPOSITORY}/actions/workflows/changeset-release.yml/runs?per_page=100"
older_jq='[.workflow_runs[] | select(.status != "completed" and .run_number < '"${RUN_NUMBER}"')]
| .[] | " waiting on run #\(.run_number) (\(.status)) \(.html_url)"'
deadline=$(( $(date +%s) + MAX_WAIT_SECONDS ))
while : ; do
if ! older=$(gh api "${runs_path}" --jq "${older_jq}"); then
echo "::warning::Could not list this workflow's runs, so the release lane is not serialised for this run. Proceeding — this step may delay a release, never block one (objectui#5404)."
exit 0
fi
count=$(printf '%s\n' "${older}" | grep -c 'waiting on run' || true)
if [ "${count}" -eq 0 ]; then
echo "No older release run is still in flight. Proceeding."
exit 0
fi
if [ "$(date +%s)" -ge "${deadline}" ]; then
echo "::warning::${count} older release run(s) still unfinished after ${MAX_WAIT_SECONDS}s. Proceeding without waiting further — a release run that never executes is the defect this queue exists to prevent (objectui#5404)."
exit 0
fi
echo "${count} older release run(s) still in flight:"
printf '%s\n' "${older}"
sleep "${POLL_SECONDS}"
done
- name: Checkout Repo
uses: actions/checkout@v7
with:
fetch-depth: 0
submodules: true
- name: Enable Corepack
run: corepack enable
- name: Verify pnpm version
run: pnpm --version
- name: Setup Node.js
uses: actions/setup-node@v7
with:
node-version: '22.x'
cache: 'pnpm'
registry-url: 'https://registry.npmjs.org'
# ⚠️ MEASURED 2026-08-26 (objectui#6391): this job performs NO local merge,
# so the driver configured below has no occasion to fire HERE. The step is
# kept anyway — see the ⛔ at the bottom of this comment, which is the part
# that matters.
#
# The sweep, so nobody re-derives it a fourth time (objectui#6358 and
# objectui#6369 each re-derived it for a different workflow):
#
# Every `git` in this file, enumerated rather than grepped-for-absence:
# the two `git config` lines below, `git status --porcelain` (x2),
# `git checkout -- .` and `git clean -fdq` in "Restore the pre-version
# tree". No `merge`, `rebase`, `pull`, `cherry-pick`, `am`, `apply` or
# `revert` anywhere in the file — each of those zero-hits was taken with
# a control term that DID hit the same file, because a zero-hit with no
# control is not a reading.
#
# `git checkout -- .` (below, in the restore step) takes tracked paths
# from the INDEX to undo what `pnpm changeset:version` wrote. That is a
# discard, not a merge; no merge driver participates.
#
# ⭐ The load-bearing part is not in this file at all — it is what
# `changesets/action@v1` does, and it has to be read to be known. Read at
# v1.9.0 (`a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d`), `src/git.ts` and the
# executed `dist/` bundle agreeing: the action's COMPLETE git surface is
# `checkout <branch>` / `checkout -b`, `reset --hard <sha>`, `add .`,
# `commit -m`, `push origin HEAD:<branch> --force`, `push origin <tag>`
# and `config user.*`. The version-branch update is `prepareBranch`
# (checkout, then `reset --hard github.context.sha`) followed by a commit
# and a FORCE-PUSH. It never merges, rebases or three-ways anything, so on
# this side too the driver has no occasion to fire. (Its `commitMode:
# github-api` path — not selected here; the default is `git-cli` — builds
# the commit through the GitHub API instead, and the bundle reads no
# `.gitattributes` and implements no merge driver at all.)
#
# ⛔ DO NOT delete this step "because it is dead" without a ruling. It is
# dead only in the CI half. The `.gitattributes` line it depends on
# (`pnpm-lock.yaml merge=pnpm-merge`) is NOT dead: `CONTRIBUTING.md` tells
# contributors to configure this same driver locally and then to
# `git merge upstream/main`, which is a real local merge on the attributed
# path. Measured both ways in a scratch repo: with the attribute the driver
# fires and the lockfile is regenerated; with the attribute removed and
# nothing else changed, the same merge ends in `CONFLICT (content)` with
# conflict markers in `pnpm-lock.yaml`. Removing the CI half forces
# `ci-cd-pipeline-doc.test.ts` to demand the attribute's removal too, which
# would break that live contributor path — so the two must be decided
# together, not dropped as one more dead row. objectui#6391 carries it.
- name: Configure Git merge driver for pnpm-lock.yaml
run: |
# Configure custom merge driver for pnpm-lock.yaml
# This allows Git to automatically resolve lockfile conflicts by regenerating it
git config merge.pnpm-merge.name "pnpm-lock.yaml merge driver"
git config merge.pnpm-merge.driver "pnpm install --no-frozen-lockfile"
- name: Turbo Cache
uses: actions/cache@v6
with:
path: .turbo/cache
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
# ⛔ Do not add a `pnpm test` step back here.
#
# There used to be one, and it was 31m19s of a 33m22s job (medians over the 106 runs
# of #3712-#3911 that executed; everything else combined is ~90s — install 6s, build
# 35s, the changesets action 24s). It re-tested a commit that is ALREADY on `main`, so
# it could not keep anything out; the only thing it could do was stop the release
# afterwards, and it did: 2 of the last 27 release-PR merges (runs #3606 and #3901)
# failed here and skipped the changesets step entirely. #3606 was the run that would
# have published @object-ui/*@17.5.0, which reached npm 6h50m late, on a later push
# that happened to still find `.changeset/` empty.
#
# The backstop for `main` is `ci.yml`'s push lane, which runs the whole suite under
# coverage across four shards on every push and enforces the thresholds on the merged
# report (objectui#5403). That is the copy that tests this commit; this one only ever
# duplicated it, ~34 minutes later.
- name: Build packages
run: pnpm build
# ══════════════════════════════════════════════════════════════════════
# One changesets step per lane, and each one CANNOT do the other's job.
# ══════════════════════════════════════════════════════════════════════
# These were a single step carrying both `version:` and `publish:`, which
# let repository state pick the behaviour (the algorithm is quoted in this
# file's header). Splitting them is what makes each lane's limit structural
# instead of conditional: the refresh step has no `publish:` input, so no
# state — not even a `.changeset/` that has just been emptied by a merge
# racing the tick — can make a scheduled run publish. An `if:` cannot say
# that; an absent input can.
#
# The `id:`s are for the log, nothing reads them.
#
# ══════════════════════════════════════════════════════════════════════
# ⛔ THE `@v1` PIN IS LOAD-BEARING. READ THIS BEFORE BUMPING IT.
# ══════════════════════════════════════════════════════════════════════
# `changesets/action@v1` is pinned because two lanes in this file are
# built on properties asserted against **v1's source**, not because anyone
# prefers an old version. Dependabot has proposed the v1 -> v2 bump in both
# repositories and it was declined in both, deliberately and on the same
# reasoning: objectstack#9208 (closed unmerged 2026-08-19) and
# objectui#4945 (closed unmerged 2026-08-23). Neither was merged; both
# repositories stay on v1.
#
# A bump is not forbidden — it is UNVERIFIED. What follows is what a future
# upgrade has to re-establish, written here because the next reader stands
# here and not in a closed PR.
#
# THE TWO PROPERTIES AN UPGRADE MUST RE-VERIFY
# --------------------------------------------
# (1) THE DISPATCH TABLE. `!hasChangesets && hasPublishScript ->
# runPublish` (quoted in full in this file's header, from v1's
# `src/index.ts`). BOTH the `lane` job's predicate and the "Clear
# pending changesets" step exist only because the action chooses its
# branch from repository state rather than from an input. If v-next
# takes an explicit instruction instead, the clear step is no longer a
# workaround to keep — it is dead weight to delete, and keeping it
# would be the bug.
#
# (2) `runPublish` NEVER COMMITS AND NEVER PUSHES A BRANCH. Verified in
# v1's `src/run.ts`: `runPublish` spans lines 83-198 and the only git
# operation inside it is `git.pushTag()` (lines 124 and 146);
# `git.prepareBranch()` (line 290) and `git.pushChanges()` (line 362)
# occur solely inside `runVersion`. THIS is what makes the clear
# step's runner-local deletion safe. ⚠️ If that ever stops holding,
# that step stops being a local trick and becomes a commit that
# DELETES EVERY PENDING CHANGESET IN THE REPOSITORY — ~161 files at
# the time of writing. Re-verify it by reading the source, not by
# reading release notes.
#
# WHAT v2 ACTUALLY CHANGES (read from its CHANGELOG, not from compiled
# source; every item below was checked against the v2.0.0 entry)
# ---------------------------------------------------------------------
# - #681 / #668 — INPUTS RENAMED AND KEBAB-CASED: `version` ->
# `version-script`, `publish` -> `publish-script`, `commit` ->
# `commit-message`, `title` -> `pr-title` (also `branch` ->
# `pr-base-branch`, which this file does not use). This file passes all
# four of the old names. Under v2 they are simply unknown inputs, and
# the two failures are both SILENT: the publish lane loses its
# `publish:` and stops publishing, and the refresh lane loses its
# `version:` and falls back to bare `changeset version` — which skips
# `scripts/sync-quick-reference-release.mjs` and breaks objectui#5394.
# (`scripts/__tests__/sync-quick-reference-release.test.ts` pins the
# `version:` line and would catch the rename, so that half fails loudly
# at least in CI.)
# - #692 — RELEASE COMMITS AND TAGS ARE PUSHED VIA THE GitHub API BY
# DEFAULT (`commit-mode` replaced by a boolean `push-with-git-cli`).
# ⚠️ This is the one that undercuts property (2): that verification is
# about v1's git-CLI path. An API-based push is a different code path
# and property (2) must be re-established against it from scratch.
# - #674 — the `GITHUB_TOKEN` ENVIRONMENT VARIABLE IS NO LONGER ACCEPTED;
# a custom token must go through the `github-token` input. Both steps
# below pass it via `env:`.
# - #695 — `.npmrc` HANDLING REMOVED when `NPM_TOKEN` is set; npm auth is
# expected via Trusted Publishing or `actions/setup-node`'s
# `registry-url`. This job does run `actions/setup-node` with
# `registry-url`, so that half may already be satisfied — but it is
# `NODE_AUTH_TOKEN` that such a setup consumes, and the interaction has
# to be checked rather than assumed.
# - #678 — PUBLISHED-PACKAGE DETECTION MOVED off stdout parsing onto a
# shared output file named by a `CHANGESETS_OUTPUT` environment
# variable, which custom scripts must forward to the Changesets CLI.
# `pnpm changeset:publish` is a custom script
# (`check-published-dist-tooling.mjs && changeset publish`), so it would
# have to forward it or git tags and GitHub releases stop being created
# (#697 downgrades that to a warning rather than an error — i.e. it
# fails QUIETLY, which is this card's whole subject matter).
#
# ⛔ DO NOT WRITE "we are held back by the Changesets CLI generation."
# That is the plausible wrong answer. v2's #699 validates that projects use
# Changesets CLI v3 and directs CLI **v2** users to `changesets/action@v1`
# — and this repository is already on v3: `package.json` declares
# `@changesets/cli: ^3.0.0` and `pnpm-lock.yaml` resolves it to 3.0.1
# (`../objectstack` declares `^3.0.0` too). Measured, not assumed. The CLI
# generation is NOT what pins us; the two properties above are.
#
# Worth knowing rather than acting on: v2's #656 adds `/select-mode`,
# `/version` and `/publish` SUB-ACTIONS. A dedicated publish sub-action is
# plausibly the shape that makes the clear step unnecessary — which is an
# argument for revisiting the pin eventually, not for bumping it blind.
#
# ⛔ WHAT NOTICES A BUMP. This `@v1` ref is pinned by exactly one thing:
# `scripts/__tests__/changeset-release-action-ref-pin.test.ts`. It fails on
# a bump of EITHER `uses: changesets/action@v1` line in this file and names
# the two properties above as what a bump must re-establish. There is no
# second guard — the type checker and the linters do not read this file at
# all — so ⛔ do not delete that test as collateral of some other cleanup.
#
# It is deliberately a pin on the REF and not a copy of the action's
# behaviour. objectui#5775 tried the other shape: the `lane` job carried a
# bash mirror of the action's file scan, measured against BOTH readers and
# found at different majors (this action bundles `@changesets/read@^0.6.7`,
# the repository installs `1.0.0`, and they disagree on `AGENTS.md` /
# `CLAUDE.md` / `GEMINI.md` and on `.changeset/pre/`). objectui#6081
# deleted that mirror, because the bundled copy is a minified chunk behind
# a MOVING tag and cannot be pinned from here at any effort — a retag of
# `v1` in place changed it and nothing here failed. A ref pin cannot catch
# a retag either, but it does not pretend to: it catches the DELIBERATE
# bump, which is the only moment anyone can act on.
# ⚠️ THE STEP THAT LETS THE PREDICATE REACH THE ACTION (objectui#5442).
#
# The job guard above no longer says anything about `.changeset/` — it says
# the declared version is not on npm. But `changesets/action@v1` still
# picks its own branch from `hasChangesets`, so on a release-merge commit
# carrying the NEXT version's changesets it would take `runVersion` and
# publish nothing. That is objectui#5442's defect arriving through the
# action instead of through the job guard, and no `if:` can talk the action
# out of it. A tree with nothing pending can.
#
# ⛔ Nothing is committed and nothing is pushed. That is verified against
# the action's source rather than assumed: `runPublish` (`src/run.ts`) runs
# the publish script, then for each package it published pushes a TAG and
# creates a GitHub release. It never commits, and never pushes a branch. So
# this deletion cannot leave the runner, and `.changeset/` on `main` is
# untouched — those changesets are still owed to the next version PR, which
# the 6-hourly refresh lane will render as usual.
#
# This is deliberately a SUPERSET of what the action counts, and it is NOT
# a second copy of the `lane` job's mirror. The mirror's job is to PREDICT
# the action's branch, and predicting requires agreeing exactly. This step
# has a one-directional obligation — leave the count at zero — so removing
# more than the action would count is safe, and it is checked directly
# below instead of being reasoned about. Keeping only `README.md` covers
# every name the reader ignores (`README.md` case-insensitively, plus
# `AGENTS.md` / `CLAUDE.md` / `GEMINI.md`) as well as `.changeset/pre/*.md`,
# without owning a list that has to track theirs.
#
# `.changeset/config.json` is NOT touched: `changeset publish` reads the
# `fixed` group from it, and so does `check-published-dist-tooling.mjs`,
# which `pnpm changeset:publish` runs first.
#
# ⛔ THE `github.event_name == 'push'` TEST IS LOAD-BEARING. DO NOT DROP IT.
#
# This guard used to carry a second half that read the `lane` job's
# `pending_changesets` output. objectui#6081 deleted that half — the
# step is idempotent and a measured no-op on an empty tree, so predicting
# whether it has work was buying nothing at the price of a mirror of
# another project's file scan (see the `lane` job's report step).
#
# ⚠️ The EVENT half is a different thing entirely and dropping it kills the
# release SILENTLY. On the refresh lane this step would empty the tree
# before the action runs, so `hasChangesets` is false — and the refresh
# step passes no `publish:` input, so `hasPublishScript` is false too.
# v1 then takes `case !hasChangesets && !hasPublishScript` (`src/index.ts`
# line 68), logs "No changesets present or were removed by merging release
# PR", and RETURNS. The 6-hourly refresh becomes a permanent no-op and the
# standing "chore: release packages" PR fossilises forever, with nothing
# failing anywhere. That is this file's own failure class, on the other
# lane. What objectui#6081 deleted is the PREDICTION, not the event scoping.
#
# ⚠️ ONE CASE WHERE THIS STEP IS NOT LITERALLY A NO-OP — dormant here, real
# later. In PRE mode, the ids already listed in `pre.json.changesets` are
# consumed records; the deleted mirror filtered them out and this step does
# not, so it removes them (measured: `Removed 2`) while `pre.json` itself
# SURVIVES, so pre mode is still detected. Harmless on three independent
# grounds — the deletion is runner-local (property (2) above), the next
# `changeset version` deletes those records anyway, and this repository has
# no `pre.json` and no `.changeset/pre/` directory today. Written down
# because a pre-release is precisely when nobody wants to rediscover it.
- name: Clear pending changesets for the publish branch
if: github.event_name == 'push'
run: |
set -euo pipefail
shopt -s nullglob
# `/^README\.md$/i` is how `@changesets/read` spells its exclusion, so
# the comparison is case-folded rather than matching one spelling.
keep_this() {
[ "$(printf '%s' "${1##*/}" | tr '[:upper:]' '[:lower:]')" = 'readme.md' ]
}
removed=0
for file in .changeset/*.md .changeset/pre/*.md; do
if keep_this "$file"; then continue; fi
rm -f "$file"
removed=$((removed + 1))
done
echo "Removed ${removed} pending changeset file(s) from the runner's working tree."
left=0
for file in .changeset/*.md .changeset/pre/*.md; do
if keep_this "$file"; then continue; fi
echo "still pending: ${file}"
left=$((left + 1))
done
if [ "${left}" -ne 0 ]; then
echo "::error::${left} changeset file(s) survived the clear step, so changesets/action will take its runVersion branch and publish nothing (objectui#5442)."
exit 1
fi
# PUBLISH. Reachable only from `push`. Between the job guard (the declared
# version is not on npm) and the step above (nothing pending in the tree),
# the action takes its `!hasChangesets && hasPublishScript` branch, which is
# `runPublish`. No `version:` here: it would be dead configuration, and a
# reader is better served by a step that carries only what it can use.
#
# ⚠️ Do not mistake that for a guarantee. Omitting `version:` does NOT
# disable the refresh branch — the action falls back to plain
# `changeset version` — so what keeps THIS step from force-pushing the
# version PR is now the clear step above, and only that. If you ever drop
# or weaken it, this step starts refreshing the PR again, and the release
# goes back to being skipped exactly as objectui#5442 describes.
- name: Publish to npm
id: changesets-publish
if: github.event_name == 'push'
uses: changesets/action@v1
with:
publish: pnpm changeset:publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# ══════════════════════════════════════════════════════════════════════
# THE LOUD CHECK (objectui#5442, ruled alongside the predicate)
# ══════════════════════════════════════════════════════════════════════
# The defect this closes was never a red run — it was a GREEN one. Run 3370
# on `cfeb378b5` (#3598) completed `success` having published nothing,
# purely because three changesets were pending; 17.4.0 was skipped, the next
# version PR bumped past it, and only an audit of CHANGELOG-against-registry
# noticed, 16 versions later. A publish lane whose failure mode is a green
# tick is not a publish lane, so this one is not allowed to finish green
# without the version it exists to ship being on npm.
#
# There is no innocent reading of a failure here: the job only reached this
# step because the `lane` job said this version was ABSENT from npm.
#
# Scope, so it is not mistaken for more. This asserts the fixed group's
# anchor, and that is enough because the OTHER failure shapes are already
# red without it: a per-package publish failure makes `changeset publish`
# exit non-zero and the action forwards that exit code
# (`process.exit(result.exitCode)` in its `src/index.ts`). What only this
# step can catch is the lane publishing NOTHING while reporting success.
#
# The retries are for registry propagation, not for flakiness: `npm publish`
# returns before the version is readable through every CDN edge. An
# unreadable registry is a failure too — a release nobody can confirm is not
# a confirmed release.
- name: Verify the release reached npm
if: github.event_name == 'push'
env:
ANCHOR_PKG: '@object-ui/core'
ANCHOR_MANIFEST: packages/core/package.json
EXPECTED: ${{ needs.lane.outputs.manifest_version }}
ATTEMPTS: '10'
BACKOFF_SECONDS: '15'
run: |
set -euo pipefail
# The lane job read the anchor from a sparse checkout of the same sha;
# if the two disagree the predicate was computed against a different
# tree than the one just published, and nothing below means what it says.
from_tree=$(jq -r '.version // empty' "$ANCHOR_MANIFEST")
if [ "${from_tree}" != "${EXPECTED}" ]; then
echo "::error::The lane job keyed on ${ANCHOR_PKG}@${EXPECTED} but this checkout declares ${from_tree}. The publish decision was made against a different tree (objectui#5442)."
exit 1
fi
encoded=$(printf '%s' "$ANCHOR_PKG" | sed 's|/|%2F|')
url="https://registry.npmjs.org/${encoded}/${EXPECTED}"
for attempt in $(seq 1 "$ATTEMPTS"); do
code=$(curl -sS -o /dev/null -w '%{http_code}' \
-H 'Cache-Control: no-cache' --max-time 20 "$url" 2>/dev/null) || code='000'
if [ "${code}" = '200' ]; then
echo "${ANCHOR_PKG}@${EXPECTED} is on npm."
echo "- published: \`${ANCHOR_PKG}@${EXPECTED}\`" >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "attempt ${attempt}/${ATTEMPTS}: registry answered ${code} for ${url}"
# An explicit `if`, not `[ ... ] && sleep`: bash exempts AND-lists
# from `set -e`, so the short form works, but it works for a reason
# a reader has to already know.
if [ "${attempt}" -lt "${ATTEMPTS}" ]; then sleep "$BACKOFF_SECONDS"; fi
done
echo "::error::${ANCHOR_PKG}@${EXPECTED} is STILL not on npm after a publish run that reported success. The repository declares a version the registry has never seen — this is objectui#5442's silent failure, made loud. Check the 'Publish to npm' step's log for which branch changesets/action took."
exit 1
# ══════════════════════════════════════════════════════════════════════
# POST-VERSION VALIDATION — THE ONLY GATE THE RELEASE COMMIT EVER PASSES
# (objectui#5397)
# ══════════════════════════════════════════════════════════════════════
# The release PR runs NO CI, and cannot be made to. Re-measured
# 2026-08-25: `ci.yml` has 849 runs on `changeset-release/main` and every
# recent one is `action_required` with
# `created_at == run_started_at == updated_at` — created and immediately
# parked, nothing executed. GitHub does not start workflow runs from
# events raised by `GITHUB_TOKEN`, and the refresh step below force-pushes
# that branch, so there is no stable head for a human to re-run against
# either. On the 17.6.0 release PR the check-runs endpoint returned
# `total_count: 1` — one job, started 7 SECONDS AFTER the merge. The
# largest machine-generated diff of the week is therefore the only commit
# that reaches `main` without passing the merge queue.
#
# Maintainer ruling 2026-08-22 (「接受所有」): validate the POST-VERSION
# tree HERE, before the action opens or updates the PR. Not by giving the
# PR a PAT / GitHub App token (widens the supply-chain trust surface and
# adds rotation obligations), and not by guaranteeing outputs one artifact
# at a time (objectui#5396 saved exactly one doc row and did not
# generalise). The ruling attached a condition — PRICE IT FIRST — and the
# price is what chose the shape below.
#
# WHY THIS IS NOT `pnpm test` (measured 2026-08-25, not argued)
# ------------------------------------------------------------
# The naive reading of the ruling is "run the suite on the post-version
# tree". It is the wrong shape, because THE VERSION STEP CANNOT MOVE A
# SOURCE BYTE. Rendered against this repository's real tree (328 pending
# changesets, 17.6.0 -> 17.7.0) `pnpm changeset:version` touches 411 paths
# and every one of them is one of four kinds:
#
# 330 .changeset/*.md deleted (consumed by the version)
# 40 */package.json the `"version"` key and NOTHING else —
# 80 changed lines, all of them `"version":`
# 40 */CHANGELOG.md generated from the changeset bodies
# 1 QUICK_REFERENCE.md rewritten by `changeset:version`'s
# `sync-quick-reference-release.mjs`
#
# Zero `.ts`, zero `.tsx`, zero config. The source in the post-version
# tree is byte-identical to the `main` commit this run checked out, and
# `ci.yml`'s push lane already ran the whole suite over that commit under
# coverage across four shards. A `pnpm test` here would re-test bytes that
# were tested ~13 min ago and would report nothing new — at 40 minutes a
# go (`ci.yml` measured its own unsharded suite at 39 min 51 s of tests /
# 40 min 19 s of job), four times a day on the cron below, i.e. ~2.7 h of
# runner time daily for a PR nobody reads until release day. That is the
# cost objectstack#10850 was closed to REMOVE, walking back in.
#
# So the validation is scoped to what the diff can actually move, and the
# union — `ci.yml` on the source, these steps on the version outputs — is
# the whole post-version tree.
#
# pnpm quick-reference:check QUICK_REFERENCE.md ~1 s
# pnpm check:control-bytes the 40 generated CHANGELOGs ~4 s
# pnpm test scripts/__tests__ 73 files / 1996 tests ~50 s
#
# The tooling suite is named as a DIRECTORY rather than as a list of test
# files, deliberately: every test in this repository that reads a
# manifest version, `QUICK_REFERENCE.md` or a `CHANGELOG.md` lives in it
# (measured — 7 of 7 matches), `doc-version-claims.test.ts` among them,
# and a list would go stale the first time someone adds an eighth.
# Measured green on a real rendered tree: 1 s + 4 s + 50 s.
#
# ⛔ WHAT IS DELIBERATELY NOT RUN, AND WHY IT IS NOT AN OMISSION.
# `check:spec-floors` and `check:published-dist` read dependency RANGES
# and built `dist/` output. The measurement above is what rules them out:
# no range moved (the only changed key is `"version"`) and no source moved,
# so on this tree they can only restate the verdict they gave on `main`.
# `check:published-dist` also costs a full build (8 m 39 s cold, measured),
# and `pnpm changeset:publish` runs both of them first on the publish lane
# anyway, where the tree they judge is the one being shipped.
#
# ⛔ WHY THERE IS NO "ONLY WHEN THE PR CONTENT CHANGED" CONDITION. That
# layer was the other half of the ruling's condition, and the measurement
# retired it: across the seven consecutive 6-hourly windows from
# 2026-08-23T06:08Z to 2026-08-25T00:10Z, SIX carried new changeset files
# (median 18 per window; 219, 46, 23, 18, 13, 9, and one zero). This
# repository lands ~18 merges a working day and most carry a changeset, so
# a content-change predicate would skip about one refresh in seven — it
# would buy ~14% of 70 seconds while adding exactly the thing
# objectui#6081 just deleted from this file: a local prediction of another
# project's state that is free to be wrong silently. The cost was solved
# by SCOPE, so frequency does not need solving.
#
# FAILURE SEMANTICS. A red validation fails the job and the refresh step
# below never runs, so the standing PR keeps its last VALIDATED content
# instead of being force-pushed to a broken one. If the post-version tree
# stays broken the PR goes stale — but loudly, on a red run every six
# hours, which is the opposite of this card's defect. Nothing here can
# block the publish lane: every step is scoped to `schedule` /
# `workflow_dispatch`, and `push` skips all three.
#
# ⚠️ THE RESTORE STEP IS LOAD-BEARING — DO NOT DROP IT. These steps render
# the version into the runner's working tree, and `changesets/action`
# picks its branch from that tree: with `.changeset/` already consumed it
# would find `hasChangesets` false, and with no `publish:` input on the