Repository navigation
Commit 04273b9
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
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3326 | 3326 | | |
3327 | 3327 | | |
3328 | 3328 | | |
| 3329 | + | |
| 3330 | + | |
| 3331 | + | |
| 3332 | + | |
| 3333 | + | |
| 3334 | + | |
| 3335 | + | |
3329 | 3336 | | |
3330 | 3337 | | |
3331 | 3338 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2126 | 2126 | | |
2127 | 2127 | | |
2128 | 2128 | | |
| 2129 | + | |
| 2130 | + | |
| 2131 | + | |
| 2132 | + | |
| 2133 | + | |
| 2134 | + | |
| 2135 | + | |
| 2136 | + | |
| 2137 | + | |
| 2138 | + | |
| 2139 | + | |
| 2140 | + | |
| 2141 | + | |
| 2142 | + | |
| 2143 | + | |
| 2144 | + | |
| 2145 | + | |
| 2146 | + | |
| 2147 | + | |
| 2148 | + | |
| 2149 | + | |
| 2150 | + | |
| 2151 | + | |
| 2152 | + | |
2129 | 2153 | | |
2130 | 2154 | | |
2131 | 2155 | | |
| |||
0 commit comments