Skip to content

Commit 0e79128

Browse files
ci: verify exact released versions and prepare native isolation
* ci: prepare isolation and verify exact released versions * ci: keep Homebrew upgrade failures visible * ci: keep released consumers in the SubOS-owned workspace * ci: assert structured ARM default toolchain facts * ci: use the native macOS artifact checksum tool --------- Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent f5a7742 commit 0e79128

7 files changed

Lines changed: 171 additions & 33 deletions

File tree

‎.github/tools/check_published_arm64_cn_ecosystem.sh‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,35 @@
33
set -euo pipefail
44
phase="${1:?usage: ecosystem-cn.sh release VERSION}"
55
version="${2:?mcpp version under test required}"
6-
base="${XLINGS_HOME:?explicit isolated XLINGS_HOME required}"
6+
controller="${XLINGS_HOME:?explicit isolated XLINGS_HOME required}"
7+
probes="${CN_PROBE_ROOT:?instance-owned probe root required}"
8+
[[ "$probes" == "$controller/subos/cn-arm64-published/probes" ]] || exit 1
9+
base="$probes/client-home"
10+
[[ ! -e "$base" ]] || { echo 'consumer client home must be cold'; exit 1; }
711
[[ "$(uname -m)" == aarch64 ]] || { echo "native ARM64 required"; exit 1; }
812
[[ "$phase" == release ]] || { echo "published release only"; exit 1; }
913
[[ ! -d "${CN_HOST_CHECKOUT:?host checkout path required}" ]] || {
1014
echo 'host checkout must not be visible in the SubOS'; exit 1;
1115
}
1216
xl="${CN_PUBLISHED_XLINGS:?published client path required}"
13-
work="${CN_PROBE_RUN_DIR:-$base/probes/runs/$phase-$version-$(date +%Y%m%dT%H%M%S)}"
14-
[[ "$work" == "$base/probes/runs/"* ]] || exit 1
17+
work="${CN_PROBE_RUN_DIR:-$probes/runs/$phase-$version-$(date +%Y%m%dT%H%M%S)}"
18+
[[ "$work" == "$probes/runs/"* ]] || exit 1
1519
mkdir -p "$work"
1620
exec > >(tee -a "$work/probe.log") 2>&1
21+
export XLINGS_HOME="$base"
22+
# A fresh client inside the namespace must not inherit its controller's
23+
# project, active instance, or shim dispatch context. Filesystem isolation
24+
# remains enforced by the already-entered SubOS, including the check above.
25+
unset XLINGS_PROJECT_DIR XLINGS_ACTIVE_SUBOS XLINGS_ROOT_INSTANCE XLINGS_ROOT_LOGIN
26+
unset XLINGS_SHIM_DEPTH XLINGS_SUBOS_LIB XLINGS_BIN
1727
printf 'phase=%s version=%s XLINGS_HOME=%s sandbox_HOME=%s\n' "$phase" "$version" "$base" "$HOME"
1828
cat /proc/self/mountinfo > "$work/subos-mountinfo.txt"
1929
export MCPP_HOME="$work/mcpp-home"
2030
export MCPP_VENDORED_XLINGS="$xl"
2131
export MCPP_E2E_MIRROR=CN MCPP_E2E_LLVM_VERSION=23.1.3 MCPP_E2E_EXPECT_ARCH=aarch64
2232
unset LD_LIBRARY_PATH LD_PRELOAD MCPP_TOOLCHAIN
2333
"$xl" config --mirror CN
34+
"$xl" update
2435
"$xl" install "mcpp@$version" -g -y -u
2536
MCPP="$base/data/xpkgs/xim-x-mcpp/$version/bin/mcpp"
2637
[[ -x "$MCPP" ]] || { echo "mcpp not executable: $MCPP"; exit 1; }
@@ -43,8 +54,20 @@ project="${CN_PROBE_PROJECT:-hello}"
4354
"$MCPP" new "$project"
4455
cd "$work/$project"
4556
"$MCPP" build 2>&1 | tee "$work/native-default-build.log"
46-
grep -q 'Resolved llvm@23.1.3' "$work/native-default-build.log"
47-
grep -Eq 'aarch64-(unknown-)?linux-gnu' "$work/native-default-build.log"
57+
# Cold automatic provisioning and warm resolution have different progress
58+
# messages. Assert the documented build facts instead of a human log banner.
59+
"$MCPP" emit build-database --format json > "$work/native-build-database.json"
60+
python3 - "$work/native-build-database.json" "$project" "$MCPP_HOME" <<'PYDEFAULT'
61+
import json,pathlib,sys
62+
envelope=json.load(open(sys.argv[1]))
63+
assert envelope['kind']=='mcpp.build-database',envelope
64+
database=envelope['data']['database']
65+
selected=next(s for s in database['sets'] if s['name']==sys.argv[2])
66+
toolchain=database['ide']['toolchains'][selected['ide']['toolchain']]
67+
assert toolchain['family']=='clang' and toolchain['version']=='23.1.3',toolchain
68+
assert toolchain['target'] in ('aarch64-linux-gnu','aarch64-unknown-linux-gnu'),toolchain
69+
pathlib.Path(toolchain['driver']).resolve().relative_to(pathlib.Path(sys.argv[3]).resolve())
70+
PYDEFAULT
4871
mapfile -t native_bins < <(find target -type f -path "*/bin/$project")
4972
[[ "${#native_bins[@]}" == 1 ]] || exit 1
5073
readelf -hW "${native_bins[0]}" | tee "$work/native-header.txt"
@@ -157,5 +180,5 @@ for target in x86_64-linux-gnu aarch64-macos x86_64-windows-gnu; do
157180
done
158181
echo 'RELEASE BUILD PASS: CN install/default/new/build/run/pack, native openkal, four consumers and three target artifacts.'
159182
cp "$work/openkal-source-sha.txt" "$work/cross/source-sha.txt"
160-
printf '%s\n' "$work" > "$base/probes/last-run.txt"
183+
printf '%s\n' "$work" > "$probes/last-run.txt"
161184
echo 'Foreign target execution is pending the three target-system jobs.'

