Skip to content

Commit 40f40df

Browse files
committed
ci: derive the fresh-install version instead of hand-editing it, and document releasing
`ci-fresh-install.yml` carried MCPP_PIN as a hardcoded literal that a maintainer had to bump on every release. It was never the same thing as the `.xlings.json` pin it was kept equal to: MCPP_PIN is the version UNDER TEST — always the newest published release — while `.xlings.json` is the version BOOTSTRAPPED FROM, which only has to be a released mcpp that can build the current tree. The workflow already computed the right answer. Its `wait-index` guard queries the releases API for the newest tag, waits for xim-pkgindex to track it — and then discards it, while the install jobs used the literal. That gap is exactly the #265 failure: the guard reported "index tracks 0.0.102" and the jobs installed 0.0.100 ten seconds later, meeting an index whose floor was 0.0.101. wait-index now exports the derived version and all four install jobs consume it. Both properties the literal was protecting hold, and one of them structurally: explicit version a derived string is as explicit as a literal, so a lagging runner index still fails loudly with `version not found` rather than silently testing an older binary one source the guard and the jobs cannot disagree about which version is under test, because there is only one value Deriving happens on every trigger, not just post-release — "the newest published release" is the version under test on cron and manual runs too. Only the WAIT stays conditional, since only a post-release run can legitimately race the index. A non-numeric or empty API result is refused, because `mcpp@` with an empty version degrades into bare `mcpp` — straight back to "newest in the runner's index copy". check_version_pins.sh loses the now-meaningless "both pin sites agree" invariant and gains a guard against a literal MCPP_PIN reappearing (probe-tested: it fails when the literal is restored, passes when it is not). Also adds docs/09-release.md (+ zh), which did not exist. The release process lived in commit messages and workflow comments only. It documents the two version groups, what the pipeline automates and what it does not, how to verify a release without trusting a sha256 sidecar, and the index's CDN propagation lag. It also corrects 3b1cb6b, which claimed "the index no longer serves .1" and made the bootstrap bump look mandatory. That diagnosis is wrong — 2026.7.29.1 installs fine from the current index, which retains all 105 published versions; the real cause was a stale local index copy. The bump is a useful check (a green round proves the new release can self-host), not a prerequisite. The one hard rule is direction: never pin a version that is not yet installable.
1 parent 761eeb5 commit 40f40df

6 files changed

Lines changed: 361 additions & 24 deletions

File tree

.github/tools/check_version_pins.sh

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -104,32 +104,42 @@ v_src=$(grep -oE 'MCPP_VERSION[[:space:]]*=[[:space:]]*"[^"]+"' src/toolchain/fi
104104
| grep -oE '"[^"]+"' | tr -d '"' | head -1)
105105
v_xl=$(grep -oE '"mcpp"[[:space:]]*:[[:space:]]*"[^"]+"' .xlings.json \
106106
| grep -oE '"[^"]+"$' | tr -d '"' | head -1)
107-
v_pin=$(grep -oE "MCPP_PIN:[[:space:]]*'[^']+'" .github/workflows/ci-fresh-install.yml \
108-
| grep -oE "'[^']+'" | tr -d "'" | head -1)
109-
110-
note "mcpp version: building=$v_toml (fingerprint=$v_src) bootstrap pin=$v_xl (MCPP_PIN=$v_pin)"
107+
note "mcpp version: building=$v_toml (fingerprint=$v_src) bootstrap pin=$v_xl"
111108

112109
for n in "mcpp.toml:$v_toml" "src/toolchain/fingerprint.cppm:$v_src" \
113-
".xlings.json:$v_xl" "ci-fresh-install.yml MCPP_PIN:$v_pin"; do
110+
".xlings.json:$v_xl"; do
114111
[ -n "${n##*:}" ] || bad "${n%:*} — could not read the mcpp version"
115112
done
116113

