diff --git a/.github/tools/check_version_pins.sh b/.github/tools/check_version_pins.sh index aba1bcfc..d0059b27 100755 --- a/.github/tools/check_version_pins.sh +++ b/.github/tools/check_version_pins.sh @@ -157,11 +157,11 @@ fi # full index, and the mechanism is not yet understood; what is established # is the pattern and its fix. # -# The operational rule that satisfies both directions: after publishing -# release N, the post-release commit sets this pin to N. Never ahead -# (check (c) enforces that), and in practice never behind either. This is -# not checkable here — it needs the index — so it surfaces as the error -# above, and this note is where to look when it does. +# That job now checks the repository out last, so the pin no longer +# reaches it. The rule that stands: never ahead (check (c) enforces it), +# and behind for as long as the pinned mcpp builds the tree, which +# ci-bootstrap.yml and build.yml measure with `mcpp build --strict` +# (docs/92-release.md §4). # (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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15cfc57f..f7eddce0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,7 +59,12 @@ jobs: export MCPP_VENDORED_XLINGS="$XLINGS_BIN" "$XLINGS_BIN" config --mirror GLOBAL 2>/dev/null || true "$MCPP" self config --mirror GLOBAL 2>/dev/null || true - "$MCPP" build + # --strict: a key this bootstrap mcpp does not know is an error, not + # a warning. A bootstrap older than the manifest used to drop such a + # key and build something else, and still exit 0 (2026.9.24.1 and + # `[test] windows_code_page`): the moment `.xlings.json` must move is + # the moment this fails. + "$MCPP" build --strict # target/ is not restored from a cache, so the tree holds exactly the # binary this build linked. case "${{ inputs.host }}" in windows-*) exe=mcpp.exe ;; *) exe=mcpp ;; esac diff --git a/.github/workflows/ci-bootstrap.yml b/.github/workflows/ci-bootstrap.yml new file mode 100644 index 00000000..b85fffe2 --- /dev/null +++ b/.github/workflows/ci-bootstrap.yml @@ -0,0 +1,143 @@ +name: ci-bootstrap + +# Which released mcpp builds this tree, measured the way a developer builds it. +# +# `.xlings.json` names the mcpp a clone of this repository builds with: inside +# the checkout the `mcpp` shim refuses to run any other version +# ("mcpp@ is the version this project asks for ... set this project up: +# xlings install"). The pin does not move with every release; it moves when the +# tree starts to need something the pinned mcpp does not have. Two jobs answer +# whether that moment has come: +# +# developer the pinned mcpp, installed by `xlings install` in the checkout +# (project scope, no -g) and run through the shim, builds the tree +# with --strict, on Linux, macOS and Windows. build.yml builds with +# the same version but installs it globally and runs it by path; a +# shim or project-scope defect, or a cold runner, is only seen here. +# latest the newest release builds the tree with --strict. It runs only +# while the pin lags the newest release; when they are equal the +# developer job has already answered. +# +# --strict is the assertion. A mcpp older than the manifest warns about a key it +# does not know, drops it and exits 0 (2026.9.24.1 against +# `[test] windows_code_page`); under --strict that is an error. +# +# No cache, on purpose: the subject is a fresh clone. + +on: + workflow_call: + +permissions: + contents: read + +jobs: + developer: + name: the pinned mcpp builds the tree (${{ matrix.os }}, xlings install + mcpp build --strict) + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + os: [ubuntu-24.04, macos-15, windows-2025] + env: + XLINGS_NON_INTERACTIVE: '1' + XLINGS_VERSION: '2026.10.10.2' + steps: + - uses: actions/checkout@v4 + + - name: Install xlings + if: runner.os != 'Windows' + shell: bash + run: | + set -euo pipefail + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.10.2 + echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + echo "$HOME/.xlings/bin" >> "$GITHUB_PATH" + + - name: Install xlings + if: runner.os == 'Windows' + shell: pwsh + run: | + irm https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.ps1 | iex + "$env:USERPROFILE\.xlings\subos\current\bin" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 + + - name: Install the mcpp .xlings.json names, in this project + shell: bash + run: | + set -euo pipefail + xlings install -y + pin=$(grep -oE '"mcpp"[[:space:]]*:[[:space:]]*"[^"]+"' .xlings.json \ + | grep -oE '[0-9]+(\.[0-9]+)+' | head -1) + got=$(mcpp --version | head -1 | awk '{print $2}' | tr -d '\r') + [ -n "$pin" ] && [ "$got" = "$pin" ] || { + echo "::error::.xlings.json names mcpp ${pin:-?}; the shim in this checkout runs ${got:-nothing}"; exit 1; } + echo "the shim runs mcpp $got, the version .xlings.json names" + + - name: Build the tree with it (mcpp build --strict) + shell: bash + run: | + set -euo pipefail + mcpp self config --mirror GLOBAL + mcpp build --strict + case "$RUNNER_OS" in Windows) exe=mcpp.exe ;; *) exe=mcpp ;; esac + built=$(find target -type f -name "$exe" -path '*/bin/*' | grep -v '/dist/' || true) + [ "$(printf '%s\n' "$built" | grep -c .)" = 1 ] || { + echo "::error::expected one $exe under target/, found: ${built:-none}"; exit 1; } + want=$(awk -F '"' '/^version[[:space:]]*=/{print $2; exit}' mcpp.toml) + got=$("$built" --version | head -1 | tr -d '\r') + [ "$got" = "mcpp $want" ] || { + echo "::error::the built binary says '$got', mcpp.toml says $want"; exit 1; } + echo "built $got" + + latest: + name: the newest release builds the tree (linux, when the pin lags it) + runs-on: ubuntu-24.04 + timeout-minutes: 45 + env: + XLINGS_NON_INTERACTIVE: '1' + steps: + - uses: actions/checkout@v4 + + - name: Compare the pin with the newest release + id: cmp + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + pin=$(grep -oE '"mcpp"[[:space:]]*:[[:space:]]*"[^"]+"' .xlings.json \ + | grep -oE '[0-9]+(\.[0-9]+)+' | head -1) + latest=$(gh api "repos/${{ github.repository }}/releases/latest" --jq .tag_name | sed 's/^v//') + [ -n "$pin" ] && [ -n "$latest" ] || { + echo "::error::could not read the pin (${pin:-?}) or the newest release (${latest:-?})"; exit 1; } + echo "pin=$pin latest=$latest" + echo "latest=$latest" >> "$GITHUB_OUTPUT" + if [ "$pin" = "$latest" ]; then + echo "run=false" >> "$GITHUB_OUTPUT" + echo "::notice::.xlings.json names the newest release ($latest); the developer job covers it" + else + echo "run=true" >> "$GITHUB_OUTPUT" + fi + + - name: Install xlings + if: steps.cmp.outputs.run == 'true' + shell: bash + run: | + set -euo pipefail + curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.10.10.2 + echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH" + + - name: Install the newest release and build the tree with it (mcpp build --strict) + if: steps.cmp.outputs.run == 'true' + shell: bash + run: | + set -euo pipefail + # Makes the released version the one the shim runs here, over the + # checkout's own pin, and asserts it. + bash .github/tools/install_released_mcpp.sh "${{ steps.cmp.outputs.latest }}" "$(pwd)" + mcpp self config --mirror GLOBAL + mcpp build --strict + built=$(find target -type f -name mcpp -path '*/bin/*' | grep -v '/dist/' || true) + [ "$(printf '%s\n' "$built" | grep -c .)" = 1 ] || { + echo "::error::expected one mcpp under target/, found: ${built:-none}"; exit 1; } + "$built" --version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 78831618..a61cf442 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -186,6 +186,13 @@ jobs: # The MinGW payload every Windows e2e shard installs. prewarm: mingw 16.1.0 + # The mcpp .xlings.json names builds the tree as a developer's clone does, + # and so does the newest release while the pin lags it (ci-bootstrap.yml). + bootstrap: + needs: changes + if: needs.changes.outputs.code == 'true' + uses: ./.github/workflows/ci-bootstrap.yml + linux: needs: build-linux uses: ./.github/workflows/ci-linux.yml diff --git a/.xlings.json b/.xlings.json index 4aab7d29..d52b884e 100644 --- a/.xlings.json +++ b/.xlings.json @@ -1,5 +1,5 @@ { "workspace": { - "mcpp": "2026.10.8.1" + "mcpp": "2026.10.10.2" } } diff --git a/docs/92-release.md b/docs/92-release.md index 927aa059..9516f671 100644 --- a/docs/92-release.md +++ b/docs/92-release.md @@ -189,17 +189,33 @@ up. `ci-fresh-install`'s `wait-index` job encodes exactly this with a bounded ## 4. The bootstrap pin: its definition and its update conditions `.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. +the released mcpp that `xlings install` puts in the workspace so CI can build +mcpp from source. It is also what a developer builds with: inside a clone, the +`mcpp` shim runs this version and no other, and refuses to run until +`xlings install` has installed it. 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. +**It moves when the tree needs it**: when `mcpp.toml` or the sources use something +the pinned mcpp does not have — a key, a table, a toolchain line. CI tells when +that moment has come, because the pinned mcpp builds the tree with `--strict`, +which makes a key it does not know an error. Without it the pinned mcpp warns, +drops the key, builds something other than the manifest describes, and exits 0 +(2026.9.24.1 against the `[test] windows_code_page` of 2026.10.5.2). Three jobs build +the tree on every pull request that changes code: + +| Job | mcpp | Installation and build | +| --- | --- | --- | +| `build.yml` (four hosts) | the pin | installed globally by `install_pinned_mcpp.sh`, `mcpp build --strict` | +| `ci-bootstrap.yml` `developer` (Linux, macOS, Windows) | the pin | a cold clone: `xlings install` in the checkout, then `mcpp build --strict` through the shim | +| `ci-bootstrap.yml` `latest` (Linux) | the newest release | only while the pin lags it | + +A pull request that needs a newer pin fails there, and moves the pin in the same +pull request. **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, diff --git a/docs/zh/92-release.md b/docs/zh/92-release.md index 4a77fa94..219849d7 100644 --- a/docs/zh/92-release.md +++ b/docs/zh/92-release.md @@ -178,16 +178,28 @@ $(find "$XLINGS_HOME" -name mcpp -type f -path '*/bin/*' | head -1) --version ## 4. 自举 pin 的定义与更新条件 `.xlings.json` 的 `[workspace].mcpp` 是**自举的起点**——那个由 -`xlings install mcpp` 装进 workspace、供 CI 从源码构建 mcpp 的已发布 -mcpp。它唯一的要求是:能构建**当前**这棵源码树。 +`xlings install` 装进 workspace、供 CI 从源码构建 mcpp 的已发布 mcpp。 +它也是开发者构建所用的版本:在克隆的仓库里,`mcpp` shim 只运行这个版本, +未经 `xlings install` 安装时拒绝运行。它唯一的要求是:能构建**当前**这棵 +源码树。 **它不必每次发布都跟着动。** 索引保留每一个已发布版本(撰写时 105 个 条目,一直回溯到 0.0.x 系列),旧 pin 可以无限期继续解析——这一点用 「在当前索引下安装一个隔了两个版本的旧版」实测验证过。 -跟着 bump 仍然是合理的,也是本仓库的实际做法:bump 后 CI 一轮全绿,直接 -证明了新发布能在每个平台上构建 mcpp 自己。把它当作**一项有用的检查**, -而不是前置条件。 +**树需要时它才动**:`mcpp.toml` 或源码用到了固定版本没有的东西——一个键、 +一张表、一条工具链线。何时到了这一刻由 CI 判定:固定版本以 `--strict` 构建 +这棵树,它不认识的键是错误。不加 `--strict` 时,固定版本对不认识的键只给 +警告、丢弃该键、构建出与清单不同的结果,并以 0 退出(2026.9.24.1 遇到 +2026.10.5.2 的 `[test] windows_code_page`)。每个改动代码的 PR 上有三组构建: + +| 作业 | mcpp | 方式 | +| --- | --- | --- | +| `build.yml`(四个主机) | 固定版本 | `install_pinned_mcpp.sh` 全局安装,`mcpp build --strict` | +| `ci-bootstrap.yml` `developer`(Linux、macOS、Windows) | 固定版本 | 冷克隆:在仓库内 `xlings install`,经 shim 执行 `mcpp build --strict` | +| `ci-bootstrap.yml` `latest`(Linux) | 最新发布版 | 仅在固定版本落后于最新发布版时运行 | + +需要更新固定版本的 PR 在这里失败,并在同一个 PR 中更新它。 **唯一的硬约束是方向**:pin 绝不能指向一个尚不可安装的版本。只在发布 已完成、已镜像、**且已合入 xim-pkgindex 之后**再 bump——否则所有 CI 会