‎.github/tools/check_version_pins.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ done
123123
# literal:
124124
if grep -qE "MCPP_PIN:[[:space:]]*['\"]?[0-9]" .github/workflows/ci-fresh-install.yml; then
125125
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
126+
resolve-version's releases-API lookup, or the index guard and the install jobs
127127
can once more disagree about which version is under test (#265)"
128128
fi
129129

‎.github/tools/run_published_arm64_cn.sh‎

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33
set -euo pipefail
44
version=${1:?exact published mcpp version}
55
repo=${2:?checkout holding these tools and the canonical client pin}
6-
backend=${3:-proot}
6+
backend=${3:-bwrap}
77
[[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit 2
88
[[ "$(uname -m)" == aarch64 ]] || exit 2
99
[[ "$backend" == proot || "$backend" == bwrap ]] || exit 2
1010
base=${XLINGS_HOME:?cold isolated home required}
1111
[[ ! -e "$base" ]] || { echo 'home must be cold'; exit 1; }
12-
mkdir -p "$base/bootstrap" "$base/probes" "$base/reports"
12+
mkdir -p "$base/bootstrap" "$base/reports"
1313
cd "$base"
1414
# Derive the client version from the repository's existing single pin.
1515
xl_version=$(python3 - "$repo/src/provider/xlings/xlings.cppm" <<'PY'
@@ -43,17 +43,17 @@ xl="$base/bootstrap/xlings-$xl_version-linux-aarch64/bin/xlings"
4343
"$xl" update
4444
cp "$base/data/xim-pkgindex/.xlings-index-cache.json" "$base/reports/outer-published-index.json"
4545
# Native backend is provided by distro infrastructure, never labeled an xim
46-
# ARM package. The published bwrap package is x86-only. The client locates the
47-
# system proot directly; bwrap's current locator needs this explicit private
48-
# system-backend directory. Record the symlink and actual distro version.
46+
# ARM package. Record its distro version and use the published client's
47+
# documented isolation setup, which prepares a root-owned bwrap and a narrow
48+
# AppArmor profile when needed, without disabling the host's restriction.
4949
backend_package="$backend"
5050
[[ "$backend" != bwrap ]] || backend_package=bubblewrap
5151
dpkg-query -W "$backend_package" | tee "$base/reports/backend-version.txt"
5252
command -v "$backend" | tee "$base/reports/backend-path.txt"
5353
sha256sum "$(command -v "$backend")" > "$base/reports/backend.sha256"
5454
if [[ "$backend" == bwrap ]]; then
55-
mkdir -p "$base/data/xpkgs/xim-x-bwrap/ci-system-backend/bin"
56-
ln -s /usr/bin/bwrap "$base/data/xpkgs/xim-x-bwrap/ci-system-backend/bin/bwrap"
55+
"$xl" self doctor --isolation --fix -y | tee "$base/reports/isolation-doctor.txt"
56+
"$xl" self doctor --isolation --json > "$base/reports/isolation-doctor.json"
5757
fi
5858
# Independently prove the exact mcpp archive is available through public CN.
5959
mcpp_archive="mcpp-$version-linux-aarch64.tar.gz"
@@ -69,15 +69,20 @@ mcpp_binary_sha=$(sha256sum "${archive_binaries[0]}" | cut -d ' ' -f 1)
6969
printf '%s\n' "$mcpp_binary_sha" > "$base/reports/mcpp-cn-binary.sha256"
7070
"$xl" subos new cn-arm64-published
7171
"$xl" subos runtime glibc@2.44.3 cn-arm64-published
72-
cp "$repo/.github/tools/check_published_arm64_cn_ecosystem.sh" "$base/probes/ecosystem.sh"
73-
printf -v command 'CN_HOST_CHECKOUT=%q CN_EXPECTED_BINARY_SHA=%q CN_PUBLISHED_XLINGS=%q bash %q release %q' "$repo" "$mcpp_binary_sha" "$xl" "$base/probes/ecosystem.sh" "$version"
72+
# The controller home is read-only inside a real SubOS. The selected instance
73+
# owns its writable workspace; its consumer uses a separate cold client home.
74+
probes="$base/subos/cn-arm64-published/probes"
75+
mkdir -p "$probes"
76+
cp "$repo/.github/tools/check_published_arm64_cn_ecosystem.sh" "$probes/ecosystem.sh"
77+
printf -v command 'CN_HOST_CHECKOUT=%q CN_EXPECTED_BINARY_SHA=%q CN_PUBLISHED_XLINGS=%q CN_PROBE_ROOT=%q bash %q release %q' "$repo" "$mcpp_binary_sha" "$xl" "$probes" "$probes/ecosystem.sh" "$version"
7478
"$xl" subos use cn-arm64-published --sandbox "$backend" --cmd "$command"
75-
work=$(cat "$base/probes/last-run.txt")
76-
[[ "$work" == "$base/probes/runs/"* ]] || exit 1
79+
work=$(cat "$probes/last-run.txt")
80+
[[ "$work" == "$probes/runs/"* ]] || exit 1
7781
cmp "$base/reports/release-tag-sha.txt" "$work/mcpp-source-sha.txt"
7882
cp "$base/reports/release-tag-sha.txt" "$work/cross/engine-release-sha.txt"
7983
cp "$work/mcpp-home/registry/data/xim-pkgindex/.xlings-index-cache.json" "$base/reports/inner-published-index.json"
8084
cp "$work/mcpp-home/registry/.xlings.json" "$base/reports/inner-config.json"
85+
cp "$probes/client-home/.xlings.json" "$base/reports/consumer-client-config.json"
8186
cp "$base/.xlings.json" "$base/reports/outer-config.json"
8287
echo "CN_REPORT=$base/reports" >> "$GITHUB_ENV"
8388
echo "CN_CONSUMER_REPORT=$work" >> "$GITHUB_ENV"

‎.github/workflows/ci-arm64-published-cn.yml‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ on:
1010
description: Native distro backend used only as CI isolation infrastructure
1111
required: true
1212
type: choice
13-
default: proot
14-
options: [proot, bwrap]
13+
default: bwrap
14+
options: [bwrap, proot]
1515
permissions:
1616
contents: read
1717
concurrency:
@@ -56,11 +56,12 @@ jobs:
5656
${{ env.XLINGS_HOME }}/reports/*.json
5757
${{ env.XLINGS_HOME }}/reports/*.txt
5858
${{ env.XLINGS_HOME }}/reports/*.sha256
59-
${{ env.XLINGS_HOME }}/probes/runs/*/probe.log
60-
${{ env.XLINGS_HOME }}/probes/runs/*/*.txt
61-
${{ env.XLINGS_HOME }}/probes/runs/*/*.sha256
62-
${{ env.XLINGS_HOME }}/probes/runs/*/*.log
63-
${{ env.XLINGS_HOME }}/probes/runs/*/members.tsv
59+
${{ env.XLINGS_HOME }}/subos/cn-arm64-published/probes/runs/*/probe.log
60+
${{ env.XLINGS_HOME }}/subos/cn-arm64-published/probes/runs/*/*.txt
61+
${{ env.XLINGS_HOME }}/subos/cn-arm64-published/probes/runs/*/*.json
62+
${{ env.XLINGS_HOME }}/subos/cn-arm64-published/probes/runs/*/*.sha256
63+
${{ env.XLINGS_HOME }}/subos/cn-arm64-published/probes/runs/*/*.log
64+
${{ env.XLINGS_HOME }}/subos/cn-arm64-published/probes/runs/*/members.tsv
6465
if-no-files-found: warn
6566
target-run:
6667
needs: consume
@@ -90,7 +91,11 @@ jobs:
9091
run: |
9192
set -euo pipefail
9293
cd art
93-
sha256sum -c SHA256SUMS
94+
if [ "$TARGET_SYSTEM" = macos ]; then
95+
shasum -a 256 -c SHA256SUMS
96+
else
97+
sha256sum -c SHA256SUMS
98+
fi
9499
grep -Eq '^[0-9a-f]{40}$' source-sha.txt
95100
grep -Eq '^[0-9a-f]{40}$' engine-release-sha.txt
96101
grep -Eq '^[0-9a-f]{40}$' index-source-sha.txt

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

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ jobs:
6767
# Convert the race into a bounded wait: poll the index's mcpp.lua until
6868
# it carries the released version (<=15 min), then let every job run.
6969
# Non-workflow_run triggers (manual, cron, PR) skip the wait.
70-
wait-index:
70+
resolve-version:
7171
runs-on: ubuntu-latest
7272
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
73-
timeout-minutes: 20
73+
timeout-minutes: 5
7474
outputs:
7575
version: ${{ steps.resolve.outputs.version }}
7676
steps:
@@ -91,10 +91,19 @@ jobs:
9191
esac
9292
echo "version=$VER" >> "$GITHUB_OUTPUT"
9393
echo "version under test: $VER"
94+
95+
wait-index:
96+
needs: resolve-version
97+
runs-on: ubuntu-latest
98+
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
99+
timeout-minutes: 20
100+
outputs:
101+
version: ${{ needs.resolve-version.outputs.version }}
102+
steps:
94103
- name: Wait for the PUBLISHED index artifact to track the released mcpp
95104
if: ${{ github.event_name == 'workflow_run' }}
96105
env:
97-
VER: ${{ steps.resolve.outputs.version }}
106+
VER: ${{ needs.resolve-version.outputs.version }}
98107
run: |
99108
# Poll the ARTIFACT, not the git file.
100109
#
@@ -537,7 +546,10 @@ jobs:
537546
# from BOTH sides: refused before `brew trust`, accepted after.
538547
macos-brew-fresh:
539548
name: macOS fresh install (Homebrew, ${{ matrix.image }})
549+
needs: resolve-version
540550
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
551+
env:
552+
MCPP_PIN: ${{ needs.resolve-version.outputs.version }}
541553
# Same two images as macos-fresh: the formula declares `depends_on macos:
542554
# :sonoma` + arm64, macos-14 is the oldest image satisfying it, and
543555
# xcode-27 is macOS 27, the newest release.
@@ -561,6 +573,27 @@ jobs:
561573
[ "$(sw_vers -productVersion | cut -d. -f1)" = 27 ] || { echo "::error::xcode-27 delivered macOS $(sw_vers -productVersion), not 27"; exit 1; }
562574
fi
563575
576+
# The tap's update is asynchronous, independently of the xlings index.
577+
# Read its published formula without tapping, so the one-liner below
578+
# still exercises a machine that has never added this tap.
579+
- name: Wait for the tap to carry the version under test
580+
run: |
581+
set -euo pipefail
582+
deadline=$((SECONDS + 600))
583+
ready=false
584+
while (( SECONDS < deadline )); do
585+
formula=$(curl -fsSL --retry 2 --retry-all-errors --max-time 30 \
586+
https://raw.githubusercontent.com/mcpp-community/homebrew-mcpp/main/Formula/mcpp-m.rb 2>/dev/null || true)
587+
version=$(printf '%s\n' "$formula" | sed -nE 's/^[[:space:]]*version "([^"]+)".*/\1/p' | head -1)
588+
if [ "$version" = "$MCPP_PIN" ]; then
589+
ready=true
590+
break
591+
fi
592+
echo "Waiting for tap: expected $MCPP_PIN, observed ${version:-unavailable}"
593+
sleep 15
594+
done
595+
[ "$ready" = true ] || { echo "::error::the tap never published mcpp $MCPP_PIN"; exit 1; }
596+
564597
# ① The command README.md documents, on a machine that has never tapped.
565598
# Fully qualified, so Homebrew treats it as explicit intent.
566599
- name: "Documented one-liner installs"
@@ -570,8 +603,8 @@ jobs:
570603
# runtime, so a bare symlink into the Cellar would make MCPP_HOME the
571604
# versioned Cellar dir and `brew upgrade` would drop every installed
572605
# toolchain. Assert the binary on PATH is brew's and reports the
573-
# version the FORMULA declares — not a hardcoded one, which would go
574-
# stale on every release.
606+
# exact release selected for every installation job. Agreement with
607+
# an old formula alone does not validate the new release.
575608
- name: "Installed binary is brew's, and agrees with the formula"
576609
run: |
577610
set -euo pipefail
@@ -583,8 +616,9 @@ jobs:
583616
formula_version="$(brew info --json=v2 mcpp-community/mcpp/mcpp-m \
584617
| python3 -c 'import json,sys; print(json.load(sys.stdin)["formulae"][0]["versions"]["stable"])')"
585618
echo "formula declares: $formula_version"
619+
test "$formula_version" = "$MCPP_PIN"
586620
mcpp --version
587-
mcpp --version | grep -Fq "$formula_version"
621+
test "$(mcpp --version)" = "mcpp $MCPP_PIN"
588622
589623
# ③ THE TRUST GATE, from the failing side first.
590624
#
@@ -620,7 +654,8 @@ jobs:
620654
brew install mcpp-m # short form
621655
brew uninstall --force mcpp-m
622656
brew install mcpp-community/mcpp/mcpp # the `mcpp` alias
623-
brew upgrade mcpp-m || true # no-op when current; must not be refused
657+
brew upgrade mcpp-m # no-op when current; must not be refused
658+
test "$(mcpp --version)" = "mcpp $MCPP_PIN"
624659
625660
# ⑤ The only assertion a user actually cares about: it builds and runs.
626661
# Exercises the launcher's MCPP_HOME/MCPP_VENDORED_XLINGS pinning,

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ jobs:
114114
GH_TOKEN: ${{ github.token }}
115115
run: |
116116
python3 tests/scripts/test_check_workflow_assertions.py
117+
python3 tests/scripts/test_release_install_verification.py
117118
python3 .github/tools/check_workflow_assertions.py --check-open
118119
119120
- name: The release canary runner runs each command under the named bash
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env python3
2+
"""Run the real Homebrew verification step against controlled command results.
3+
4+
An old formula and its matching old binary must not validate a newer release.
5+
"""
6+
from __future__ import annotations
7+
8+
import importlib.util
9+
import os
10+
from pathlib import Path
11+
import subprocess
12+
import sys
13+
import tempfile
14+
import unittest
15+
16+
ROOT = Path(__file__).resolve().parents[2]
17+
WORKFLOW = ROOT / ".github/workflows/ci-fresh-install.yml"
18+
spec = importlib.util.spec_from_file_location(
19+
"release_ci_assertions", ROOT / ".github/tools/check_workflow_assertions.py"
20+
)
21+
parser = importlib.util.module_from_spec(spec)
22+
sys.modules[spec.name] = parser
23+
spec.loader.exec_module(parser)
24+
25+
26+
def run_guard(formula: str, binary: str) -> subprocess.CompletedProcess[str]:
27+
workflow = parser.parse(WORKFLOW)
28+
job = next(j for j in workflow.jobs if j.key == "macos-brew-fresh")
29+
step = next(s for s in job.steps if s.name.startswith("Installed binary is brew's"))
30+
with tempfile.TemporaryDirectory() as directory:
31+
prefix = Path(directory) / "brew"
32+
bins = prefix / "bin"
33+
bins.mkdir(parents=True)
34+
(bins / "brew").write_text(r'''#!/bin/sh
35+
case "$1" in
36+
--prefix) printf '%s\n' "$CI_STUB_PREFIX" ;;
37+
info) printf '{"formulae":[{"versions":{"stable":"%s"}}]}\n' "$CI_STUB_FORMULA" ;;
38+
*) exit 99 ;;
39+
esac
40+
''')
41+
(bins / "mcpp").write_text(r'''#!/bin/sh
42+
[ "$1" = --version ] || exit 99
43+
printf 'mcpp %s\n' "$CI_STUB_BINARY"
44+
''')
45+
for name in ("brew", "mcpp"):
46+
(bins / name).chmod(0o755)
47+
env = dict(os.environ, PATH=str(bins) + os.pathsep + os.environ["PATH"],
48+
CI_STUB_PREFIX=str(prefix), CI_STUB_FORMULA=formula,
49+
CI_STUB_BINARY=binary, MCPP_PIN="2026.10.21.1")
50+
return subprocess.run(["bash", "-c", step.run], env=env,
51+
capture_output=True, text=True, check=False)
52+
53+
54+
class ReleasedHomebrewVersion(unittest.TestCase):
55+
def test_matching_old_formula_and_binary_do_not_validate_the_new_release(self):
56+
result = run_guard("2026.10.19.1", "2026.10.19.1")
57+
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
58+
59+
def test_the_requested_release_passes(self):
60+
result = run_guard("2026.10.21.1", "2026.10.21.1")
61+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
62+
63+
def test_a_wrong_binary_is_rejected_even_with_the_new_formula(self):
64+
result = run_guard("2026.10.21.1", "2026.10.19.1")
65+
self.assertNotEqual(result.returncode, 0, result.stdout + result.stderr)
66+
67+
68+
if __name__ == "__main__":
69+
unittest.main()

0 commit comments

Comments
 (0)