Skip to content

Commit 5993c5b

Browse files
os-zhuangclaude
andauthored
ci(release): 自建 GitHub Release,body 截断到 125k 以内,并把 ADR-0087 D4 重新挂上 (#4900) (#5290)
* ci(release): build GitHub Releases ourselves, with bodies that fit the 125k limit (#4900) changesets/action's `createGithubReleases` posts each package's raw CHANGELOG section as the Release body. @objectstack/spec's section for a single v17 RC is 342,893 characters against the API's 125,000 limit, so the POST 422'd — inside runPublish, i.e. after `changeset publish` had fully succeeded but BEFORE the action set its `published` output. The step went red, `published` stayed false, and the docker job gated on it was skipped: a published npm version with no runtime image. The section only grows, so this failed identically every release in the window. Measured against the live API: @objectstack/spec has NO Release for 17.0.0-rc.0, rc.1 or rc.2 (all 404 by tag), while 16.0.0 and 16.1.0 — 62,886 and 1,523 characters — have theirs, each carrying the ADR-0087 D4 spec-changes.json asset. That asset uploads ONTO the spec Release, so D4 has been silently unmounted for the whole v17 RC window too, not just the Release. `createGithubReleases: false`, and scripts/release-github-releases.mjs does the job instead. It is faithful to what the action produced — same tag, name, prerelease rule, and a direct port of the action's own getChangelogEntry for the body, which reproduces the real @objectstack/cli@17.0.0-rc.2 release body byte for byte (73,993 chars) — plus the three properties it lacked: - Bounded. An over-limit body is cut on a line boundary, any code fence the cut opened is closed so the notice renders as markdown rather than inside a code block, no surrogate pair is split, and both ends carry a link to the complete entry in CHANGELOG.md at the release commit. Cost is measured in UTF-16 code units, which is >= the code-point count for every string, so it can only over-estimate against whichever definition of "character" the API applies (the failing section is 342,893 characters but 359,636 UTF-8 bytes; the API quoted the former). - Idempotent. Looks the release up by tag and PATCHes when it exists, POSTs when it does not. rc.2 left ~69 of 70 releases created, so recovering over a partial set is the normal case, not the exception. - Isolated per package. The action ran the set through one Promise.all, so the first rejection abandoned the rest. This runs sequentially, collects failures and still exits non-zero, so one bad changelog can no longer cost @objectstack/spec its Release — and D4 its mount point. Turning createGithubReleases off also disables the action's per-tag `git push`, which lives in the same block. That is a bonus: scripts/release-publish.sh already pushes every tag in one atomic `git push origin --tags` precisely because those concurrent per-tag pushes raced GitHub's ref backend (#2191). Both publish paths are covered. The recovery step (#4901) now reports `npm-published` separately from `published` — the former means "packages went out and owe Releases", the latter "the docker job must build" — and emits its version unconditionally, since an npm repair whose image happens to exist still owes its Releases. release-spec-changes.sh takes that version as a fallback, so D4 mounts on the recovery path as well, which it never could before. `pnpm check:release-body` runs the script's --self-test in lint.yml: 49 assertions over the real code path, fed the REAL oversized section out of packages/spec/CHANGELOG.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015W6nhsDrz6zWQc8je12a1t * chore(changeset): declare that #4900's release-machinery fix releases nothing Empty frontmatter — the repo's sanctioned "this PR releases nothing" declaration, on par with the skip-changeset label (both are named in the Check Changeset gate). The PR changes only .github/workflows/, root scripts/ and one check: entry in the root (private) package.json, so nothing reaches a published package; a non-empty changeset would bump all 69 packages of the fixed group in lockstep and burn an extra rc for no shipped product code. The body records the one caveat that matters here: an empty changeset is the exact input #4898 showed can jam a release, which is now bounded rather than silent by the recovery step (#4899, made reachable by #4901) — and this PR is what extends the GitHub Releases and the ADR-0087 D4 spec-changes.json attachment onto that recovery path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015W6nhsDrz6zWQc8je12a1t * Revert "chore(changeset): declare that #4900's release-machinery fix releases nothing" This reverts 1f50271, keeping the `skip-changeset` label as this PR's only "releases nothing" declaration. The empty changeset was redundant with the label — Check Changeset exempts a labelled PR at the job level, and the earlier red run predated the label (the PR was created at 15:54:54, the label applied at ~15:56, so that run's event payload carried no labels at all). Any subsequent synchronize event re-evaluates the job `if:` against current labels. Redundancy is not free when the redundant copy is a known-dangerous shape. An empty changeset is exactly the input #4898 showed can jam a release: changesets/action reaches its publish branch only with ZERO pending changesets, and an empty one still counts as pending. The argument that this is now bounded rests on the recovery step (#4899/#4901) — which is a path THIS PR modifies. A PR whose whole purpose is repairing the release machinery should not plant a known-hazardous input and then lean on the very mechanism it is changing to catch it. One declaration, via the label, and no hazard. The gate text that recommends an empty changeset as the way out is tracked separately as #5292. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015W6nhsDrz6zWQc8je12a1t --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 15e61fb commit 5993c5b

5 files changed

Lines changed: 1130 additions & 13 deletions

File tree

.github/workflows/lint.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,23 @@ jobs:
260260
- name: Release-notes drift guard
261261
run: pnpm check:release-notes
262262

263+
# Release-body limit guard (#4900). The GitHub Releases API rejects any
264+
# body over 125,000 characters, and @objectstack/spec's changelog section
265+
# for one v17 RC is ~343,000 — so the release step 422'd after npm had
266+
# already published, took `published` down with it, and silently lost the
267+
# runtime image. scripts/release-github-releases.mjs now builds those
268+
# bodies; this runs its --self-test, which feeds the REAL oversized
269+
# section out of packages/spec/CHANGELOG.md through the real code path and
270+
# asserts the result fits, still links the full entry, closes any code
271+
# fence the cut opened, and splits no surrogate pair. It also covers the
272+
# properties the failure taught us to want: every package in the fixed
273+
# group gets a release, a re-run updates instead of 422-ing on
274+
# `already_exists`, and one package's rejection no longer abandons the
275+
# rest — @objectstack/spec keeps its release, which is where ADR-0087 D4's
276+
# spec-changes.json is attached.
277+
- name: Release-body limit guard
278+
run: pnpm check:release-body
279+
263280
# #3825 Node-version drift guard: a runtime pin is 18 separate string
264281
# literals across .github/workflows, so a split is invisible until someone
265282
# greps for it. One did open — every PR gate sat on Node 20 (EOL

.github/workflows/release.yml

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,25 @@ jobs:
158158
version: pnpm run version
159159
commit: 'chore: version packages'
160160
title: 'chore: version packages'
161+
# GitHub Releases are created by the step below instead (#4900). The
162+
# action posts each package's raw CHANGELOG section as the Release
163+
# body, and @objectstack/spec's section for a single v17 RC is ~343k
164+
# characters against the API's 125,000 limit — so the POST 422'd,
165+
# INSIDE runPublish and therefore BEFORE `published` was set. npm had
166+
# already published; the step went red anyway, `published` stayed
167+
# false, and the docker job was skipped. The section only grows, so it
168+
# failed identically every release in the window: spec has no Release
169+
# for 17.0.0-rc.0/rc.1/rc.2 (all 404), while 16.0.0 and 16.1.0 — 62,886
170+
# and 1,523 characters — have theirs, with the ADR-0087 D4
171+
# spec-changes.json asset that uploads onto it.
172+
#
173+
# NOTE this also disables the action's own per-tag `git push` — those
174+
# calls live inside the same `if (createGithubReleases)` block in
175+
# runPublish. That is a bonus, not a loss: scripts/release-publish.sh
176+
# already pushes every tag in ONE atomic `git push origin --tags`
177+
# precisely because the action's concurrent per-tag pushes raced
178+
# GitHub's ref backend (#2191). The workaround's own cause is now gone.
179+
createGithubReleases: false
161180
env:
162181
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
163182
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -194,6 +213,10 @@ jobs:
194213
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
195214
run: |
196215
version=$(node -p "require('./packages/cli/package.json').version")
216+
# Emitted unconditionally: the two facts this step can establish —
217+
# "npm needed repairing" and "the image is missing" — are independent,
218+
# and both consumers need the version regardless of which fired.
219+
echo "version=$version" >> "$GITHUB_OUTPUT"
197220
198221
# ── npm ─────────────────────────────────────────────────────────────
199222
if npm view "@objectstack/cli@$version" version >/dev/null 2>&1; then
@@ -208,7 +231,12 @@ jobs:
208231
echo "::error::publish ran but @objectstack/cli@$version is still not on npm"
209232
exit 1
210233
fi
211-
echo "::warning::Recovered npm packages and git tags. The GitHub Releases and the ADR-0087 D4 spec-changes attachment were NOT created — those only exist on the Changesets action's own publish path. Create them by hand if this release needs them."
234+
echo "::warning::Recovered npm packages and git tags. The GitHub Releases and the ADR-0087 D4 spec-changes attachment are created by the steps below, which follow this recovery path too (#4900)."
235+
# Distinct from `published` below, which means "the docker job must
236+
# build". This one means "a publish happened here", which is what
237+
# the Release/D4 steps key off — an npm repair whose image happens to
238+
# exist still owes its GitHub Releases.
239+
echo "npm-published=true" >> "$GITHUB_OUTPUT"
212240
fi
213241
214242
# ── runtime image ───────────────────────────────────────────────────
@@ -228,20 +256,56 @@ jobs:
228256
fi
229257
230258
echo "::warning::No ghcr image for $version (or the registry could not be probed) — requesting the Docker job."
231-
{
232-
echo "published=true"
233-
echo "version=$version"
234-
} >> "$GITHUB_OUTPUT"
259+
echo "published=true" >> "$GITHUB_OUTPUT"
260+
261+
- name: Create GitHub Releases (bodies truncated to the API limit)
262+
id: github-releases
263+
# Replaces the Changesets action's own createGithubReleases (#4900).
264+
# Same tag, name, prerelease rule and changelog-entry body — the body
265+
# extractor is a direct port of the action's getChangelogEntry, verified
266+
# to reproduce the real @objectstack/cli@17.0.0-rc.2 release body exactly
267+
# — plus the three properties it lacked: bodies truncated to fit the
268+
# 125,000-character limit with a link to the full CHANGELOG entry,
269+
# idempotent create-or-update so a re-run over a partially-created set
270+
# cannot 422, and per-package isolation so one bad package no longer
271+
# abandons the rest of the fixed group (the action ran them under a
272+
# single Promise.all).
273+
#
274+
# `!cancelled()` for the same reason the recovery step above carries it:
275+
# a bare `if:` is implicitly wrapped in success(), which would skip this
276+
# for exactly the failures it exists to survive.
277+
#
278+
# Runs on BOTH publish paths. `npm-published` — not `published`, which
279+
# means "the docker job must build" — is the recovery path's signal that
280+
# packages went out and therefore owe Releases.
281+
if: ${{ !cancelled() && (steps.changesets.outputs.published == 'true' || steps.recover-publish.outputs.npm-published == 'true') }}
282+
env:
283+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
284+
# Authoritative when the action published. Empty on the recovery path,
285+
# where RELEASE_VERSION drives the whole publishable workspace instead
286+
# (the Changesets `fixed` group bumps every public package in lockstep,
287+
# which scripts/check-changeset-fixed.mjs gates).
288+
PUBLISHED: ${{ steps.changesets.outputs.publishedPackages }}
289+
RELEASE_VERSION: ${{ steps.recover-publish.outputs.version }}
290+
run: node scripts/release-github-releases.mjs
235291

236292
- name: Attach spec-changes.json to the GitHub Release (ADR-0087 D4)
237293
# Rebuilds the change manifest with the api-surface diff against the
238294
# previously PUBLISHED spec (reusing the ADR-0059 §3 gate artifact) and
239-
# uploads it to the @objectstack/spec release the changesets action
240-
# just created. The npm artifact carries the registry-derived copy.
241-
if: steps.changesets.outputs.published == 'true'
295+
# uploads it to the @objectstack/spec release the step above created.
296+
# The npm artifact carries the registry-derived copy.
297+
#
298+
# This is the D4 mount point, and it is why #4900 could not be answered
299+
# by simply turning createGithubReleases off: `gh release upload` needs a
300+
# Release to upload ONTO. Measured while fixing this — spec has no
301+
# Release for 17.0.0-rc.0/rc.1/rc.2, so D4 has in fact been unmounted for
302+
# the whole v17 RC window; 16.0.0 and 16.1.0 both carry the asset.
303+
# Ordering is load-bearing: this step must follow the one above.
304+
if: ${{ !cancelled() && (steps.changesets.outputs.published == 'true' || steps.recover-publish.outputs.npm-published == 'true') }}
242305
env:
243306
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
244307
PUBLISHED: ${{ steps.changesets.outputs.publishedPackages }}
308+
RELEASE_VERSION: ${{ steps.recover-publish.outputs.version }}
245309
run: bash scripts/release-spec-changes.sh
246310

247311
- name: Extract published @objectstack/cli version

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"check:objectui-changeset": "node scripts/objectui-changeset-digest.mjs --self-test && node scripts/objectui-range.mjs --self-test",
5252
"check:objectui-pin-fresh": "node scripts/check-objectui-pin-fresh.mjs --self-test && node scripts/check-objectui-pin-fresh.mjs",
5353
"check:release-notes": "node scripts/check-release-notes.mjs",
54+
"check:release-body": "node scripts/release-github-releases.mjs --self-test",
5455
"check:node-version": "node scripts/check-node-version.mjs",
5556
"check:published-files": "node scripts/check-published-files.mjs --self-test && node scripts/check-published-files.mjs",
5657
"check:type-check-coverage": "node scripts/check-type-check-coverage.mjs --self-test && node scripts/check-type-check-coverage.mjs",

0 commit comments

Comments
 (0)