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
14 changes: 13 additions & 1 deletion .github/tools/release_canaries.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
`set -eo pipefail`, so a failure is the command's own; a command listed under
`expect` must also print the given text. The whole list runs, and the exit
status says whether every command held.

The bash is `$CANARY_BASH` when the workflow names one. On Windows it must: a
Windows program that starts `bash` by name gets `System32\bash.exe`, the WSL
launcher, because the loader searches the system directory before PATH (the
first release run of these canaries printed "Windows Subsystem for Linux has
no installed distributions" for every command).
"""
from __future__ import annotations

Expand Down Expand Up @@ -67,6 +73,12 @@ def cmd_unpin(checkout: str) -> int:
return 0


def shell_argv(command: str) -> list[str]:
"""The argv that runs one canary command: the named bash, else `bash`."""
shell = os.environ.get("CANARY_BASH") or "bash"
return [shell, "-c", f"set -eo pipefail\n{command}"]


def cmd_run(name: str) -> int:
mcpp = os.environ.get("MCPP", "")
if not mcpp:
Expand All @@ -79,7 +91,7 @@ def cmd_run(name: str) -> int:
failed = []
for command in c["commands"]:
print(f"::group::{command}", flush=True)
proc = subprocess.run(["bash", "-c", f"set -eo pipefail\n{command}"],
proc = subprocess.run(shell_argv(command),
capture_output=True, text=True, check=False,
env={**os.environ, "MCPP": mcpp})
sys.stdout.write(proc.stdout)
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/ci-linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ jobs:
python3 tests/scripts/test_check_workflow_assertions.py
python3 .github/tools/check_workflow_assertions.py --check-open

- name: The release canary runner runs each command under the named bash
run: python3 tests/scripts/test_release_canaries.py

# Text-only, like the two steps around it, and it belongs here rather
# than in the target-matrix workflow: that workflow runs the matrix, and
# this asserts a property of the TABLE, which is readable without a
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/release-canaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ jobs:
run: |
export MCPP="$CANDIDATE"
export MCPP_VENDORED_XLINGS="$XLINGS_BIN"
# The bash running this step, by path (release_canaries.py says why).
if [ "$RUNNER_OS" = Windows ]; then
export CANARY_BASH="$(cygpath -w "$BASH")"
else
export CANARY_BASH="$BASH"
fi
"$MCPP" self config --mirror GLOBAL
tool="$GITHUB_WORKSPACE/.github/tools/release_canaries.py"
py=python3; command -v python3 >/dev/null 2>&1 || py=python
Expand Down
102 changes: 102 additions & 0 deletions tests/scripts/test_release_canaries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#!/usr/bin/env python3
"""Tests for .github/tools/release_canaries.py (WS10 of the 2026-09-28 design).

The canary runner runs each command under a named bash (`CANARY_BASH`): on
Windows, `bash` by name is System32's WSL launcher, and the first release run
of the canaries failed every command that way. The runner also reports every
command, enforces `expect`, and removes only the project's own mcpp pin.
"""

from __future__ import annotations

import importlib.util
import json
import os
import shutil
import sys
import tempfile
import unittest
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parents[2]
SCRIPT = REPO_ROOT / ".github" / "tools" / "release_canaries.py"

spec = importlib.util.spec_from_file_location("release_canaries", SCRIPT)
rc = importlib.util.module_from_spec(spec)
spec.loader.exec_module(rc)


class ShellArgv(unittest.TestCase):
def test_the_named_bash_runs_the_command(self) -> None:
os.environ["CANARY_BASH"] = "/opt/git/usr/bin/bash.exe"
try:
argv = rc.shell_argv("echo hi")
finally:
del os.environ["CANARY_BASH"]
self.assertEqual(argv[0], "/opt/git/usr/bin/bash.exe")
self.assertEqual(argv[1], "-c")
self.assertTrue(argv[2].startswith("set -eo pipefail\n"))
self.assertTrue(argv[2].endswith("echo hi"))

def test_without_a_name_the_runner_uses_bash(self) -> None:
os.environ.pop("CANARY_BASH", None)
self.assertEqual(rc.shell_argv("true")[0], "bash")


@unittest.skipIf(shutil.which("bash") is None, "no bash on this host")
class Run(unittest.TestCase):
def run_list(self, toml: str) -> int:
with tempfile.TemporaryDirectory() as d:
listing = Path(d) / "canaries.toml"
listing.write_text(toml, encoding="utf-8")
saved = rc.LIST
rc.LIST = listing
os.environ["MCPP"] = "/bin/true"
try:
return rc.cmd_run("probe")
finally:
rc.LIST = saved
del os.environ["MCPP"]

def test_every_command_held(self) -> None:
self.assertEqual(self.run_list(
'[[canary]]\nname = "probe"\nrepo = "a/b"\nref = "main"\nos = "x"\n'
'commands = ["true", "echo canary-ok"]\n'
'expect = { "echo canary-ok" = "canary-ok" }\n'), 0)

def test_a_failing_command_and_a_missing_expectation_fail(self) -> None:
self.assertEqual(self.run_list(
'[[canary]]\nname = "probe"\nrepo = "a/b"\nref = "main"\nos = "x"\n'
'commands = ["false"]\n'), 1)
self.assertEqual(self.run_list(
'[[canary]]\nname = "probe"\nrepo = "a/b"\nref = "main"\nos = "x"\n'
'commands = ["echo other"]\nexpect = { "echo other" = "canary-ok" }\n'), 1)

def test_a_pipeline_fails_on_its_first_command(self) -> None:
self.assertEqual(self.run_list(
'[[canary]]\nname = "probe"\nrepo = "a/b"\nref = "main"\nos = "x"\n'
'commands = ["false | cat"]\n'), 1)


class Unpin(unittest.TestCase):
def test_only_the_mcpp_pin_is_removed(self) -> None:
with tempfile.TemporaryDirectory() as d:
path = Path(d) / ".xlings.json"
path.write_text(json.dumps({"workspace": {"mcpp": "2026.9.28.1", "cmake": "4.0"},
"mirror": "GLOBAL"}), encoding="utf-8")
self.assertEqual(rc.cmd_unpin(d), 0)
data = json.loads(path.read_text(encoding="utf-8"))
self.assertNotIn("mcpp", data["workspace"])
self.assertEqual(data["workspace"]["cmake"], "4.0")
self.assertEqual(data["mirror"], "GLOBAL")


class Matrix(unittest.TestCase):
def test_the_repository_list_is_well_formed(self) -> None:
names = [c["name"] for c in rc.canaries()]
self.assertEqual(len(names), len(set(names)))
self.assertIn("GalTranslPP", names)


if __name__ == "__main__":
unittest.main(verbosity=2)
Loading