114+
# ci-fresh-install.yml used to carry a SECOND hand-edited copy of the bootstrap
115+
# pin (MCPP_PIN), and this script enforced that the two stayed equal. They are
116+
# not the same thing and never were: MCPP_PIN is the version UNDER TEST (always
117+
# the newest published release), while .xlings.json is the version BOOTSTRAPPED
118+
# FROM (any released mcpp that can build the current tree). Keeping them equal
119+
# forced a manual edit on every release for a value the workflow could derive —
120+
# and the workflow's own index guard was already deriving it from the releases
121+
# API and discarding it. MCPP_PIN is now that derived value, so there is no
122+
# second site left to agree with. Guard against a silent regression to a
123+
# literal:
124+
if grep -qE "MCPP_PIN:[[:space:]]*['\"]?[0-9]" .github/workflows/ci-fresh-install.yml; then
125+
bad "ci-fresh-install.yml hardcodes MCPP_PIN again — it must stay derived from
126+
wait-index's releases-API lookup, or the index guard and the install jobs
127+
can once more disagree about which version is under test (#265)"
128+
fi
129+
117130
# (a) The version being BUILT: mcpp.toml and the compiled-in constant are the
118131
# same number by definition — release.yml derives the tag from the former
119132
# and the smoke test greps the latter out of `mcpp --version`.
120133
[ -z "$v_src" ] || [ "$v_src" = "$v_toml" ] \
121134
|| bad "src/toolchain/fingerprint.cppm has '$v_src' but mcpp.toml has '$v_toml'"
122135

123-
# (b) The version BOOTSTRAPPED FROM: both sites name a mcpp that is already
124-
# published, so they must agree with each other — but they are NOT required
125-
# to equal the version being built. They deliberately lag, and are bumped in
126-
# a separate commit AFTER the release exists in xim-pkgindex (see the
127-
# MCPP_PIN comment in ci-fresh-install.yml). Requiring equality here is what
128-
# an earlier revision of this script got wrong: it sent CI to install a
136+
# (b) The version BOOTSTRAPPED FROM (.xlings.json) names a mcpp that is already
137+
# published, and is NOT required to equal the version being built. It
138+
# deliberately lags, and is bumped in a separate commit AFTER the release
139+
# exists in xim-pkgindex (see docs/09-release.md). Requiring equality here is
140+
# what an earlier revision of this script got wrong: it sent CI to install a
129141
# version that did not exist yet, and every job died with
130142
# `package 'mcpp@<unreleased>' not found`.
131-
[ -z "$v_xl" ] || [ -z "$v_pin" ] || [ "$v_xl" = "$v_pin" ] \
132-
|| bad ".xlings.json pins '$v_xl' but ci-fresh-install.yml MCPP_PIN is '$v_pin' — both bootstrap the same released mcpp"
133143

134144
# (c) …and the bootstrap pin must never run AHEAD of the version being built.
135145
# Four-key numeric sort, so the date scheme orders correctly (a plain

.github/workflows/ci-fresh-install.yml

Lines changed: 60 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,31 @@ concurrency:
3131
group: ci-fresh-install
3232
cancel-in-progress: false # use false to test in PRs, true to only test released mcpp
3333

34-
# Version under test. Bare `xlings install mcpp` resolves "newest in the
35-
# runner's index copy", which is NOT the same source the wait-index job
36-
# polls (raw.githubusercontent.com) — on 2026-07-21 the guard reported
37-
# "index tracks 0.0.102" and the jobs still installed 0.0.100 ten seconds
38-
# later, which then met an index whose floor was 0.0.101 (#265). Pinning
39-
# makes the version explicit: if the index cannot serve it yet, the job
40-
# fails with `version not found` instead of silently testing an older
41-
# binary. Bump together with the .xlings.json workspace pin at release.
42-
env:
43-
MCPP_PIN: '2026.7.30.3'
34+
# The version under test is DERIVED, once, by the resolve-version job below,
35+
# and every install job consumes that one value.
36+
#
37+
# Two things had to hold, and a hardcoded pin only bought the first:
38+
#
39+
# 1. It must be an explicit version string. Bare `xlings install mcpp`
40+
# resolves "newest in the runner's index copy", so a runner whose copy
41+
# lags silently tests an OLDER binary and reports green. Naming the
42+
# version makes a lagging index fail loudly with `version not found`.
43+
#
44+
# 2. The version the index guard waits for must be the version the jobs
45+
# install. On 2026-07-21 they disagreed: the guard reported "index tracks
46+
# 0.0.102" and the jobs installed 0.0.100 ten seconds later, which then met
47+
# an index whose floor was 0.0.101 (#265). The guard already derived the
48+
# real answer from the releases API and threw it away.
49+
#
50+
# Deriving once and feeding both satisfies (1) and makes (2) structurally
51+
# impossible, instead of relying on a human to keep two hand-edited numbers in
52+
# step with a third that moves on its own. `xlings install mcpp@<derived>` is
53+
# every bit as explicit as `mcpp@<literal>`.
54+
#
55+
# NOT to be confused with the .xlings.json workspace pin, which this used to be
56+
# kept equal to. That one is the BOOTSTRAP compiler for the self-host builds and
57+
# has a different requirement — it must be a released mcpp that can build the
58+
# CURRENT source tree — so it stays hand-maintained. See docs/09-release.md.
4459

4560
jobs:
4661
# ──────────────────────────────────────────────────────────────────
@@ -56,11 +71,31 @@ jobs:
5671
runs-on: ubuntu-latest
5772
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
5873
timeout-minutes: 20
74+
outputs:
75+
version: ${{ steps.resolve.outputs.version }}
5976
steps:
77+
# Derive on EVERY trigger, not just post-release. "The newest published
78+
# release" is the version under test whether we got here from the release
79+
# pipeline, from cron, or by hand — only the WAIT below is specific to a
80+
# post-release run, because only then can the index legitimately lag.
81+
- name: Resolve the version under test (newest published release)
82+
id: resolve
83+
run: |
84+
VER=$(curl -fsSL "https://api.github.com/repos/mcpp-community/mcpp/releases/latest" \
85+
| python3 -c "import json,sys; print(json.load(sys.stdin)['tag_name'].lstrip('v'))")
86+
# A blank version would silently degrade `mcpp@$VER` into bare `mcpp`,
87+
# i.e. straight back to "newest in the runner's index copy" — the exact
88+
# failure this job exists to prevent. Refuse instead.
89+
case "$VER" in
90+
''|*[!0-9.]*) echo "::error::could not resolve a version from the releases API (got '$VER')"; exit 1 ;;
91+
esac
92+
echo "version=$VER" >> "$GITHUB_OUTPUT"
93+
echo "version under test: $VER"
6094
- name: Wait for xim-pkgindex to track the released mcpp
6195
if: ${{ github.event_name == 'workflow_run' }}
96+
env:
97+
VER: ${{ steps.resolve.outputs.version }}
6298
run: |
63-
VER=$(curl -fsSL "https://api.github.com/repos/mcpp-community/mcpp/releases/latest" | python3 -c "import json,sys; print(json.load(sys.stdin)['tag_name'].lstrip('v'))")
6499
echo "released: $VER — waiting for index..."
65100
for i in $(seq 1 30); do
66101
if curl -fsSL "https://raw.githubusercontent.com/openxlings/xim-pkgindex/main/pkgs/m/mcpp.lua" | grep -q "\"$VER\""; then
@@ -81,6 +116,9 @@ jobs:
81116
runs-on: ubuntu-24.04
82117
timeout-minutes: 60
83118
env:
119+
# The one derived value (see the header comment): every install job names
120+
# the SAME version the index guard waited for, so the two cannot disagree.
121+
MCPP_PIN: ${{ needs.wait-index.outputs.version }}
84122
# Verbose every mcpp invocation — fresh-install is the cold index/sandbox
85123
# bootstrap path, exactly where extra diagnostics matter (src/cli.cppm).
86124
MCPP_VERBOSE: "1"
@@ -217,6 +255,9 @@ jobs:
217255
image: debian:11
218256
setup: apt-get update && apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file
219257
env:
258+
# The one derived value (see the header comment): every install job names
259+
# the SAME version the index guard waited for, so the two cannot disagree.
260+
MCPP_PIN: ${{ needs.wait-index.outputs.version }}
220261
XLINGS_NON_INTERACTIVE: '1'
221262
HOME: /root
222263
steps:
@@ -277,6 +318,10 @@ jobs:
277318
# independence (this image has no sha256sum; macos-15 does).
278319
runs-on: macos-14
279320
timeout-minutes: 30
321+
env:
322+
# The one derived value (see the header comment): every install job names
323+
# the SAME version the index guard waited for, so the two cannot disagree.
324+
MCPP_PIN: ${{ needs.wait-index.outputs.version }}
280325
steps:
281326
- uses: actions/checkout@v4
282327

