mirror of
https://github.com/unslothai/unsloth.git
synced 2026-08-26 09:13:05 +00:00
Stop running every macOS workflow on every commit to main (#9174)
* Stop running every macOS workflow on every commit to main
GitHub caps macOS at five concurrent jobs account-wide, across every repository,
which makes a macOS runner slot the scarcest thing in this repo's CI. Four
workflows scoped their pull_request trigger carefully and then declared
`push: branches: [main]` with no paths filter at all, so after merge they ran
on everything.
Commit 6371f46a changes README.md and nothing else. It started Mac Studio GGUF
CI, Mac Studio UI + API + Update CI, Mac Studio Install Matrix CI and Unsloth
Tauri CI: seven macOS legs, 40 percent over the entire account cap, for a
documentation typo. Every one of them then queued behind the others.
Replaying each workflow's own pull_request filter against the last 40 commits
to main puts the cost at 280 macOS job-slots today against 94 with the filters
mirrored, and drops the peak on a single commit from 7 to 3, under the cap.
Most of that is one workflow: Mac Studio Install Matrix fired on 40 of 40
sampled commits and would fire on none of them, at 4 slots each, and it also
carries the worst queue in the set at a 200 minute median for a 2m38s job.
No coverage changes. Each push list is its own pull_request list copied
verbatim, which is the same question asked twice, and the post-merge backstop
still runs whenever a commit can affect what the workflow tests.
clean-machine-install-ci.yml and mlx-ci.yml already do exactly this and say
why; this brings the other four in line.
tests/studio/test_macos_slots_per_commit.py asserts no macOS workflow has an
unfiltered push trigger, that the two filters do not drift apart in either
direction, and that a README-only commit starts nothing. Its first cut
reported workflow-trigger-lint and studio-inference-smoke as macOS workflows,
because it scanned whole jobs and both merely mention macOS in a step; it now
reads runs-on and resolves matrix expressions. Three mutations verified red.
* List the helpers these workflows execute, now that the trigger is scoped
Scoping a trigger is only safe if its path list is complete, and these were
not. Five helper scripts and one auditor are invoked by name from a run step
and matched no pattern in either list:
studio-mac-inference-smoke assert-llama-loads.sh, hf-download-with-retry.sh,
studio_smoke/multi_turn_chat.py
studio-mac-ui-smoke assert-llama-loads.sh, hf-download-with-retry.sh
studio-tauri-smoke scripts/lockfile_supply_chain_audit.py
The gap predates this PR, but it was invisible while the push trigger was
unfiltered, because every commit ran everything after merge. Narrowing the
trigger is what turns it into a real hole: editing assert-llama-loads.sh would
stop running the workflows that assert with it.
studio-tauri-smoke also gains studio/package.json and studio/package-lock.json.
Its Linux job installs and runs the Tauri CLI out of that package root
(npm install --prefix studio, npx --prefix studio), so those manifests decide
what it builds with. They are listed by hand rather than found, because that
dependency does not go through a file path in a run step.
test_every_helper_a_workflow_executes_is_in_its_trigger asserts the rest, from
the run bodies rather than a fixed list, for both triggers. Verified red by
dropping assert-llama-loads.sh again.
* Cover the install matrix's transitive inputs, and run the guard on workflow-only PRs
studio-mac-install-matrix listed install.sh and studio/setup.sh but not what
they reach: setup.sh:1444 runs install_python_stack.py, which imports
install_manifest.py, and install_llama_prebuilt.py imports prebuilt_core.py at
line 55. A commit touching only one of those changed exactly what this matrix
asserts on and did not start it.
Not studio/backend/requirements/** or pyproject.toml, which
install_python_stack.py also consumes. Generic install breakage on macOS is
clean-machine-install-ci's job, and it lists both on either trigger with seven
macOS legs; duplicating them here would re-spend the slots this filter exists
to save. That is a judgement rather than an oversight, so it is written in the
filter next to the paths it excludes.
test_a_listed_python_input_brings_its_sibling_imports asserts the mechanical
half: a listed .py must bring the modules it imports from its own directory.
The full transitive closure of an installer is most of the repo, and chasing it
is how a filter stops saving anything, so anything deeper stays hand-listed
with a reason. The guard found install_manifest.py on its first run, which is
one hop further out than the report that prompted it.
The guard itself is now wired into workflow-trigger-lint, the one job with no
paths filter. Every way it can regress is a workflow-only edit, which no other
job matches, so it would otherwise have been collected first by Backend CI's
unfiltered push -- after the change had merged.
This commit is contained in:
parent
3bf9bff5dd
commit
fc325f431a
6 changed files with 443 additions and 0 deletions
33
.github/workflows/studio-mac-inference-smoke.yml
vendored
33
.github/workflows/studio-mac-inference-smoke.yml
vendored
|
|
@ -48,8 +48,41 @@ on:
|
|||
# The install step in this workflow is `uses:` on that composite action,
|
||||
# so an edit to the action changes what this workflow actually runs.
|
||||
- '.github/actions/install-unsloth-local/action.yml'
|
||||
# Executed directly by this workflow: the load assertion, every model
|
||||
# download, and the multi-turn chat driver. A change to any of them changes
|
||||
# what this workflow actually runs.
|
||||
- '.github/scripts/assert-llama-loads.sh'
|
||||
- '.github/scripts/hf-download-with-retry.sh'
|
||||
- '.github/scripts/studio_smoke/multi_turn_chat.py'
|
||||
push:
|
||||
branches: [main]
|
||||
# Same list as the PR filter. Without it this workflow ran on EVERY commit to
|
||||
# main: a README-only commit fired all four unfiltered macOS workflows, seven
|
||||
# macOS legs, against an account-wide cap of five concurrent macOS jobs. The
|
||||
# post-merge backstop is unchanged, because a commit that cannot touch what
|
||||
# this workflow tests has nothing here to back up.
|
||||
# clean-machine-install-ci.yml and mlx-ci.yml already do exactly this.
|
||||
paths:
|
||||
- 'studio/**'
|
||||
- 'unsloth/**'
|
||||
- 'unsloth_cli/**'
|
||||
- 'install.sh'
|
||||
- 'pyproject.toml'
|
||||
- '.github/workflows/studio-mac-inference-smoke.yml'
|
||||
# Every server boot in this workflow shells out to that script, so an edit
|
||||
# to it changes what this workflow actually runs.
|
||||
- '.github/scripts/boot-studio-api-only.sh'
|
||||
# Same for the /api/health wait that runs after a boot.
|
||||
- '.github/scripts/wait-for-health.sh'
|
||||
# The install step in this workflow is `uses:` on that composite action,
|
||||
# so an edit to the action changes what this workflow actually runs.
|
||||
- '.github/actions/install-unsloth-local/action.yml'
|
||||
# Executed directly by this workflow: the load assertion, every model
|
||||
# download, and the multi-turn chat driver. A change to any of them changes
|
||||
# what this workflow actually runs.
|
||||
- '.github/scripts/assert-llama-loads.sh'
|
||||
- '.github/scripts/hf-download-with-retry.sh'
|
||||
- '.github/scripts/studio_smoke/multi_turn_chat.py'
|
||||
# Manual trigger for pre-warming model caches on main, or re-running
|
||||
# against an arbitrary branch without pushing a no-op commit.
|
||||
workflow_dispatch:
|
||||
|
|
|
|||
43
.github/workflows/studio-mac-install-matrix.yml
vendored
43
.github/workflows/studio-mac-install-matrix.yml
vendored
|
|
@ -19,8 +19,51 @@ on:
|
|||
# The install step in this workflow is `uses:` on that composite action,
|
||||
# so an edit to the action changes what this workflow actually runs.
|
||||
- '.github/actions/install-unsloth-local/action.yml'
|
||||
# Reached transitively by the install this workflow asserts on:
|
||||
# install.sh selects studio/setup.sh, which runs install_python_stack.py
|
||||
# (setup.sh:1444), and install_llama_prebuilt.py imports prebuilt_core
|
||||
# (install_llama_prebuilt.py:55).
|
||||
#
|
||||
# Deliberately NOT studio/backend/requirements/** or pyproject.toml, which
|
||||
# install_python_stack.py also consumes: generic install breakage on macOS is
|
||||
# clean-machine-install-ci's job and it lists both on either trigger with 7
|
||||
# macOS legs. Duplicating them here would re-spend the slots this filter saves.
|
||||
- 'studio/install_python_stack.py'
|
||||
- 'studio/prebuilt_core.py'
|
||||
# install_python_stack.py imports this; interrupted-install-ci.yml already
|
||||
# lists it for the same reason.
|
||||
- 'studio/install_manifest.py'
|
||||
push:
|
||||
branches: [main]
|
||||
# Same list as the PR filter. Without it this workflow ran on EVERY commit to
|
||||
# main: a README-only commit fired all four unfiltered macOS workflows, seven
|
||||
# macOS legs, against an account-wide cap of five concurrent macOS jobs. The
|
||||
# post-merge backstop is unchanged, because a commit that cannot touch what
|
||||
# this workflow tests has nothing here to back up.
|
||||
# clean-machine-install-ci.yml and mlx-ci.yml already do exactly this.
|
||||
paths:
|
||||
- 'studio/install_llama_prebuilt.py'
|
||||
- 'studio/setup.sh'
|
||||
- 'install.sh'
|
||||
- '.github/scripts/assert-llama-loads.sh'
|
||||
- '.github/workflows/studio-mac-install-matrix.yml'
|
||||
# The install step in this workflow is `uses:` on that composite action,
|
||||
# so an edit to the action changes what this workflow actually runs.
|
||||
- '.github/actions/install-unsloth-local/action.yml'
|
||||
# Reached transitively by the install this workflow asserts on:
|
||||
# install.sh selects studio/setup.sh, which runs install_python_stack.py
|
||||
# (setup.sh:1444), and install_llama_prebuilt.py imports prebuilt_core
|
||||
# (install_llama_prebuilt.py:55).
|
||||
#
|
||||
# Deliberately NOT studio/backend/requirements/** or pyproject.toml, which
|
||||
# install_python_stack.py also consumes: generic install breakage on macOS is
|
||||
# clean-machine-install-ci's job and it lists both on either trigger with 7
|
||||
# macOS legs. Duplicating them here would re-spend the slots this filter saves.
|
||||
- 'studio/install_python_stack.py'
|
||||
- 'studio/prebuilt_core.py'
|
||||
# install_python_stack.py imports this; interrupted-install-ci.yml already
|
||||
# lists it for the same reason.
|
||||
- 'studio/install_manifest.py'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
|
|
|||
38
.github/workflows/studio-mac-ui-smoke.yml
vendored
38
.github/workflows/studio-mac-ui-smoke.yml
vendored
|
|
@ -50,8 +50,46 @@ on:
|
|||
# No new entries needed for the absorbed API phase: every path
|
||||
# studio-mac-api-smoke.yml watched is already covered above, and
|
||||
# tests/studio/** covers studio_api_smoke.py itself.
|
||||
# Executed directly by this workflow: the load assertion and every model
|
||||
# download.
|
||||
- '.github/scripts/assert-llama-loads.sh'
|
||||
- '.github/scripts/hf-download-with-retry.sh'
|
||||
push:
|
||||
branches: [main]
|
||||
# Same list as the PR filter. Without it this workflow ran on EVERY commit to
|
||||
# main: a README-only commit fired all four unfiltered macOS workflows, seven
|
||||
# macOS legs, against an account-wide cap of five concurrent macOS jobs. The
|
||||
# post-merge backstop is unchanged, because a commit that cannot touch what
|
||||
# this workflow tests has nothing here to back up.
|
||||
# clean-machine-install-ci.yml and mlx-ci.yml already do exactly this.
|
||||
paths:
|
||||
- 'studio/**'
|
||||
- 'unsloth/**'
|
||||
- 'unsloth_cli/**'
|
||||
- 'install.sh'
|
||||
- 'pyproject.toml'
|
||||
- 'tests/studio/**'
|
||||
- '.github/scripts/run-studio-permission-browser.sh'
|
||||
- '.github/scripts/run-studio-indicator-browser.sh'
|
||||
- '.github/workflows/studio-mac-ui-smoke.yml'
|
||||
# Every server boot in this workflow shells out to that script, so an edit
|
||||
# to it changes what this workflow actually runs.
|
||||
- '.github/scripts/boot-studio-api-only.sh'
|
||||
# Same for the /api/health wait that runs after a boot.
|
||||
- '.github/scripts/wait-for-health.sh'
|
||||
# The install step in this workflow is `uses:` on that composite action,
|
||||
# so an edit to the action changes what this workflow actually runs.
|
||||
- '.github/actions/install-unsloth-local/action.yml'
|
||||
# The absorbed update phase round-trips scripts/uninstall.sh, which none
|
||||
# of the entries above cover.
|
||||
- 'scripts/uninstall.sh'
|
||||
# No new entries needed for the absorbed API phase: every path
|
||||
# studio-mac-api-smoke.yml watched is already covered above, and
|
||||
# tests/studio/** covers studio_api_smoke.py itself.
|
||||
# Executed directly by this workflow: the load assertion and every model
|
||||
# download.
|
||||
- '.github/scripts/assert-llama-loads.sh'
|
||||
- '.github/scripts/hf-download-with-retry.sh'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
|
|
|
|||
27
.github/workflows/studio-tauri-smoke.yml
vendored
27
.github/workflows/studio-tauri-smoke.yml
vendored
|
|
@ -25,8 +25,35 @@ on:
|
|||
# `unsloth studio` -- include unsloth_cli in the trigger set.
|
||||
- 'unsloth_cli/**'
|
||||
- '.github/workflows/studio-tauri-smoke.yml'
|
||||
# The Linux job installs and runs the Tauri CLI out of the studio/ package
|
||||
# root (`npm install --prefix studio`, `npx --prefix studio`), so these
|
||||
# manifests decide what it builds with.
|
||||
- 'studio/package.json'
|
||||
- 'studio/package-lock.json'
|
||||
# Run as a step of the Linux job.
|
||||
- 'scripts/lockfile_supply_chain_audit.py'
|
||||
push:
|
||||
branches: [main]
|
||||
# Same list as the PR filter. Without it this workflow ran on EVERY commit to
|
||||
# main: a README-only commit fired all four unfiltered macOS workflows, seven
|
||||
# macOS legs, against an account-wide cap of five concurrent macOS jobs. The
|
||||
# post-merge backstop is unchanged, because a commit that cannot touch what
|
||||
# this workflow tests has nothing here to back up.
|
||||
# clean-machine-install-ci.yml and mlx-ci.yml already do exactly this.
|
||||
paths:
|
||||
- 'studio/frontend/**'
|
||||
- 'studio/src-tauri/**'
|
||||
# CLI rename / signature change can break Tauri's spawned
|
||||
# `unsloth studio` -- include unsloth_cli in the trigger set.
|
||||
- 'unsloth_cli/**'
|
||||
- '.github/workflows/studio-tauri-smoke.yml'
|
||||
# The Linux job installs and runs the Tauri CLI out of the studio/ package
|
||||
# root (`npm install --prefix studio`, `npx --prefix studio`), so these
|
||||
# manifests decide what it builds with.
|
||||
- 'studio/package.json'
|
||||
- 'studio/package-lock.json'
|
||||
# Run as a step of the Linux job.
|
||||
- 'scripts/lockfile_supply_chain_audit.py'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.ref == 'refs/heads/main' && github.sha || '' }}
|
||||
|
|
|
|||
9
.github/workflows/workflow-trigger-lint.yml
vendored
9
.github/workflows/workflow-trigger-lint.yml
vendored
|
|
@ -127,3 +127,12 @@ jobs:
|
|||
# exists to reject.
|
||||
- name: The host-offload opt-out is still macOS-only
|
||||
run: python3 -m pytest tests/studio/test_mac_host_offload_optin.py -q
|
||||
|
||||
# Same reason again, and this one guards THIS kind of file specifically. The test
|
||||
# asserts that no macOS workflow has an unfiltered push trigger, and that each
|
||||
# trigger lists the helpers its workflow executes. Every way it can regress is a
|
||||
# workflow-only edit, which no other job's paths filter matches, so without this
|
||||
# step the guard would first be collected by Backend CI's unfiltered push -- after
|
||||
# the change had already merged.
|
||||
- name: macOS workflows still do not run on every commit
|
||||
run: python3 -m pytest tests/studio/test_macos_slots_per_commit.py -q
|
||||
|
|
|
|||
293
tests/studio/test_macos_slots_per_commit.py
Normal file
293
tests/studio/test_macos_slots_per_commit.py
Normal file
|
|
@ -0,0 +1,293 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"""No macOS workflow may run on every commit to main.
|
||||
|
||||
GitHub caps macOS at **five concurrent jobs account-wide** -- across every repository,
|
||||
on Free, Pro and Team alike. That makes a macOS runner slot the scarcest resource in this
|
||||
repo's CI by a wide margin, and it is the reason macOS queue times dominate: measured over
|
||||
the last 20 main runs, `studio-mac-ui-smoke` waited a median of 245 minutes to execute for
|
||||
21, and `Unsloth Tauri CI :: Rust unit tests (macos)` waited a median of 270 minutes to run
|
||||
for 3.
|
||||
|
||||
Four workflows used to declare `push: branches: [main]` with no `paths:` filter while their
|
||||
`pull_request` trigger was carefully scoped. The effect was invisible on a PR and only
|
||||
appeared after merge: commit 6371f46a changes README.md and nothing else, and it started
|
||||
`Mac Studio GGUF CI`, `Mac Studio UI + API + Update CI`, `Mac Studio Install Matrix CI` and
|
||||
`Unsloth Tauri CI` -- seven macOS legs, 40% over the entire account cap, for a
|
||||
documentation typo. Every one of those runs then queued behind the others.
|
||||
|
||||
`clean-machine-install-ci.yml` and `mlx-ci.yml` already got this right and say why:
|
||||
"Same list as the PR filter: without it a direct push to main touching any of these skipped
|
||||
the workflow and the post-merge backstop never happened." This asserts the rest match.
|
||||
|
||||
The failure is silent in both directions, which is why it is a test rather than a review
|
||||
note: an unfiltered push trigger costs nothing on the PR that introduces it, and the cost
|
||||
lands on unrelated commits weeks later as queue time nobody attributes to it.
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
WORKFLOWS = REPO / ".github" / "workflows"
|
||||
|
||||
MACOS = re.compile(r"macos[-\w.]*", re.I)
|
||||
|
||||
|
||||
def _on(doc):
|
||||
"""The `on:` mapping, which PyYAML parses as the boolean True."""
|
||||
return doc.get(True) if True in doc else doc.get("on")
|
||||
|
||||
|
||||
def _job_runs_on_macos(job) -> bool:
|
||||
"""Whether ``job`` schedules a macOS runner.
|
||||
|
||||
Reads `runs-on` and, when that is a matrix expression, the matrix values it selects
|
||||
from. Scanning the whole job instead was the first cut and it over-matched badly:
|
||||
`workflow-trigger-lint.yml` and `studio-inference-smoke.yml` both have ubuntu-only jobs
|
||||
that merely NAME macOS somewhere in a step, and both were reported as macOS workflows.
|
||||
A guard about runner slots has to read what actually allocates a runner.
|
||||
"""
|
||||
runs_on = job.get("runs-on")
|
||||
values = runs_on if isinstance(runs_on, list) else [runs_on]
|
||||
for value in values:
|
||||
if not isinstance(value, str):
|
||||
continue
|
||||
if MACOS.search(value):
|
||||
return True
|
||||
# `runs-on: ${{ matrix.os }}` -- resolve against the matrix it names.
|
||||
for key in re.findall(r"matrix\.([\w-]+)", value):
|
||||
matrix = (job.get("strategy") or {}).get("matrix") or {}
|
||||
candidates = list(matrix.get(key) or [])
|
||||
for entry in matrix.get("include") or []:
|
||||
if isinstance(entry, dict) and key in entry:
|
||||
candidates.append(entry[key])
|
||||
if any(isinstance(c, str) and MACOS.search(c) for c in candidates):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _macos_workflows():
|
||||
"""Workflows with at least one macOS leg."""
|
||||
for path in sorted(WORKFLOWS.glob("*.yml")):
|
||||
text = path.read_text(encoding = "utf-8")
|
||||
doc = yaml.safe_load(text)
|
||||
if not isinstance(doc, dict) or not isinstance(doc.get("jobs"), dict):
|
||||
continue
|
||||
if any(_job_runs_on_macos(j) for j in doc["jobs"].values() if isinstance(j, dict)):
|
||||
yield path.name, doc, text
|
||||
|
||||
|
||||
def test_the_scan_finds_the_macos_workflows_it_claims_to():
|
||||
"""A scan that matched nothing would pass every check below."""
|
||||
names = {name for name, _, _ in _macos_workflows()}
|
||||
for expected in (
|
||||
"studio-mac-ui-smoke.yml",
|
||||
"studio-mac-inference-smoke.yml",
|
||||
"studio-mac-install-matrix.yml",
|
||||
"studio-tauri-smoke.yml",
|
||||
"mlx-ci.yml",
|
||||
"clean-machine-install-ci.yml",
|
||||
):
|
||||
assert expected in names, f"{expected} is no longer detected as having a macOS leg"
|
||||
|
||||
|
||||
def test_no_macos_workflow_runs_on_every_push_to_main():
|
||||
offenders = []
|
||||
for name, doc, _ in _macos_workflows():
|
||||
push = (_on(doc) or {}).get("push")
|
||||
if not isinstance(push, dict):
|
||||
continue # no push trigger at all is the strongest form of this
|
||||
if not push.get("paths") and not push.get("paths-ignore"):
|
||||
offenders.append(name)
|
||||
assert not offenders, (
|
||||
f"these workflows run macOS jobs on EVERY commit to main: {offenders}. macOS is "
|
||||
f"capped at five concurrent jobs account-wide, so an unfiltered push trigger here "
|
||||
f"oversubscribes the whole account on commits that cannot affect what it tests. "
|
||||
f"Mirror the pull_request paths onto push, as clean-machine-install-ci.yml and "
|
||||
f"mlx-ci.yml do."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
[
|
||||
"studio-mac-ui-smoke.yml",
|
||||
"studio-mac-inference-smoke.yml",
|
||||
"studio-mac-install-matrix.yml",
|
||||
"studio-tauri-smoke.yml",
|
||||
"clean-machine-install-ci.yml",
|
||||
"mlx-ci.yml",
|
||||
],
|
||||
)
|
||||
def test_the_push_filter_matches_the_pull_request_filter(name):
|
||||
"""Narrower on push than on PR would drop the post-merge backstop.
|
||||
|
||||
The two lists are the same question asked twice -- "could this commit break this
|
||||
workflow" -- so they drifting apart is always a bug, in whichever direction. A push
|
||||
list that is a strict subset silently stops testing something after merge that was
|
||||
tested before it, which is the more dangerous direction and the harder to notice.
|
||||
"""
|
||||
doc = yaml.safe_load((WORKFLOWS / name).read_text(encoding = "utf-8"))
|
||||
on = _on(doc) or {}
|
||||
pr_paths = (on.get("pull_request") or {}).get("paths")
|
||||
push_paths = (on.get("push") or {}).get("paths")
|
||||
assert pr_paths, f"{name} no longer scopes its pull_request trigger"
|
||||
assert push_paths, f"{name} no longer scopes its push trigger"
|
||||
assert sorted(pr_paths) == sorted(push_paths), (
|
||||
f"{name}: the push and pull_request path filters have drifted apart.\n"
|
||||
f" only on pull_request: {sorted(set(pr_paths) - set(push_paths))}\n"
|
||||
f" only on push: {sorted(set(push_paths) - set(pr_paths))}"
|
||||
)
|
||||
|
||||
|
||||
def _covered(path: str, patterns) -> bool:
|
||||
"""Whether ``path`` matches any Actions path filter in ``patterns``."""
|
||||
import fnmatch
|
||||
|
||||
for pattern in patterns:
|
||||
if pattern == path or fnmatch.fnmatch(path, pattern):
|
||||
return True
|
||||
if pattern.endswith("/**") and path.startswith(pattern[:-2]):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
[
|
||||
"studio-mac-ui-smoke.yml",
|
||||
"studio-mac-inference-smoke.yml",
|
||||
"studio-mac-install-matrix.yml",
|
||||
"studio-tauri-smoke.yml",
|
||||
],
|
||||
)
|
||||
def test_every_helper_a_workflow_executes_is_in_its_trigger(name):
|
||||
"""A scoped trigger must list the checked-in files the workflow actually runs.
|
||||
|
||||
Scoping a trigger is only safe if the list is complete, and these lists were not: five
|
||||
helper scripts and one auditor were executed by name and matched no pattern. While the
|
||||
push trigger was unfiltered that gap was invisible, because every commit ran everything
|
||||
after merge; narrowing the trigger is what turns it into a real hole, where editing
|
||||
`assert-llama-loads.sh` stops running the workflow that asserts with it.
|
||||
|
||||
Matched by looking for the path in a `run:` body, which is how every one of these is
|
||||
invoked. That deliberately says nothing about files a workflow depends on more
|
||||
loosely -- `studio/package.json` reaches the Tauri build through
|
||||
`npm install --prefix studio` and is listed by hand, not found here.
|
||||
"""
|
||||
doc = yaml.safe_load((WORKFLOWS / name).read_text(encoding = "utf-8"))
|
||||
on = _on(doc) or {}
|
||||
runs = "\n".join(
|
||||
str(step.get("run", ""))
|
||||
for job in doc["jobs"].values()
|
||||
if isinstance(job, dict)
|
||||
for step in job.get("steps") or []
|
||||
if isinstance(step, dict)
|
||||
)
|
||||
referenced = {
|
||||
ref.strip()
|
||||
for pattern in (r"\.github/scripts/[\w./-]+", r"(?:^|\s)scripts/[\w./-]+")
|
||||
for ref in re.findall(pattern, runs, re.M)
|
||||
}
|
||||
existing = sorted(r for r in referenced if (REPO / r).is_file())
|
||||
assert existing, f"{name} appears to execute no checked-in helper; the scan is wrong"
|
||||
|
||||
for trigger in ("pull_request", "push"):
|
||||
patterns = (on.get(trigger) or {}).get("paths") or []
|
||||
missing = [r for r in existing if not _covered(r, patterns)]
|
||||
assert not missing, (
|
||||
f"{name}: these files are executed by the workflow but match no {trigger} path "
|
||||
f"filter, so editing one of them does not run the workflow that uses it: "
|
||||
f"{missing}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
[
|
||||
"studio-mac-ui-smoke.yml",
|
||||
"studio-mac-inference-smoke.yml",
|
||||
"studio-mac-install-matrix.yml",
|
||||
"studio-tauri-smoke.yml",
|
||||
],
|
||||
)
|
||||
def test_a_listed_python_input_brings_its_sibling_imports(name):
|
||||
"""Listing a script but not the module it imports leaves half a dependency in the filter.
|
||||
|
||||
`studio/install_llama_prebuilt.py` was listed; `studio/prebuilt_core.py`, which it
|
||||
imports at line 55, was not. Editing only the latter changed exactly what the install
|
||||
matrix asserts on and did not run it.
|
||||
|
||||
Scoped to same-directory imports on purpose. The full transitive closure of an
|
||||
installer is most of the repo, and chasing it would put `pyproject.toml` and every
|
||||
requirements file into a macOS trigger, which is how a filter stops saving anything.
|
||||
Where a deeper dependency matters it is listed by hand with a comment saying why; this
|
||||
covers the one case that is mechanical and therefore easy to forget.
|
||||
"""
|
||||
import ast
|
||||
|
||||
doc = yaml.safe_load((WORKFLOWS / name).read_text(encoding = "utf-8"))
|
||||
on = _on(doc) or {}
|
||||
patterns = (on.get("pull_request") or {}).get("paths") or []
|
||||
|
||||
missing = []
|
||||
for pattern in patterns:
|
||||
source = REPO / pattern
|
||||
if not (source.is_file() and source.suffix == ".py"):
|
||||
continue
|
||||
tree = ast.parse(source.read_text(encoding = "utf-8", errors = "replace"))
|
||||
names = set()
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.Import):
|
||||
names.update(a.name.split(".")[0] for a in node.names)
|
||||
elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
|
||||
names.add(node.module.split(".")[0])
|
||||
for module in sorted(names):
|
||||
sibling = source.parent / f"{module}.py"
|
||||
if not sibling.is_file():
|
||||
continue # stdlib or third-party, not a checked-in sibling
|
||||
rel = sibling.relative_to(REPO).as_posix()
|
||||
if not _covered(rel, patterns):
|
||||
missing.append(f"{rel} (imported by {pattern})")
|
||||
assert not missing, (
|
||||
f"{name} lists a Python input but not a module it imports from the same directory, "
|
||||
f"so editing that module does not run the workflow that depends on it: {missing}"
|
||||
)
|
||||
|
||||
|
||||
def test_a_commit_that_touches_nothing_relevant_starts_no_macos_job():
|
||||
"""The property the whole change exists for, checked against a concrete commit.
|
||||
|
||||
README-only is not a hypothetical: commit 6371f46a is exactly that, and it started
|
||||
four macOS workflows. Matching is by the same prefix/glob rules Actions uses, kept
|
||||
simple deliberately -- every filter in these files is either a literal, a `dir/**`
|
||||
prefix or a single `*` glob, and this asserts that stays true so the simple matcher
|
||||
cannot quietly become wrong.
|
||||
"""
|
||||
import fnmatch
|
||||
|
||||
changed = ["README.md"]
|
||||
triggered = []
|
||||
for name, doc, _ in _macos_workflows():
|
||||
push = (_on(doc) or {}).get("push")
|
||||
if not isinstance(push, dict):
|
||||
continue
|
||||
for pattern in push.get("paths") or []:
|
||||
assert "!" not in pattern, (
|
||||
f"{name} uses a negated push path ({pattern!r}); this matcher does not "
|
||||
f"model negation, so extend it before relying on this test"
|
||||
)
|
||||
for path in changed:
|
||||
if fnmatch.fnmatch(path, pattern) or (
|
||||
pattern.endswith("/**") and path.startswith(pattern[:-2])
|
||||
):
|
||||
triggered.append(f"{name} via {pattern!r}")
|
||||
assert not triggered, (
|
||||
f"a README-only commit still starts macOS jobs: {triggered}. That was the "
|
||||
f"original symptom: seven macOS legs against a five-slot cap for a docs typo."
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue