Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions .github/tools/check_version_pins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -104,32 +104,42 @@ v_src=$(grep -oE 'MCPP_VERSION[[:space:]]*=[[:space:]]*"[^"]+"' src/toolchain/fi
| grep -oE '"[^"]+"' | tr -d '"' | head -1)
v_xl=$(grep -oE '"mcpp"[[:space:]]*:[[:space:]]*"[^"]+"' .xlings.json \
| grep -oE '"[^"]+"$' | tr -d '"' | head -1)
v_pin=$(grep -oE "MCPP_PIN:[[:space:]]*'[^']+'" .github/workflows/ci-fresh-install.yml \
| grep -oE "'[^']+'" | tr -d "'" | head -1)

note "mcpp version: building=$v_toml (fingerprint=$v_src) bootstrap pin=$v_xl (MCPP_PIN=$v_pin)"
note "mcpp version: building=$v_toml (fingerprint=$v_src) bootstrap pin=$v_xl"

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

# ci-fresh-install.yml used to carry a SECOND hand-edited copy of the bootstrap
# pin (MCPP_PIN), and this script enforced that the two stayed equal. They are
# not the same thing and never were: MCPP_PIN is the version UNDER TEST (always
# the newest published release), while .xlings.json is the version BOOTSTRAPPED
# FROM (any released mcpp that can build the current tree). Keeping them equal
# forced a manual edit on every release for a value the workflow could derive —
# and the workflow's own index guard was already deriving it from the releases
# API and discarding it. MCPP_PIN is now that derived value, so there is no
# second site left to agree with. Guard against a silent regression to a
# literal:
if grep -qE "MCPP_PIN:[[:space:]]*['\"]?[0-9]" .github/workflows/ci-fresh-install.yml; then
bad "ci-fresh-install.yml hardcodes MCPP_PIN again — it must stay derived from
wait-index's releases-API lookup, or the index guard and the install jobs
can once more disagree about which version is under test (#265)"
fi

# (a) The version being BUILT: mcpp.toml and the compiled-in constant are the
# same number by definition — release.yml derives the tag from the former
# and the smoke test greps the latter out of `mcpp --version`.
[ -z "$v_src" ] || [ "$v_src" = "$v_toml" ] \
|| bad "src/toolchain/fingerprint.cppm has '$v_src' but mcpp.toml has '$v_toml'"

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

# (c) …and the bootstrap pin must never run AHEAD of the version being built.
# Four-key numeric sort, so the date scheme orders correctly (a plain
Expand Down
71 changes: 60 additions & 11 deletions .github/workflows/ci-fresh-install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,31 @@ concurrency:
group: ci-fresh-install
cancel-in-progress: false # use false to test in PRs, true to only test released mcpp

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

jobs:
# ──────────────────────────────────────────────────────────────────
Expand All @@ -56,11 +71,31 @@ jobs:
runs-on: ubuntu-latest
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
timeout-minutes: 20
outputs:
version: ${{ steps.resolve.outputs.version }}
steps:
# Derive on EVERY trigger, not just post-release. "The newest published
# release" is the version under test whether we got here from the release
# pipeline, from cron, or by hand — only the WAIT below is specific to a
# post-release run, because only then can the index legitimately lag.
- name: Resolve the version under test (newest published release)
id: resolve
run: |
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'))")
# A blank version would silently degrade `mcpp@$VER` into bare `mcpp`,
# i.e. straight back to "newest in the runner's index copy" — the exact
# failure this job exists to prevent. Refuse instead.
case "$VER" in
''|*[!0-9.]*) echo "::error::could not resolve a version from the releases API (got '$VER')"; exit 1 ;;
esac
echo "version=$VER" >> "$GITHUB_OUTPUT"
echo "version under test: $VER"
- name: Wait for xim-pkgindex to track the released mcpp
if: ${{ github.event_name == 'workflow_run' }}
env:
VER: ${{ steps.resolve.outputs.version }}
run: |
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'))")
echo "released: $VER — waiting for index..."
for i in $(seq 1 30); do
if curl -fsSL "https://raw.githubusercontent.com/openxlings/xim-pkgindex/main/pkgs/m/mcpp.lua" | grep -q "\"$VER\""; then
Expand All @@ -81,6 +116,9 @@ jobs:
runs-on: ubuntu-24.04
timeout-minutes: 60
env:
# The one derived value (see the header comment): every install job names
# the SAME version the index guard waited for, so the two cannot disagree.
MCPP_PIN: ${{ needs.wait-index.outputs.version }}
# Verbose every mcpp invocation — fresh-install is the cold index/sandbox
# bootstrap path, exactly where extra diagnostics matter (src/cli.cppm).
MCPP_VERBOSE: "1"
Expand Down Expand Up @@ -217,6 +255,9 @@ jobs:
image: debian:11
setup: apt-get update && apt-get -y install curl bash tar gzip xz-utils git ca-certificates binutils findutils file
env:
# The one derived value (see the header comment): every install job names
# the SAME version the index guard waited for, so the two cannot disagree.
MCPP_PIN: ${{ needs.wait-index.outputs.version }}
XLINGS_NON_INTERACTIVE: '1'
HOME: /root
steps:
Expand Down Expand Up @@ -277,6 +318,10 @@ jobs:
# independence (this image has no sha256sum; macos-15 does).
runs-on: macos-14
timeout-minutes: 30
env:
# The one derived value (see the header comment): every install job names
# the SAME version the index guard waited for, so the two cannot disagree.
MCPP_PIN: ${{ needs.wait-index.outputs.version }}
steps:
- uses: actions/checkout@v4

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

