All checks were successful
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 1h4m33s
standards workspaces build + test / aac-rs (pull_request) Successful in 15m41s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 5m23s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 4m7s
standards workspaces build + test / map-rs (pull_request) Successful in 5m41s
standards workspaces build + test / mesh-rs (pull_request) Successful in 2m16s
standards workspaces build + test / openpay (pull_request) Successful in 13m43s
standards workspaces build + test / arl-rs (pull_request) Successful in 1m13s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m55s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m30s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m13s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m17s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m18s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m16s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m5s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m37s
wai-pack / wai-pack · does this change touch it (pull_request) Successful in 4s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 4s
wai-pack / wai-pack · linux-arm64 (pull_request) Successful in 2m59s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 2m47s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 4m54s
wai-pack / wai-pack · linux (pull_request) Successful in 1m48s
wai-pack / wai-pack · independent verifier (Python) (pull_request) Successful in 3m0s
wai-pack / wai-pack · the floor through a media prober (pull_request) Successful in 24s
wai-pack / wai-pack · dependency licences (pull_request) Successful in 2m0s
wai-pack / wai-pack · macos (pull_request) Successful in 2m11s
Forgejo matches a workflow's `paths:` filter against the **push delta**, not the pull request's diff. A rebase moves a branch across every commit main has gained, so its delta touches nearly every path and every path-filtered workflow fires. Measured on this repo, as a clean natural experiment — five PRs, all changing only `jcp/` or `mesh/`: | PRs | how pushed | workflows triggered | |---|---|---| | #189, #190 | fresh branch, one push | **2** | | #165, #177, #183 | force-push after rebase | **15** | Fifteen includes JPEG-AI whole decode, `wai-pack` on three platforms, and cross-architecture byte-equality on three — none of which a change to `jcp/jcp-rs/crates/jcp-grant/src/checkpoint.rs` can affect. Rebasing three PRs put the queue 65 runs deep behind one macOS runner, which delays everyone's real jobs, and there is no way to rebase without it: a merge of main into the branch has the same delta. So the `paths:` filters still gate the *trigger*, and a new `relevant` job gates the *work*, from the pull request's own diff. It reads the path list out of the workflow file it guards, so there is one list rather than a third copy to drift from the two `on:` blocks. On a push to main there is no pull-request context, the shas are empty, and the gate answers `true` — main runs everything, as before. **It fails open, by construction.** A gate that decides whether tests run is load-bearing in the dangerous direction: a wrong `false` silently skips a whole workflow, which is the "passes but tested nothing" failure. Every way the script can fail to decide — a missing workflow file, a filter it cannot parse, an empty or unresolvable sha, too few arguments, an empty diff — prints `true`. The self-test pins all six of those, both directions of the real decision, and that `**` spans a path separator while `*` does not. Writing that self-test caught one of its own cases: the single-star case first created a top-level `.md` alongside the nested one, so the glob matched legitimately and the case proved nothing. It now changes only the nested file, and asserts separately that the glob does match at its own level — so it cannot pass by never matching anything. Applied to `wai-pack.yml` alone, not all eighteen. What a *skipped* job reports as a commit status on this forge is the open question, and a required context that never reports would make PRs unmergeable. That failure is at least loud rather than silent, but it is worth learning on the most expensive workflow — six job instances across three platforms — before changing the rest. Validated against real branches first: every WAI workflow answers `false` for #165's jcp-only diff and `true` for a WAI branch, and `standards-build.yml` correctly answers `true` for #165 because it builds every workspace. The hygiene workflow runs the self-test, so the gate cannot regress silently, and its own `paths:` filter now lists the scripts it checks. Co-Authored-By: Claude Opus 5 <[email protected]>
104 lines
3.6 KiB
Python
Executable file
104 lines
3.6 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
"""Does this pull request's own diff touch any path its workflow filters on?
|
|
|
|
Forgejo matches a workflow's `paths:` filter against the **push delta**, not the
|
|
pull request's diff. A rebase moves a branch across every commit main has
|
|
gained, so its push delta touches nearly every path and every path-filtered
|
|
workflow fires. Measured on this repo: a jcp-only change pushed to a fresh
|
|
branch ran 2 workflows; the same change force-pushed after a rebase ran 15,
|
|
including JPEG-AI whole decode and wai-pack on three platforms. That is what
|
|
fills the queue, and it delays everyone's real jobs behind irrelevant ones.
|
|
|
|
This reads the path list out of the workflow file itself — one source of truth,
|
|
rather than a third copy of the list to drift from the two `on:` blocks — and
|
|
compares it against `git diff --name-only BASE...HEAD`.
|
|
|
|
pr-touches-paths.py <workflow.yml> <base-sha> <head-sha>
|
|
|
|
Prints `true` or `false`. **Fails open**: anything it cannot determine prints
|
|
`true`, because a wrong `false` silently skips a test and a wrong `true` only
|
|
costs runner time. A gate that guesses must guess toward running.
|
|
"""
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def paths_of(workflow: Path) -> list[str]:
|
|
"""The union of every `paths:` list under `on:` in this workflow.
|
|
|
|
Parsed rather than imported from a library: the runner image has no PyYAML
|
|
guaranteed, and the shape here is a flat `paths:` followed by `- 'glob'`
|
|
lines. A list this parser fails to read yields none, which fails open.
|
|
"""
|
|
globs: list[str] = []
|
|
in_paths = False
|
|
for line in workflow.read_text().splitlines():
|
|
if re.match(r"\s*paths(-ignore)?:\s*$", line):
|
|
in_paths = True
|
|
continue
|
|
if in_paths:
|
|
m = re.match(r"\s*-\s*'([^']+)'\s*$", line) or re.match(r'\s*-\s*"([^"]+)"\s*$', line)
|
|
if m:
|
|
globs.append(m.group(1))
|
|
continue
|
|
if line.strip().startswith("- "):
|
|
globs.append(line.strip()[2:].strip())
|
|
continue
|
|
in_paths = False
|
|
return globs
|
|
|
|
|
|
def matches(path: str, glob: str) -> bool:
|
|
"""`glob` in the Actions sense: `**` spans separators, `*` does not."""
|
|
rx = []
|
|
i = 0
|
|
while i < len(glob):
|
|
if glob.startswith("**", i):
|
|
rx.append(".*")
|
|
i += 2
|
|
elif glob[i] == "*":
|
|
rx.append("[^/]*")
|
|
i += 1
|
|
elif glob[i] == "?":
|
|
rx.append("[^/]")
|
|
i += 1
|
|
else:
|
|
rx.append(re.escape(glob[i]))
|
|
i += 1
|
|
return re.fullmatch("".join(rx), path) is not None
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) != 4:
|
|
print("true") # cannot tell: run it
|
|
return
|
|
workflow, base, head = Path(sys.argv[1]), sys.argv[2], sys.argv[3]
|
|
if not workflow.is_file() or not base or not head:
|
|
print("true")
|
|
return
|
|
globs = paths_of(workflow)
|
|
if not globs:
|
|
# No filter, or a shape this parser does not read: the workflow wanted
|
|
# to run on everything, or we cannot say it did not.
|
|
print("true")
|
|
return
|
|
try:
|
|
out = subprocess.run(
|
|
["git", "diff", "--name-only", f"{base}...{head}"],
|
|
capture_output=True, text=True, check=True,
|
|
).stdout
|
|
except (subprocess.CalledProcessError, OSError):
|
|
print("true")
|
|
return
|
|
changed = [l for l in out.splitlines() if l.strip()]
|
|
if not changed:
|
|
print("true")
|
|
return
|
|
hit = any(matches(p, g) for p in changed for g in globs)
|
|
print("true" if hit else "false")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|