diff --git a/.github/scripts/dispatch.py b/.github/scripts/dispatch.py index 35e56706..f54cd589 100644 --- a/.github/scripts/dispatch.py +++ b/.github/scripts/dispatch.py @@ -95,12 +95,25 @@ class DispatchPlan: # Reading build.toml and selecting workflows -def read_backends(kernel_name: str) -> list[str] | None: - build_toml = Path(kernel_name) / "build.toml" - if not build_toml.exists(): - return None - with open(build_toml, "rb") as f: - config = tomllib.load(f) +def read_backends(kernel_name: str, ref: str = "") -> list[str] | None: + # ref reads build.toml from that revision (the PR branch), not the working tree. + if ref: + try: + raw = subprocess.run( + ["git", "show", f"{ref}:{kernel_name}/build.toml"], + capture_output=True, + text=True, + check=True, + ).stdout + except (FileNotFoundError, subprocess.CalledProcessError): + return None + config = tomllib.loads(raw) + else: + build_toml = Path(kernel_name) / "build.toml" + if not build_toml.exists(): + return None + with open(build_toml, "rb") as f: + config = tomllib.load(f) backends = config.get("general", {}).get("backends") if backends is None: backends = config.get("backends") @@ -109,8 +122,10 @@ def read_backends(kernel_name: str) -> list[str] | None: return None -def select_workflows(kernel_name: str, *, notes: list[str]) -> set[str]: - backends = read_backends(kernel_name) +def select_workflows( + kernel_name: str, *, notes: list[str], metadata_ref: str = "" +) -> set[str]: + backends = read_backends(kernel_name, metadata_ref) if backends is None: notes.append( f"Could not read backends for {kernel_name}, dispatching all workflows" @@ -232,9 +247,12 @@ def _plan_build_actions( head_sha: str, target_branch: str, upload: bool, + metadata_ref: str, ) -> None: - backends = read_backends(kernel_name) or [] - workflows = select_workflows(kernel_name, notes=plan.notes) + backends = read_backends(kernel_name, metadata_ref) or [] + workflows = select_workflows( + kernel_name, notes=plan.notes, metadata_ref=metadata_ref + ) plan.skipped = sorted(set(WORKFLOWS["build"]) - workflows) for workflow in sorted(workflows): @@ -294,6 +312,7 @@ def plan_dispatch( upload: bool = True, run_security: bool = False, security_only: bool = False, + metadata_ref: str = "", ) -> DispatchPlan: want_security = run_security or security_only plan = DispatchPlan(kernel_name=kernel_name, head_sha=head_sha) @@ -306,6 +325,7 @@ def plan_dispatch( mode=mode, repo_prefix=repo_prefix, dispatch_key_prefix=dispatch_key_prefix, + metadata_ref=metadata_ref, skip_build=skip_build, pr_number=pr_number, head_sha=head_sha, @@ -460,6 +480,7 @@ def dispatch( upload: bool = True, run_security: bool = False, security_only: bool = False, + metadata_ref: str = "", ) -> DispatchResult: if not security_only and (not kernel_name or not KERNEL_NAME_RE.match(kernel_name)): result = DispatchResult(kernel_name=kernel_name) @@ -481,6 +502,7 @@ def dispatch( upload=upload, run_security=run_security, security_only=security_only, + metadata_ref=metadata_ref, ) if dry_run: return _result_from_plan(plan) diff --git a/.github/scripts/pr_comment_kernel_bot.py b/.github/scripts/pr_comment_kernel_bot.py index 490fd7d2..4d4b1cb6 100644 --- a/.github/scripts/pr_comment_kernel_bot.py +++ b/.github/scripts/pr_comment_kernel_bot.py @@ -878,6 +878,8 @@ def main(*, dry_run: bool = False): # The audit is per-PR, so request it only once (on the first kernel). run_security=run_security and index == 0, dry_run=dry_run, + # Read backends from the PR commit (dry-run reads the working tree). + metadata_ref="" if dry_run else (pr_head_sha or ""), ) emit_dispatch_diagnostics(release_result, dry_run=dry_run) for wf, dk in release_result.dispatched: diff --git a/.github/scripts/test_dispatch.py b/.github/scripts/test_dispatch.py index d10c2e9c..380cf17b 100644 --- a/.github/scripts/test_dispatch.py +++ b/.github/scripts/test_dispatch.py @@ -1,5 +1,6 @@ import io import os +import subprocess import sys import unittest import urllib.error @@ -68,6 +69,31 @@ def test_security_only_plans_only_security(self): self.assertEqual(action.body["inputs"]["head_sha"], "deadbeef") +# Metadata read from a git ref (PR branch) instead of the working tree. +class MetadataRefTest(unittest.TestCase): + def test_read_backends_from_ref_uses_git_show(self): + run = mock.Mock(return_value=mock.Mock(stdout='backends = ["cuda", "rocm"]\n')) + with mock.patch.object(dispatch.subprocess, "run", run): + backends = dispatch.read_backends("somekernel", ref="abc123") + self.assertEqual(backends, ["cuda", "rocm"]) + self.assertEqual( + run.call_args.args[0], ["git", "show", "abc123:somekernel/build.toml"] + ) + + def test_read_backends_from_ref_missing_returns_none(self): + err = subprocess.CalledProcessError(128, ["git", "show"]) + with mock.patch.object(dispatch.subprocess, "run", side_effect=err): + self.assertIsNone(dispatch.read_backends("missing", ref="abc123")) + + def test_metadata_ref_routes_metal_less_kernel_without_mac_build(self): + # Regression: a cuda-only kernel read from the ref must not trigger build-mac.yaml. + run = mock.Mock(return_value=mock.Mock(stdout='backends = ["cuda"]\n')) + with mock.patch.object(dispatch.subprocess, "run", run): + plan = dispatch.plan_dispatch("newkernel", metadata_ref="prsha") + builds = _workflows([a for a in plan.actions if a.kind == "build"]) + self.assertEqual(builds, ["build.yaml"]) + + # Orchestration: kernel-name validation and the dry-run no-I/O contract. class DispatchOrchestratorTest(unittest.TestCase): def test_invalid_kernel_name_marks_all_builds_failed(self): diff --git a/.github/workflows/pr-comment-build.yaml b/.github/workflows/pr-comment-build.yaml index 7b13e799..93abeb9f 100644 --- a/.github/workflows/pr-comment-build.yaml +++ b/.github/workflows/pr-comment-build.yaml @@ -19,6 +19,9 @@ jobs: with: ref: ${{ github.event.repository.default_branch }} fetch-depth: 1 + - name: Fetch PR head for metadata + # Make the PR commit available so the bot can read its build.toml (data only). + run: git fetch --depth=1 origin "refs/pull/${{ github.event.issue.number }}/head" - name: Handle /kernel-bot command env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}