Skip to content

Commit 04273b9

Browse files
ci(release): move stale pre-release dist-tags up to latest after a stable publish (#25691)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D2u97USsjrQQKUiupKtvnSj)_ Fixes #25581 Clause-②: no Metadata-protocol: none ## What After a stable publish, every pre-release npm dist-tag of the fixed group that the release has overtaken now moves up to the released version. - **`scripts/release-dist-tags.mjs`** (new). For each non-private workspace package (the same `listWorkspacePackages` derivation `release-verify-npm.mjs` and `release-github-releases.mjs` use), it reads the dist-tags from the registry and, against a REFERENCE version: - a tag other than `latest` whose version is a semver **pre-release below** the reference is moved to the reference (`npm dist-tag add NAME@REFERENCE TAG`); - a pre-release tag **at or above** the reference is a cycle in progress and is kept; - a tag on a **stable** version (a maintenance line) and a non-semver value are left alone; `latest` is never written. - REFERENCE is `RELEASE_VERSION` in CI (and then a package whose registry `latest` is not that version is re-read on a bounded 65 s schedule, then skipped with a warning, so no tag can be moved below `latest`); run by hand without it, each package's own `latest`. - A pre-release `RELEASE_VERSION` is a no-op (exit 0). An empty package set is refused. Default mode is a read-only plan; `--apply` writes. - Every failure (unreadable registry, refused write, `latest` disagreeing) prints one `::warning::` naming the package and makes the exit code 1; the other packages are still processed. - **`.github/workflows/release.yml`** — one new step, **last in the `publish` job**, after "Publish to npm + push version tags" and the two GitHub Release steps: `if: !cancelled() && steps.publish.outputs.published == 'true'`, `continue-on-error: true`, `timeout-minutes: 15`, `RELEASE_VERSION` from `steps.guards.outputs.version`, and no secret of its own: its credential is the `$HOME/.npmrc` the publish step already wrote from `NPM_TOKEN`. Nothing above it changed: the trigger, `environment: release`, the `publish-pending` predicate and `force` are untouched. - **`.github/workflows/lint.yml`** — `node scripts/release-dist-tags.mjs --self-test` in `lint-ci-engine`, beside the post-publish npm verification self-test. ## Why non-blocking npm already holds the whole group when this step runs; a stale tag is a nuisance, never a reason to withhold the GitHub Releases or the runtime image (`docker` needs `publish` to succeed). So the release keeps going and the step's own outcome plus its `::warning::` lines say the sweep was incomplete. ## Self-test `node scripts/release-dist-tags.mjs --self-test`: 45 cases in 6 batteries, pinned by name with a floor and a verdict handshake (AGENTS.md, "Writing a `--self-test`"): 1. the plan (the card's measured `{"rc":"17.0.0-rc.6","latest":"18.0.0","next":"18.0.0-next.5"}` moves both; a tag ahead of latest is kept; stable and non-semver tags untouched); 2. the reference (pre-release and non-semver `RELEASE_VERSION`; a lagging `latest` re-read until it agrees; one that never agrees moves nothing, after exactly the settle schedule); 3. plan mode writes nothing, `--apply` writes exactly the plan; 4. failures named and counted without stopping the rest; empty set refused; 5. end to end: the real script spawned over a throwaway workspace, a stub registry and a stub `npm` on PATH, asserting the exact `npm dist-tag add` argv; 6. the release.yml wiring, read from the file: the step sits after the publish step, is `continue-on-error`, gated on `published == 'true'`, bounded, runs `--apply` with the guarded version, and carries no `secrets.` reference. The self-test was seen red before green: its first run failed battery 5 (a synchronous spawn blocked the in-process stub registry), fixed by an async spawn. ## The one-time cleanup (card item 2) The next stable publish sweeps the current stale tags by itself. To sweep them now, someone holding npm publish rights for `@objectstack` runs, from a checkout of `main` after this lands (`pnpm install` done): ```bash node scripts/release-dist-tags.mjs # read-only plan node scripts/release-dist-tags.mjs --apply # perform it (uses your npm login) ``` The read-only plan, run from this branch against the live registry: 73 packages, **141 tags would move** (`next` 18.0.0-next.5 and `rc` 17.0.0-rc.6 to 18.1.0), 0 kept, 0 skipped, 0 failures. If your npm account asks for a one-time password on every write, waiting for the next stable release is the easier path. ⛔ Not run by any agent with any token. Deliberately no new `workflow_dispatch` input: release.yml's two dispatch lanes are the publish repair lane and the version-PR refresh, and a third lane in that file is a maintenance cost the header argues against. ## Gates Local, at head `336c844c` (the branch is on `main` `b2909fd4`): - all **59** families `node scripts/pm/dispatch-gates.mjs --commands` derives for this diff exit 0; `--ran` reconciles 59 derived, 59 run, 0 NOT-MEASURED (a derived zero: every row carries its exit code). They include `check-self-test-wired`, `check-self-test-workflow-commands`, `check-ci-filter-parity`, `check-aggregator-roster`, `check-step-collectors`, `check:workflow-step-name-quoting`, `check:required-contexts`, `check:entry-guard`, `check:nul-bytes`, and the `release-verify-npm` and `release-dist-tags` self-tests; - `eslint --no-inline-config scripts/release-dist-tags.mjs`: exit 0. The repo-wide farm is CI's; its check-runs on this PR are the verdict for everything outside that list. ## Acceptance notes - Moved, not removed, as the card prefers: `@next` keeps resolving and never below `@latest`. - The rule treats a tag on a stable version as out of scope even when it is below latest (for example a future `v17` maintenance tag); only pre-release tags are swept. - `skip-changeset`: the diff ships nothing from any published package (`scripts/` and two workflows). ## 维护者速读(草稿) **改了什么**:稳定版发布成功后,`publish` 作业最后多一步:把 `next`、`rc` 等落后于新 `latest` 的预发布 dist-tag 全部挪到新版本;领先于 `latest` 的(正在进行的预发布周期)和指向稳定版的标签不动;预发布版本的发布不做任何事。逻辑在新脚本 `scripts/release-dist-tags.mjs`,带自测,lint 里跑自测。 **为什么改**:现在 npm 上 73 个包都是 `next`=18.0.0-next.5、`rc`=17.0.0-rc.6,而 `latest` 已是 18.1.0。用户或 AI 习惯性 `npx create-objectstack@next` 会装到比正式版还旧的预发布版,缺正式版的全部修复,且无任何提示;每个以正式版收尾的预发布周期都会重演。 **风险与代价(含回滚)**:该步骤在发布之后、不触碰「ONLY A HUMAN PUBLISHES」那套门控(触发、release 环境审批、publish-pending 判定都未改),不发布任何版本、只改标签,且只会把标签挪到当前 `latest`。失败只告警不让发布失败。最坏情况是标签没挪成,和今天一样。回滚:删掉这一步即可;已挪的标签可用 `npm dist-tag add` 手动改回。 **席位意见**: **你要做的**:① 本 PR 动了 `.github/workflows/release.yml`(Tier H),需要 GOVERNED_APPROVERS 账户批准一次,之后由席位落地。② 一次性清理(可选,下次正式发版会自动清):合并后在 main 检出里、用你的 npm 登录运行 `node scripts/release-dist-tags.mjs` 看计划,再 `node scripts/release-dist-tags.mjs --apply` 执行。 --- _Generated by [Claude Code](https://claude.ai/code/session_01MLah35mAfaKp7EoEM542Js)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent f3d0eab commit 04273b9

3 files changed

Lines changed: 726 additions & 0 deletions

File tree

‎.github/workflows/lint.yml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3326,6 +3326,13 @@ jobs:
33263326
continue-on-error: true
33273327
run: node scripts/release-verify-npm.mjs --self-test
33283328

3329+
# Rationale: `scripts/release-dist-tags.mjs` header (#25581). Its last
3330+
# battery reads release.yml's `publish` job and pins the step's wiring.
3331+
- name: Post-publish dist-tag sweep self-test
3332+
id: post-publish-dist-tag-sweep-self-test
3333+
continue-on-error: true
3334+
run: node scripts/release-dist-tags.mjs --self-test
3335+
33293336
# Release version-commit selection (ADR-0125 D1 as amended 2026-09-29).
33303337
# release.yml's publish lane queues a deployment only on the push that
33313338
# CARRIES the version commit, publishes that commit rather than a later

‎.github/workflows/release.yml‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2126,6 +2126,30 @@ jobs:
21262126
# not by two runs agreeing.
21272127
run: bash scripts/release-spec-changes.sh --attach
21282128

2129+
# #25581: a stable release moves `latest` and nothing else, so the `next`
2130+
# and `rc` tags a pre-release cycle left behind keep resolving to versions
2131+
# older than it (`npx create-objectstack@next` → 18.0.0-next.5 after
2132+
# 18.0.0 shipped). This moves every pre-release tag that the release has
2133+
# overtaken up to it; a tag AHEAD of it (a cycle in progress) and a tag on
2134+
# a stable version are left alone. A pre-release publish is a no-op. Rule,
2135+
# refusals and the one-time cleanup command: the script's header.
2136+
#
2137+
# ⚠️ After the publish, and outside its gating: this step mints nothing
2138+
# and moves no tag to a version that is not already `latest`. Its
2139+
# credential is the `$HOME/.npmrc` the publish step wrote — no secret is
2140+
# handed to it here.
2141+
#
2142+
# Non-blocking by design: npm already has the whole group, so a tag left
2143+
# stale warns (`::warning::` per package, step marked failed) and never
2144+
# fails the job — the runtime image below `needs` this job's success.
2145+
- name: Move stale pre-release dist-tags up to latest
2146+
if: ${{ !cancelled() && steps.publish.outputs.published == 'true' }}
2147+
continue-on-error: true
2148+
timeout-minutes: 15
2149+
env:
2150+
RELEASE_VERSION: ${{ steps.guards.outputs.version }}
2151+
run: node scripts/release-dist-tags.mjs --apply
2152+
21292153
# ══════════════════════════════════════════════════════════════════════════
21302154
# Runtime image — fed by either lane. Building an image for a version that is
21312155
# already on npm cannot mint anything, so the push lane may request it.

0 commit comments

Comments
 (0)