Expand Down
145 changes: 145 additions & 0 deletions docs/09-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# 09 — Releasing mcpp

How a release of **mcpp itself** reaches users. This is maintainer-facing; for
packaging *your own* project see [02 — Packaging for Release](02-pack-and-release.md).

Until now this process lived only in commit messages and workflow comments. One
of those commit messages contains a misdiagnosis that is corrected in §5.

## 1. The four version sites are two groups

| Site | Group | Moves when |
|---|---|---|
| `mcpp.toml` `[package].version` | **being built** | you start work on a new version |
| `src/toolchain/fingerprint.cppm` `MCPP_VERSION` | **being built** | same commit as above (compiled-in copy) |
| `.xlings.json` `[workspace].mcpp` | **bootstrapped from** | separately, *after* a release is installable |
| `ci-fresh-install.yml` `MCPP_PIN` | ~~bootstrapped from~~ | **nothing — it is derived at run time** (§4) |

`.github/tools/check_version_pins.sh` enforces what is left mechanically. The two
"being built" sites must be equal; the bootstrap pin must never be **newer** than
the version being built.

The two groups are deliberately allowed to differ. Bumping them together is what
an earlier revision of the pin checker required, and it sent every CI job to
install a version that did not exist yet.

## 2. The pipeline

`release.yml` (tag push, or `workflow_dispatch` with no input, which derives the
tag from `mcpp.toml`) does all of this:

```
build ×4 (linux x86_64 / linux aarch64 / macOS ARM64 / Windows x64)
→ GitHub Release v<version> with tarballs + .sha256 sidecars
→ mirror to xlings-res/mcpp on BOTH GitHub and GitCode
→ open the version-bump PR against openxlings/xim-pkgindex
→ workflow_run hook fires ci-fresh-install
```

Two steps are **not** automated:

- **merging the xim-pkgindex bump PR** — a maintainer does it. Until it lands,
the released version is downloadable but not installable via `xlings install`.
- **bumping `.xlings.json`** — see §4.

## 3. Verifying a release

The mirror script verifies its own uploads, but the checks worth doing by hand
are the ones that do not trust a sidecar:

```bash
V=<version>
# both hosts serve every platform, byte-exact
for a in linux-x86_64.tar.gz linux-aarch64.tar.gz macosx-arm64.tar.gz windows-x86_64.zip; do
for h in github.com gitcode.com; do
curl -fsSL -o /dev/null -w "$h $a %{http_code} %{size_download}\n" \
"https://$h/xlings-res/mcpp/releases/download/$V/mcpp-$V-$a"
done
done
# the index's sha256 values match the payloads (recompute; do not read the sidecar)
curl -fsSL -o /tmp/p.tgz "https://github.com/xlings-res/mcpp/releases/download/$V/mcpp-$V-linux-x86_64.tar.gz"
sha256sum /tmp/p.tgz # compare against pkgs/m/mcpp.lua in xim-pkgindex
```

