diff --git a/.github/workflows/testpr.yaml b/.github/workflows/testpr.yaml index e95c096f8..725f8a1f8 100644 --- a/.github/workflows/testpr.yaml +++ b/.github/workflows/testpr.yaml @@ -27,9 +27,10 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - pull-requests: read # the build: labels + pull-requests: read # the build: and build: labels outputs: distros: ${{ steps.plan.outputs.distros }} + targets: ${{ steps.plan.outputs.targets }} steps: - uses: actions/checkout@v6 with: @@ -50,6 +51,7 @@ jobs: SELECTED: ${{ inputs.distros }} GH_TOKEN: ${{ github.token }} run: | + labels="[]" if [ -n "$SELECTED" ]; then distros=$(pixi run python -c 'import json,os; print(json.dumps(os.environ["SELECTED"].split()))') elif [ -n "$BASE" ]; then @@ -61,6 +63,8 @@ jobs: distros=$(pixi run python tools/robostack.py changed-distros --base HEAD --all) fi echo "distros=$distros" | tee -a "$GITHUB_OUTPUT" + # build:linux / build:osx / build:win labels limit the platforms (all without) + echo "targets=$(pixi run python tools/robostack.py build-targets --labels "$labels")" | tee -a "$GITHUB_OUTPUT" check: runs-on: ubuntu-latest @@ -142,12 +146,7 @@ jobs: fail-fast: false matrix: distro: ${{ fromJSON(needs.plan.outputs.distros) }} - target: - - { os: ubuntu-latest, platform: linux-64 } - - { os: ubuntu-24.04-arm, platform: linux-aarch64 } - - { os: macos-15-intel, platform: osx-64 } - - { os: macos-15, platform: osx-arm64 } - - { os: windows-2022, platform: win-64 } + target: ${{ fromJSON(needs.plan.outputs.targets) }} # tools/robostack.py BUILD_TARGETS name: ${{ matrix.distro }} ${{ matrix.target.platform }} runs-on: ${{ matrix.target.os }} env: diff --git a/AGENTS.md b/AGENTS.md index eec59eca1..8e51076a9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,6 +72,8 @@ should edit a shared file at a time. - A PR builds the distributions it changes (all of them if it touches anything outside `distros//`). To build only some, add `build:` labels (e.g. `build:humble`), then push or re-run the build (adding a label doesn't start one). + `build:linux` / `build:osx` / `build:win` (or `build:`) limit the platforms; + remove them for one full build before merging. - After a merge, `main.yaml` regenerates the staged build branches `buildbranch__`; uploads go to the channel in `distro.yaml`, or to the repository variable `ROBOSTACK_UPLOAD_CHANNEL`. diff --git a/README.md b/README.md index 9a3b64a21..5e664d744 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,9 @@ that (e.g. a humble rebuild that also changes `shared/` or `tools/`), add the la `build:humble` (one per distribution to build), then push or re-run the build: adding a label doesn't start one by itself. Nothing has to be reset before merging. A PR that changes no packages (tooling, CI, docs) gets `build:none`: only the checks run. +Platform labels limit the platforms the same way: `build:linux`, `build:osx`, `build:win` +(or one platform, e.g. `build:linux-64`), e.g. to iterate on Windows fixes without +waiting for the macOS runners; remove them for a full build before merging. ### CI diff --git a/tools/robostack.py b/tools/robostack.py index 78fc21676..cc45e86a2 100644 --- a/tools/robostack.py +++ b/tools/robostack.py @@ -581,6 +581,16 @@ def generate_gha(distro: str, args: list[str]) -> int: NO_BUILD_LABEL = "build:none" # only the checks (tooling, CI or docs changes) +# the pull-request build's platforms and their runners; build:linux / build:osx / build:win +# (or build:) labels limit a PR build to those +BUILD_TARGETS = [ + {"os": "ubuntu-latest", "platform": "linux-64"}, + {"os": "ubuntu-24.04-arm", "platform": "linux-aarch64"}, + {"os": "macos-15-intel", "platform": "osx-64"}, + {"os": "macos-15", "platform": "osx-arm64"}, + {"os": "windows-2022", "platform": "win-64"}, +] +PLATFORM_LABELS = {"linux": ("linux-64", "linux-aarch64"), "osx": ("osx-64", "osx-arm64"), "win": ("win-64",)} def build_distros(changed: list[str], labels: list[str]) -> list[str]: @@ -589,13 +599,24 @@ def build_distros(changed: list[str], labels: list[str]) -> list[str]: touches shared files), else the changed ones.""" if NO_BUILD_LABEL in labels: return [] - selected = sorted({label.split(":", 1)[1] for label in labels if label.startswith("build:")}) + platforms = set(PLATFORM_LABELS) | {t["platform"] for t in BUILD_TARGETS} + selected = sorted({label.split(":", 1)[1] for label in labels if label.startswith("build:")} - platforms) unknown = sorted(set(selected) - set(distros())) if unknown: raise SystemExit(f"build: labels name unknown distributions: {', '.join(unknown)}") return selected or changed +def build_targets(labels: list[str]) -> list[dict]: + """The platforms a pull request builds: those of its build:linux / build:osx / build:win + (or build:) labels when it has any, else all of them.""" + wanted = set() + for label in labels: + name = label.split(":", 1)[1] if label.startswith("build:") else "" + wanted |= set(PLATFORM_LABELS.get(name, ())) | ({name} & {t["platform"] for t in BUILD_TARGETS}) + return [t for t in BUILD_TARGETS if not wanted or t["platform"] in wanted] + + def changed_distros(base: str) -> list[str]: # no common history (main's history was rewritten and force-pushed): nothing to # diff against, so every distribution counts as changed @@ -627,6 +648,12 @@ def main() -> int: changed = distros() if ns.all else changed_distros(ns.base) print(json.dumps(build_distros(changed, json.loads(ns.labels)))) return 0 + if len(sys.argv) >= 2 and sys.argv[1] == "build-targets": + parser = argparse.ArgumentParser(prog="rs build-targets") + parser.add_argument("--labels", default="[]", help="JSON list of the pull request's labels") + ns = parser.parse_args(sys.argv[2:]) + print(json.dumps(build_targets(json.loads(ns.labels)))) + return 0 if len(sys.argv) < 3: print(__doc__) print("distributions:", ", ".join(distros())) diff --git a/tools/tests/test_bot_commands.py b/tools/tests/test_bot_commands.py index 04810c837..25002538f 100644 --- a/tools/tests/test_bot_commands.py +++ b/tools/tests/test_bot_commands.py @@ -235,6 +235,19 @@ def test_build_distros_labels_override_the_changed_ones(): assert rs.build_distros(rs.distros(), ["build:none", "build:humble"]) == [] with pytest.raises(SystemExit): rs.build_distros([], ["build:kinetic"]) + # platform labels don't select distributions + assert rs.build_distros(["humble", "jazzy"], ["build:win", "build:linux-64"]) == ["humble", "jazzy"] + assert rs.build_distros(rs.distros(), ["build:jazzy", "build:osx"]) == ["jazzy"] + + +def test_build_targets_from_platform_labels(): + platforms = lambda labels: [t["platform"] for t in rs.build_targets(labels)] + assert platforms([]) == ["linux-64", "linux-aarch64", "osx-64", "osx-arm64", "win-64"] + assert platforms(["build:jazzy", "automerge"]) == platforms([]) + assert platforms(["build:win"]) == ["win-64"] + assert platforms(["build:linux", "build:win"]) == ["linux-64", "linux-aarch64", "win-64"] + assert platforms(["build:osx-arm64"]) == ["osx-arm64"] + assert rs.build_targets(["build:win"]) == [{"os": "windows-2022", "platform": "win-64"}] def test_run_build_retries_network_errors(tmp_path, monkeypatch):