@@ -337,6 +382,10 @@ jobs:
337382
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
338383
runs-on: windows-latest
339384
timeout-minutes: 30
385+
env:
386+
# The one derived value (see the header comment): every install job names
387+
# the SAME version the index guard waited for, so the two cannot disagree.
388+
MCPP_PIN: ${{ needs.wait-index.outputs.version }}
340389
steps:
341390
- uses: actions/checkout@v4
342391

docs/09-release.md

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# 09 — Releasing mcpp
2+
3+
How a release of **mcpp itself** reaches users. This is maintainer-facing; for
4+
packaging *your own* project see [02 — Packaging for Release](02-pack-and-release.md).
5+
6+
Until now this process lived only in commit messages and workflow comments. One
7+
of those commit messages contains a misdiagnosis that is corrected in §5.
8+
9+
## 1. The four version sites are two groups
10+
11+
| Site | Group | Moves when |
12+
|---|---|---|
13+
| `mcpp.toml` `[package].version` | **being built** | you start work on a new version |
14+
| `src/toolchain/fingerprint.cppm` `MCPP_VERSION` | **being built** | same commit as above (compiled-in copy) |
15+
| `.xlings.json` `[workspace].mcpp` | **bootstrapped from** | separately, *after* a release is installable |
16+
| `ci-fresh-install.yml` `MCPP_PIN` | ~~bootstrapped from~~ | **nothing — it is derived at run time** (§4) |
17+
18+
`.github/tools/check_version_pins.sh` enforces what is left mechanically. The two
19+
"being built" sites must be equal; the bootstrap pin must never be **newer** than
20+
the version being built.
21+
22+
The two groups are deliberately allowed to differ. Bumping them together is what
23+
an earlier revision of the pin checker required, and it sent every CI job to
24+
install a version that did not exist yet.
25+
26+
## 2. The pipeline
27+
28+
`release.yml` (tag push, or `workflow_dispatch` with no input, which derives the
29+
tag from `mcpp.toml`) does all of this:
30+
31+
```
32+
build ×4 (linux x86_64 / linux aarch64 / macOS ARM64 / Windows x64)
33+
→ GitHub Release v<version> with tarballs + .sha256 sidecars
34+
→ mirror to xlings-res/mcpp on BOTH GitHub and GitCode
35+
→ open the version-bump PR against openxlings/xim-pkgindex
36+
→ workflow_run hook fires ci-fresh-install
37+
```
38+
39+
Two steps are **not** automated:
40+
41+
- **merging the xim-pkgindex bump PR** — a maintainer does it. Until it lands,
42+
the released version is downloadable but not installable via `xlings install`.
43+
- **bumping `.xlings.json`** — see §4.
44+
45+
## 3. Verifying a release
46+
47+
The mirror script verifies its own uploads, but the checks worth doing by hand
48+
are the ones that do not trust a sidecar:
49+
50+
```bash
51+
V=<version>
52+
# both hosts serve every platform, byte-exact
53+
for a in linux-x86_64.tar.gz linux-aarch64.tar.gz macosx-arm64.tar.gz windows-x86_64.zip; do
54+
for h in github.com gitcode.com; do
55+
curl -fsSL -o /dev/null -w "$h $a %{http_code} %{size_download}\n" \
56+
"https://$h/xlings-res/mcpp/releases/download/$V/mcpp-$V-$a"
57+
done
58+
done
59+
# the index's sha256 values match the payloads (recompute; do not read the sidecar)
60+
curl -fsSL -o /tmp/p.tgz "https://github.com/xlings-res/mcpp/releases/download/$V/mcpp-$V-linux-x86_64.tar.gz"
61+
sha256sum /tmp/p.tgz # compare against pkgs/m/mcpp.lua in xim-pkgindex
62+
```
63+
64+
Then a real install, in a **clean-room `XLINGS_HOME`** — never the machine's own
65+
`~/.xlings`, which can mask a broken index with cached state:
66+
67+
```bash
68+
export XLINGS_HOME=$(mktemp -d)
69+
xlings update
70+
xlings install mcpp@$V -y
71+
$(find "$XLINGS_HOME" -name mcpp -type f -path '*/bin/*' | head -1) --version
72+
```
73+
74+
**Index propagation is not instant.** `xim-pkgindex` reaches clients as a CDN
75+
artifact, not a git clone, so a freshly merged bump is invisible for a while
76+
(measured at ~5 min on 2026-07-30; the documented worst case is ~40 min). A
77+
clean-room that still reports the old `latest` has not failed — it has not caught
78+
up. `ci-fresh-install`'s `wait-index` job encodes exactly this with a bounded
79+
15-minute wait.
80+
81+
## 4. The bootstrap pin: what it is, and when to bump it
82+
83+
`.xlings.json`'s `[workspace].mcpp` is the **starting point of self-hosting**
84+
the released mcpp that `xlings install mcpp` puts in the workspace so CI can build
85+
mcpp from source. Its only requirement is that it can build the **current** tree.
86+
87+
**It does not have to move with every release.** The index retains every
88+
published version (105 entries at the time of writing, back to the 0.0.x series),
89+
so an older pin keeps resolving indefinitely — verified by installing a
90+
two-releases-old version against the current index.
91+
92+
Bumping it anyway is reasonable and is what this repository does in practice: a
93+
green CI round on the bumped pin is a direct proof that the new release can build
94+
mcpp itself on every platform. Treat it as a *useful check*, not a prerequisite.
95+
96+
**The one hard constraint is direction**: the pin must never name a version that
97+
is not yet installable. Bump it only after the release is published, mirrored,
98+
**and merged into xim-pkgindex** — otherwise every CI job fails with
99+
`package 'mcpp@<unreleased>' not found`. `check_version_pins.sh` enforces the
100+
weaker "never newer than the version being built"; the index condition is on you.
101+
102+
## 5. `MCPP_PIN` is derived, and why that matters
103+
104+
`ci-fresh-install.yml` used to carry a second hand-edited copy of the pin. It was
105+
never the same thing: `MCPP_PIN` is the version **under test** — always the newest
106+
published release — while `.xlings.json` is the version **bootstrapped from**.
107+
108+
It is now derived once, by the `wait-index` job, from the releases API, and every
109+
install job consumes that single output. Two properties had to hold, and the
110+
hardcoded literal only bought the first:
111+
112+
1. **The version must be explicit.** Bare `xlings install mcpp` resolves "newest
113+
in the runner's index copy", so a lagging runner silently tests an *older*
114+
binary and reports green. Naming the version makes a lagging index fail loudly
115+
with `version not found`. A derived string is every bit as explicit as a
116+
literal one.
117+
2. **The guard and the jobs must name the same version.** On 2026-07-21 they did
118+
not: the index guard reported "index tracks 0.0.102" while the jobs installed
119+
0.0.100 ten seconds later, meeting an index whose floor was 0.0.101 (#265).
120+
The guard was already deriving the right answer and throwing it away. Feeding
121+
both from one value makes that disagreement structurally impossible.
122+
123+
`check_version_pins.sh` fails if a literal `MCPP_PIN:` reappears.
124+
125+
> **Correction.** Commit `3b1cb6b` ("bootstrap pin -> 2026.7.29.2") states *"the
126+
> index no longer serves .1"* and quotes `version '2026.7.29.1' not found`. That
127+
> diagnosis is wrong: `2026.7.29.1` installs fine from the current index. The real
128+
> cause was a **stale local index copy** — the same propagation lag described in
129+
> §3, seen from the other side. Nothing about a release removes older versions,
130+
> and no reasoning should be built on the idea that it does.
131+
132+
## 6. Checklist
133+
134+
```
135+
[ ] version bumped in mcpp.toml + fingerprint.cppm (one commit)
136+
[ ] CHANGELOG entry
137+
[ ] bash .github/tools/check_version_pins.sh
138+
[ ] merge to main, CI green
139+
[ ] gh workflow run release.yml --ref main
140+
[ ] release.yml green (4 builds + publish-ecosystem)
141+
[ ] mirrors serve all four platforms on BOTH hosts, sha256 recomputed
142+
[ ] merge the xim-pkgindex bump PR
143+
[ ] clean-room XLINGS_HOME: xlings install mcpp@<version> succeeds
144+
[ ] (optional) bump .xlings.json — only now, never earlier
145+
```

docs/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- [06 - Workspaces](06-workspace.md)
1212
- [07 - build.mcpp Build Program](07-build-mcpp.md)
1313
- [08 - Toolchain Internals](08-toolchain-internals.md)
14+
- [09 - Releasing mcpp](09-release.md)
1415

1516
## Specifications
1617

0 commit comments

Comments
 (0)