Then a real install, in a **clean-room `XLINGS_HOME`** — never the machine's own
`~/.xlings`, which can mask a broken index with cached state:

```bash
export XLINGS_HOME=$(mktemp -d)
xlings update
xlings install mcpp@$V -y
$(find "$XLINGS_HOME" -name mcpp -type f -path '*/bin/*' | head -1) --version
```

**Index propagation is not instant.** `xim-pkgindex` reaches clients as a CDN
artifact, not a git clone, so a freshly merged bump is invisible for a while
(measured at ~5 min on 2026-07-30; the documented worst case is ~40 min). A
clean-room that still reports the old `latest` has not failed — it has not caught
up. `ci-fresh-install`'s `wait-index` job encodes exactly this with a bounded
15-minute wait.

## 4. The bootstrap pin: what it is, and when to bump it

`.xlings.json`'s `[workspace].mcpp` is the **starting point of self-hosting** —
the released mcpp that `xlings install mcpp` puts in the workspace so CI can build
mcpp from source. Its only requirement is that it can build the **current** tree.

**It does not have to move with every release.** The index retains every
published version (105 entries at the time of writing, back to the 0.0.x series),
so an older pin keeps resolving indefinitely — verified by installing a
two-releases-old version against the current index.

Bumping it anyway is reasonable and is what this repository does in practice: a
green CI round on the bumped pin is a direct proof that the new release can build
mcpp itself on every platform. Treat it as a *useful check*, not a prerequisite.

**The one hard constraint is direction**: the pin must never name a version that
is not yet installable. Bump it only after the release is published, mirrored,
**and merged into xim-pkgindex** — otherwise every CI job fails with
`package 'mcpp@<unreleased>' not found`. `check_version_pins.sh` enforces the
weaker "never newer than the version being built"; the index condition is on you.

## 5. `MCPP_PIN` is derived, and why that matters

`ci-fresh-install.yml` used to carry a second hand-edited copy of the pin. It was
never the same thing: `MCPP_PIN` is the version **under test** — always the newest
published release — while `.xlings.json` is the version **bootstrapped from**.

It is now derived once, by the `wait-index` job, from the releases API, and every
install job consumes that single output. Two properties had to hold, and the
hardcoded literal only bought the first:

1. **The version must be explicit.** Bare `xlings install mcpp` resolves "newest
in the runner's index copy", so a lagging runner silently tests an *older*
binary and reports green. Naming the version makes a lagging index fail loudly
with `version not found`. A derived string is every bit as explicit as a
literal one.
2. **The guard and the jobs must name the same version.** On 2026-07-21 they did
not: the index guard reported "index tracks 0.0.102" while the jobs installed
0.0.100 ten seconds later, meeting an index whose floor was 0.0.101 (#265).
The guard was already deriving the right answer and throwing it away. Feeding
both from one value makes that disagreement structurally impossible.

`check_version_pins.sh` fails if a literal `MCPP_PIN:` reappears.

> **Correction.** Commit `3b1cb6b` ("bootstrap pin -> 2026.7.29.2") states *"the
> index no longer serves .1"* and quotes `version '2026.7.29.1' not found`. That
> diagnosis is wrong: `2026.7.29.1` installs fine from the current index. The real
> cause was a **stale local index copy** — the same propagation lag described in
> §3, seen from the other side. Nothing about a release removes older versions,
> and no reasoning should be built on the idea that it does.

## 6. Checklist

```
[ ] version bumped in mcpp.toml + fingerprint.cppm (one commit)
[ ] CHANGELOG entry
[ ] bash .github/tools/check_version_pins.sh
[ ] merge to main, CI green
[ ] gh workflow run release.yml --ref main
[ ] release.yml green (4 builds + publish-ecosystem)
[ ] mirrors serve all four platforms on BOTH hosts, sha256 recomputed
[ ] merge the xim-pkgindex bump PR
[ ] clean-room XLINGS_HOME: xlings install mcpp@<version> succeeds
[ ] (optional) bump .xlings.json — only now, never earlier
```
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [06 - Workspaces](06-workspace.md)
- [07 - build.mcpp Build Program](07-build-mcpp.md)
- [08 - Toolchain Internals](08-toolchain-internals.md)
- [09 - Releasing mcpp](09-release.md)

## Specifications

Expand Down
Loading
Loading