From eaa454f03eeca5400d21f8a99f297e721609d525 Mon Sep 17 00:00:00 2001 From: Chris Kearney <303316+iconidentify@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:59:24 -0700 Subject: [PATCH] ci: scope build reuse to non-PR runs; install kmod/zstd for package checks Two follow-ups from running the CI on the Namespace runner. Reuse (follow-up to #165): a reused kernel-package or kernel-verified artifact is now accepted only from a push, merge_group, schedule or workflow_dispatch run (REUSE_EVENTS), never a pull_request. A CI-only pull request from a writer has the same build key as the branch tip and could otherwise plant a package or verification record that a later queue or push run reuses before anyone reviews it. A comparison baseline (kernel-metadata) is only read, never substituted, so a pull_request run is still fine there (BASELINE_EVENTS). Package checks: inspect-package.py resolves module symbols with depmod and unpacks with tar --zstd, which the Namespace builder image does not ship. Install kmod and zstd in the package job when they are missing. Signed-off-by: Chris Kearney <303316+iconidentify@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 --- .github/workflows/kernel-ci-jobs.yml | 8 +++++ tools/aurora-ci/lookup-build.py | 37 +++++++++++++--------- tools/aurora-ci/tests/test_lookup_build.py | 35 ++++++++++++-------- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/.github/workflows/kernel-ci-jobs.yml b/.github/workflows/kernel-ci-jobs.yml index a1b63892d49e8f..f0890411a87da3 100644 --- a/.github/workflows/kernel-ci-jobs.yml +++ b/.github/workflows/kernel-ci-jobs.yml @@ -320,6 +320,14 @@ jobs: path: out run-id: ${{ env.REUSE_RUN }} github-token: ${{ github.token }} + # inspect-package.py resolves module symbols with depmod and unpacks + # with tar --zstd; the Namespace builder image ships neither. + - name: Ensure kmod and zstd for the package checks + run: | + if ! command -v depmod >/dev/null || ! command -v zstd >/dev/null; then + sudo apt-get update -q + sudo apt-get install -y -q --no-install-recommends kmod zstd + fi - name: Inspect the packages run: | python3 -I linux/tools/aurora-ci/inspect-package.py --packages out/packages \ diff --git a/tools/aurora-ci/lookup-build.py b/tools/aurora-ci/lookup-build.py index b058a006e01bd7..4d531b8b9ab3d9 100755 --- a/tools/aurora-ci/lookup-build.py +++ b/tools/aurora-ci/lookup-build.py @@ -20,8 +20,9 @@ This script computes the key, pins the omarchy-pkgs commit it was computed from, and looks those artifacts up through the Actions API. It accepts only -artifacts that this repository's kernel CI uploaded from its own branches, -never one from a fork's pull request (see TRUSTED_WORKFLOWS). +artifacts this repository's kernel CI uploaded (see TRUSTED_WORKFLOWS); +a reused package or verification record must also come from a non-pull-request +run (REUSE_EVENTS), while a comparison baseline may come from a pull request. """ import argparse @@ -84,7 +85,14 @@ def build_key(kernel_tree, recipe_trees): # from runs of this repository's kernel CI on this repository's own branches # and events. Pushing a branch here needs write access; a fork does not. TRUSTED_WORKFLOWS = {".github/workflows/kernel-ci.yml", ".github/workflows/kernel-ci-label.yml"} -TRUSTED_EVENTS = {"push", "merge_group", "schedule", "workflow_dispatch", "pull_request"} +# A package or a verification record is substituted into a later run instead +# of rebuilding/retesting, so it may only be reused from a run that is not a +# pull request: a writer's CI-only pull request has the same build key as the +# branch tip and could otherwise plant a package before anyone reviews it. +REUSE_EVENTS = {"push", "merge_group", "schedule", "workflow_dispatch"} +# A comparison baseline is only read, never substituted, so a pull request's +# metadata is fine as a baseline. +BASELINE_EVENTS = REUSE_EVENTS | {"pull_request"} class Api: @@ -105,19 +113,18 @@ def get(self, path, **query): with urllib.request.urlopen(request, timeout=60) as response: return json.load(response) - def trusted_run(self, run_id): - """Whether a run is this repository's kernel CI on one of its own - branches (or the queue, a schedule or a manual run), not a fork's.""" + def trusted_run(self, run_id, events): + """Whether a run is this repository's kernel CI, from one of the given + events, on this repository (not a fork).""" if run_id not in self._runs: - run = self.get(f"actions/runs/{run_id}") - self._runs[run_id] = ( - run.get("event") in TRUSTED_EVENTS + self._runs[run_id] = self.get(f"actions/runs/{run_id}") + run = self._runs[run_id] + return (run.get("event") in events and run.get("path") in TRUSTED_WORKFLOWS and (run.get("repository") or {}).get("id") == self.repository_id and (run.get("head_repository") or {}).get("id") == self.repository_id) - return self._runs[run_id] - def newest_artifact(self, name): + def newest_artifact(self, name, events): """Run ID of the newest unexpired artifact with this exact name that a trusted run uploaded, or ''.""" data = self.get("actions/artifacts", name=name, per_page=100) @@ -127,7 +134,7 @@ def newest_artifact(self, name): and a.get("workflow_run", {}).get("head_repository_id") == self.repository_id] live.sort(key=lambda a: a.get("created_at", ""), reverse=True) for artifact in live: - if self.trusted_run(artifact["workflow_run"]["id"]): + if self.trusted_run(artifact["workflow_run"]["id"], events): return str(artifact["workflow_run"]["id"]) return "" @@ -140,12 +147,12 @@ def lookup(api, kernel, recipe_repo, base_sha, force): out = {"key": key, "kernel_tree": kernel_tree, "recipe_commit": recipe_commit, "package_run": "", "verified_run": "", "base_tree": "", "baseline_run": ""} if not force: - out["package_run"] = api.newest_artifact(f"kernel-package-{key}") - out["verified_run"] = api.newest_artifact(f"kernel-verified-{key}") + out["package_run"] = api.newest_artifact(f"kernel-package-{key}", REUSE_EVENTS) + out["verified_run"] = api.newest_artifact(f"kernel-verified-{key}", REUSE_EVENTS) if base_sha: ensure_commit(kernel, base_sha) out["base_tree"] = kernel_id(kernel, base_sha) - out["baseline_run"] = api.newest_artifact(f"kernel-metadata-{out['base_tree']}") + out["baseline_run"] = api.newest_artifact(f"kernel-metadata-{out['base_tree']}", BASELINE_EVENTS) return out diff --git a/tools/aurora-ci/tests/test_lookup_build.py b/tools/aurora-ci/tests/test_lookup_build.py index 1d02646332563d..c706221e1dac43 100644 --- a/tools/aurora-ci/tests/test_lookup_build.py +++ b/tools/aurora-ci/tests/test_lookup_build.py @@ -8,8 +8,8 @@ class FakeApi: def __init__(self, artifacts=None): self.artifacts, self.asked = artifacts or {}, [] - def newest_artifact(self, name): - self.asked.append(name) + def newest_artifact(self, name, events): + self.asked.append((name, frozenset(events))) return self.artifacts.get(name, "") @@ -101,18 +101,16 @@ def test_newest_unexpired_artifact_from_a_trusted_run(self): api = self.api([artifact(1, "2026-10-20", expired=True), artifact(2, "2026-10-18"), artifact(3, "2026-10-19"), artifact(4, "2026-10-21", name="other")], {2: run(), 3: run("merge_group")}) - self.assertEqual(api.newest_artifact("kernel-package-x"), "3") - self.assertEqual(self.api([], {}).newest_artifact("kernel-package-x"), "") + self.assertEqual(api.newest_artifact("kernel-package-x", lookup.REUSE_EVENTS), "3") + self.assertEqual(self.api([], {}).newest_artifact("kernel-package-x", lookup.REUSE_EVENTS), "") def test_a_fork_run_is_never_reused(self): - # A fork's pull request uploads a newer artifact under the same name. api = self.api([artifact(2, "2026-10-18"), artifact(9, "2026-10-20", head_repo=2000)], {2: run(), 9: run("pull_request", head_repo=2000)}) - self.assertEqual(api.newest_artifact("kernel-package-x"), "2") + self.assertEqual(api.newest_artifact("kernel-package-x", lookup.BASELINE_EVENTS), "2") self.assertNotIn("actions/runs/9", api.calls) def test_the_run_itself_must_be_trusted(self): - # The artifact claims this repository, but the run says otherwise. cases = { "fork head": run("pull_request", head_repo=2000), "another workflow": run(path=".github/workflows/something-else.yml"), @@ -121,12 +119,21 @@ def test_the_run_itself_must_be_trusted(self): for name, untrusted in cases.items(): with self.subTest(name): api = self.api([artifact(5, "2026-10-20")], {5: untrusted}) - self.assertEqual(api.newest_artifact("kernel-package-x"), "") - - def test_branches_of_this_repository_and_every_trusted_event(self): - for event in sorted(lookup.TRUSTED_EVENTS): + self.assertEqual(api.newest_artifact("kernel-package-x", lookup.BASELINE_EVENTS), "") + + def test_package_and_verified_reuse_excludes_pull_requests(self): + # A writer's CI-only PR run uploads a package under the branch tip's key. + api = self.api([artifact(7, "2026-10-20")], {7: run("pull_request")}) + self.assertEqual(api.newest_artifact("kernel-package-x", lookup.REUSE_EVENTS), "", + "a pull-request run must not be reused as a package") + # The same PR run, as a comparison baseline, is acceptable. + api = self.api([artifact(7, "2026-10-20", name="kernel-metadata-x")], {7: run("pull_request")}) + self.assertEqual(api.newest_artifact("kernel-metadata-x", lookup.BASELINE_EVENTS), "7") + + def test_reuse_events_cover_the_non_pr_triggers(self): + for event in sorted(lookup.REUSE_EVENTS): with self.subTest(event): api = self.api([artifact(5, "2026-10-20")], {5: run(event)}) - self.assertEqual(api.newest_artifact("kernel-package-x"), "5") - api = self.api([artifact(6, "2026-10-20")], {6: run("pull_request", ".github/workflows/kernel-ci-label.yml")}) - self.assertEqual(api.newest_artifact("kernel-package-x"), "6") + self.assertEqual(api.newest_artifact("kernel-package-x", lookup.REUSE_EVENTS), "5") + self.assertNotIn("pull_request", lookup.REUSE_EVENTS) + self.assertIn("pull_request", lookup.BASELINE_EVENTS)