Repository navigation
Commit b7433d9
ci(docker-publish): a rebuild of an older version keeps latest, X.Y and X where they are (#27102)
Fixes #26961
Clause-②: no
Metadata-protocol: none
## 维护者速读
- **改了什么**:手动重建(`workflow_dispatch`)旧版本镜像时,`latest`、`X.Y`、`X`
这三个滚动标签不会再被拉回旧镜像;每个滚动标签只在该版本是 npm 上它那个范围内最新的稳定版时才移动。
- **为什么改**:以前在 18.2.0 是现行版本时重建 17.7.0,会把 `latest` 指回 17.7.0,所有拉
`:latest` 的用户下次拉取就会倒退一个大版本。
- **风险与回滚**:正式发版、或重建当前最新版本时,推送的标签列表与改前逐字节相同;读不到 npm
时,手动重建只推精确版本标签,发版通道照旧移动全部滚动标签。回滚就是 revert 这个 PR,无数据迁移。
- **先看**:`.github/workflows/docker-publish.yml` 里 `Compute image tags`
新增的 `flavor:` 一行(`latest` 有两个来源,只关一个关不住);新步骤的 `LANE`
表达式;`scripts/ci/docker-rolling-tags.mjs` 的 `decide()`。
- **两分钟验证**:`node scripts/ci/docker-rolling-tags.mjs --self-test`,预期输出
`✓ docker-rolling-tags --self-test: 16 cases, 20 CLI runs against a fake
npm.`
- **需要你做的**:审阅并合并(Tier H:`.github/workflows/**`)。
## What changes
`.github/workflows/docker-publish.yml`, within the card's file surface:
- **A new step, `Decide which rolling tags this version may move`**
(`id: rolling`), placed after the npm wait step. It runs
`scripts/ci/docker-rolling-tags.mjs --self-test`, then `--version
"$VERSION" --lane "$LANE"`, and writes `latest`, `minor` and `major`
(each `true`/`false`) plus a one-line `reason` to the step outputs and
the step summary.
- **`Compute image tags`** reads those outputs. `{{major}}.{{minor}}`
gets `enable=minor`, `{{major}}` gets `enable=major`, and the raw
`latest` line gets `enable=latest`. A new `flavor: latest=` takes the
same `latest` output.
`scripts/ci/docker-rolling-tags.mjs` (new, dependency-free) implements
the rule. A rolling tag moves only when no stable `@objectstack/cli`
version on npm in its range is newer than VERSION:
| tag | moves when |
|---|---|
| `latest` | no stable version on npm is newer |
| `X.Y` | no stable `X.Y.*` is newer |
| `X` | no stable `X.*.*` is newer |
A prerelease VERSION moves none of them, as before. Prereleases on npm
never hold a rolling tag back. npm is read once with `npm view
@objectstack/cli versions --json`.
**Fail directions** (PM hypothesis 3):
- **Dispatch lane:** when npm's list cannot be read, is not JSON, is
empty, or does not contain VERSION (so it may be stale), only the exact
tag is pushed. A `::warning::` and the step summary say so.
- **Release lane:** a registry hiccup keeps today's behaviour (every
rolling tag moves). A list that lags a fresh publish is expected, so
VERSION is compared as if it were listed.
**Why `latest` needs two switches.** Read from `docker/metadata-action`
at the pinned `dc802804`, `src/meta.ts`:
- With the default `flavor: latest=auto`, the first stable `type=semver`
line sets `version.latest = true` (`procSemver`, L183–198; `setVersion`
first-wins, L357–369).
- `generateTags` then appends `latest` (L525–527) on top of the raw
line's `latest`.
- So gating only the raw line, as the card and the suggested route
assumed, would still move `latest`.
- Both now read one output. Any value other than `true`/`false` makes
the action throw (L54–57; `flavor.ts` L40–41). A missing output
therefore fails `Compute image tags` loudly instead of guessing.
**Why the lane comes from `github.workflow_ref`, not
`github.event_name`** (PM hypothesis 3, falsified as worded):
- In a `workflow_call` run the `github` context is the caller's.
- `release.yml` has its own `workflow_dispatch` repair lane (L208). Its
`release-integrity` job (`if:` at L959–961) feeds the `docker` job
(L2071–2095), which calls this workflow.
- So a real release can arrive here with `event_name ==
'workflow_dispatch'`.
- `github.workflow_ref` names `docker-publish.yml` only when this file
was dispatched directly.
## Tests
`node scripts/ci/docker-rolling-tags.mjs --self-test` gives `✓
docker-rolling-tags --self-test: 16 cases, 20 CLI runs against a fake
npm.` (exit 0).
- Every row spawns the real CLI with a fake `npm` as the only PATH
entry, plus throwaway `GITHUB_OUTPUT` / `GITHUB_STEP_SUMMARY` files.
- Each row asserts:
- the three outputs;
- the `::warning::` line, present or absent;
- the step summary;
- the exact argv npm was called with, or that npm was not called.
- The self-test copies the battery-roster floor (16) and the verdict
handshake from `scripts/ci/nightly-tiers-card.mjs`.
**Per-scenario tag list.** This is a one-time proof, not a committed
test. The new step's `run:` body was extracted from the YAML with PyYAML
(not retyped) and run under `bash -e` with a fake `npm` on PATH, at head
`7b36a647`. npm's list was `17.6.0, 17.6.1, 17.7.0, 18.0.0, 18.1.2,
18.1.3, 18.2.0, 19.0.0-rc.1`.
The before/after columns model the pinned metadata-action's tag assembly
from the source lines cited above; the action itself was not executed
(see Acceptance notes). Image prefix omitted.
| scenario | VERSION | lane | step exit | tags before | tags after |
|---|---|---|---|---|---|
| rebuild of the current version | `18.2.0` | dispatch | 0 | 18.2.0,
18.2, 18, latest, latest | 18.2.0, 18.2, 18, latest, latest |
| release of the newest version | `18.2.0` | release | 0 | 18.2.0, 18.2,
18, latest, latest | 18.2.0, 18.2, 18, latest, latest |
| rebuild of the newest 17.x (the card's example) | `17.7.0` | dispatch
| 0 | 17.7.0, 17.7, 17, latest, latest | 17.7.0, 17.7, 17 |
| rebuild of an older 17.6 patch | `17.6.0` | dispatch | 0 | 17.6.0,
17.6, 17, latest, latest | 17.6.0 |
| rebuild of the newest patch of an older minor | `18.1.3` | dispatch |
0 | 18.1.3, 18.1, 18, latest, latest | 18.1.3, 18.1 |
| rebuild of an older patch (the card's 18.1 example) | `18.1.2` |
dispatch | 0 | 18.1.2, 18.1, 18, latest, latest | 18.1.2 |
| prerelease | `19.0.0-rc.1` | dispatch | 0 | 19.0.0-rc.1 | 19.0.0-rc.1
|
| npm unreadable | `17.7.0` | dispatch | 0 | 17.7.0, 17.7, 17, latest,
latest | 17.7.0 |
| npm unreadable | `18.2.0` | release | 0 | 18.2.0, 18.2, 18, latest,
latest | 18.2.0, 18.2, 18, latest, latest |
| release while npm's list lags the publish | `18.3.0` | release | 0 |
18.3.0, 18.3, 18, latest, latest | 18.3.0, 18.3, 18, latest, latest |
<details><summary>reason line per scenario, as the step wrote
it</summary>
```text
18.2.0 dispatch read 8 versions from npm; moves latest, 18.2, 18
18.2.0 release read 8 versions from npm; moves latest, 18.2, 18
17.7.0 dispatch read 8 versions from npm; moves 17.7, 17; stays: latest (18.2.0 is newer)
17.6.0 dispatch read 8 versions from npm; moves no rolling tag; stays: latest (18.2.0 is newer), 17.6 (17.6.1 is newer), 17 (17.7.0 is newer)
18.1.3 dispatch read 8 versions from npm; moves 18.1; stays: latest (18.2.0 is newer), 18 (18.2.0 is newer)
18.1.2 dispatch read 8 versions from npm; moves no rolling tag; stays: latest (18.2.0 is newer), 18.1 (18.1.3 is newer), 18 (18.2.0 is newer)
19.0.0-rc.1 19.0.0-rc.1 is a prerelease, so it gets only its exact tag
17.7.0 dispatch `npm view @objectstack/cli versions --json` exited 1; dispatch lane, so only the exact tag 17.7.0 is pushed and no rolling tag moves
18.2.0 release `npm view @objectstack/cli versions --json` exited 1; release lane, so every rolling tag moves as it did before this check existed
18.3.0 release read 8 versions from npm; moves latest, 18.3, 18
```
</details>
The doubled `latest` in today's list is what the source assembles (raw
line plus `latest=auto`). It is kept on purpose, so the newest-version
rows are identical before and after.
## Acceptance notes
- **The self-test runs inside the step, not on pull requests.**
- `check:self-test-wired` requires a self-test that a workflow names to
be run by some workflow, and the step runs it right before the decision
takes effect.
- The pull-request home for such self-tests is `lint.yml`'s `Moved
workflow step bodies self-test` collector. That file is outside this
card's file surface, so this PR does not touch it. Wiring it there is a
one-line follow-up for the seat.
- Until then, a broken script is caught when the docker job runs, which
fails before any tag is computed.
- **The two lanes rest on GitHub's documented `workflow_call` context
semantics.** This was not measured: by ruling, the workflow is not
dispatched or re-run to prove it.
- If `github.workflow_ref` named this file in a called run too, the
release lane would take the dispatch fail direction.
- That changes behaviour only when npm is unreadable, and then it holds
`latest` back with a warning. It never moves `latest` wrongly.
- **Older lines.** Rebuilding the newest version of an older line
(17.7.0 while 17.7.0 is the newest 17.x) still moves that line's own
`17.7` and `17`, as the card's done criterion asks; it never moves
`latest`. PM hypothesis 4's "older major: none move" holds when VERSION
is not the newest in its line (row `17.6.0`).
- **npm data could not be reused.** The npm wait step (`5eeb02c6`) reads
per-version manifests and tarballs, not the version list, so the new
step makes its own single read.
- **The release lane gets the rule too.** `release-integrity`'s image
backfill builds main's version. If that version were ever older than
npm's newest stable, the backfill would no longer move `latest`.
- **NOT MEASURED: `actionlint`.** It is not on PATH in this container.
The edited file parses with PyYAML, and the workflow gates below pass.
- **NOT MEASURED: running the pinned action.** Running the downloaded
`docker/metadata-action` `dist` offline was refused by this session's
permission classifier, so the before/after tag lists model its source
rather than its output.
## Verification
All derived families were run locally at head `7b36a647`:
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 48 commands.
- All 48 exited 0.
- `--ran` reconciliation: `48 derived famil(ies) accounted for — 48 run,
0 NOT-MEASURED (a DERIVED zero — all 48 recorded an exit code and none
of them is 3)`.
<details><summary>verdict lines</summary>
```text
check-self-test-wired: every one of the 241 script(s) CI runs that ship a `--self-test` has that self-test run by CI
check-self-test-workflow-commands: no self-test CI runs prints a line the Actions runner would parse as a workflow command.
check-step-collectors: 588 `run:` steps across 30 workflow(s) and 3 composite action(s); 5 step(s) run 2+ independent self-tests, all of them through a collector.
check-workflow-status-functions: OK (scanned 30 workflow file(s), 77 job(s), 45 job-level if: expression(s); 17 read needs.*.outputs.*, all naming a status function).
check-workflow-step-name-quoting: OK (scanned 30 workflow file(s) + 3 composite action file(s), 823 step name(s) -- 8 already quoted, 815 unquoted-and-safe).
check-nul-bytes: OK (scanned 21802 text file(s) ... no raw ASCII control bytes).
check-expected-skips self-test: 206 cases pass
dispatch-gates self-test (fast tier): 1935 cases pass; 7 slow section(s) deferred. (check:pm-dispatch-gates, EXIT=0)
docker-rolling-tags --self-test: 16 cases, 20 CLI runs against a fake npm.
```
</details>
- No package publishes anything here: `scripts/` belongs to the private
root package, so this PR carries `skip-changeset`.
- No commit on `origin/main` since the base `fbc16563` touched
`docker-publish.yml` or `scripts/ci/`.
- ⛔ The workflow was not dispatched or re-run (Prime Directive 15).
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QSeKYmhU5KEj1nukYhp1Tg)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 50b1b7d commit b7433d9
3 files changed
Lines changed: 510 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
127 | 127 | | |
128 | 128 | | |
129 | 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 | + | |
130 | 155 | | |
131 | 156 | | |
132 | 157 | | |
| |||
145 | 170 | | |
146 | 171 | | |
147 | 172 | | |
148 | | - | |
149 | | - | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
150 | 184 | | |
151 | 185 | | |
152 | | - | |
153 | | - | |
154 | | - | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
155 | 189 | | |
156 | 190 | | |
157 | 191 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3422 | 3422 | | |
3423 | 3423 | | |
3424 | 3424 | | |
| 3425 | + | |
3425 | 3426 | | |
3426 | 3427 | | |
3427 | 3428 | | |
3428 | 3429 | | |
3429 | 3430 | | |
3430 | 3431 | | |
3431 | 3432 | | |
3432 | | - | |
| 3433 | + | |
3433 | 3434 | | |
3434 | 3435 | | |
3435 | 3436 | | |
| |||
0 commit comments