Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ marketplace `ref` at once.
- **Breaking** for a workflow named `marola-sea-publish.yml` on `self-hosted`: `workflow-runners`
drops that exemption, so no workflow may name `self-hosted` or an expression in `runs-on:` or
`runner:`. marola-ml removed that workflow, and nothing else is self-hosted (#168).
- `wiring.py` counts a pin as a trigger of a workflow that runs on a push to `main` when a
`dorny/paths-filter` filter lists it and that filter's output gates a job's `if:`, so marola-app's
`ci.yml` is listed again for `corpus.version` (#169).

## v0.16.0 — 2026-10-17

Expand Down
55 changes: 54 additions & 1 deletion scripts/wiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,42 @@ def triggers(doc: dict) -> dict:
return {k: v or {} for k, v in on.items()}


def gated_paths(doc: dict) -> set[str]:
"""Paths a `dorny/paths-filter` filter lists, when that filter's output gates a job's `if:`."""
jobs = doc.get("jobs") or {}
ifs = "\n".join(str(j.get("if", "")) for j in jobs.values())
out: set[str] = set()
for name, job in jobs.items():
for step in job.get("steps") or []:
if not str(step.get("uses", "")).startswith("dorny/paths-filter"):
continue
filters = (step.get("with") or {}).get("filters")
if isinstance(filters, str): # a file path stays a str and is not read
filters = load(filters, f"{name}'s paths-filter")
if not isinstance(filters, dict):
continue
for o, v in (job.get("outputs") or {}).items():
gates = re.search(
rf"\bneeds\.{re.escape(name)}\.outputs\.{re.escape(o)}(?![\w-])", ifs
)
for sid, f in re.findall(r"steps\.([\w-]+)\.outputs\.([\w-]+)", str(v)):
if gates and sid == step.get("id") and isinstance(filters.get(f), list):
out |= {p for p in filters[f] if isinstance(p, str)}
return out


def pin_triggers(on: dict, doc: dict) -> set[str]:
if "push" not in on:
return set()
push, pins = on["push"], set(PINS + ("flake.lock",))
if push.get("paths"):
return pins & set(push["paths"])
# a push filtered to tags alone never runs on a branch
if "main" not in (push.get("branches") or ([] if push.get("tags") else ["main"])):
return set()
return (pins & gated_paths(doc)) - set(push.get("paths-ignore") or [])


def when(on: dict) -> str:
push = on.get("push", {})
if push.get("tags"):
Expand Down Expand Up @@ -346,7 +382,7 @@ def callee(repo: str, file: str, ref: str) -> tuple[dict | None, str]:
label = f"{repo} `{file}`"
for t in on.get("repository_dispatch", {}).get("types") or []:
add(w.dispatch.setdefault(t, {"sent": [], "listen": []})["listen"], label)
for pin in set(PINS + ("flake.lock",)) & set(on.get("push", {}).get("paths") or []):
for pin in pin_triggers(on, doc):
add(w.bumps.setdefault(f"{repo} `{pin}`", []), label)
repo_dir = dict(rs)[repo]
for kind, v, via in effects(doc, repo, {}, callee, dirs[repo]):
Expand Down Expand Up @@ -555,6 +591,11 @@ def check(w: Wiring, block: str, path: Path | None, allow: str) -> list[str]:


CO = " - uses: actions/checkout@v7\n with: {repository: %s, path: %s%s}\n"
CHANGES = (
" changes:\n outputs:\n %s: ${{ steps.filter.outputs.%s }}\n steps:\n"
" - uses: dorny/paths-filter@v4\n id: filter\n with:\n"
" filters: |\n %s:\n - 'Dockerfile'\n - %s\n"
)
FIXTURE = {
".gitmodules": "".join(
f'[submodule "{r}"]\n\tpath = {r}\n'
Expand Down Expand Up @@ -605,6 +646,15 @@ def check(w: Wiring, block: str, path: Path | None, allow: str) -> list[str]:
".devkit/.github/workflows/ping.yml": 'on: {workflow_call: {}}\njobs:\n p:\n steps:\n - run: curl https://api.github.com/repos/x/dispatches -d \'{"event_type":"x"}\'\n',
"marola-ml/.github/workflows/ping.yml": "on: {push: {branches: [main]}}\njobs:\n a:\n uses: marola-dev/marola-devkit/.github/workflows/ping.yml@v0.3.1\n b:\n uses: marola-dev/marola-devkit/.github/workflows/ping.yml@v0.3.1\n",
"marola-ml/scripts/corpus-fetch.sh": 'pin="$(<"$root/corpus.version")"\nbase="https://github.com/marola-dev/marola-corpus/releases/download"\n',
"marola-ml/.github/workflows/ci.yml": "on: {push: {branches: [main]}, pull_request: {}}\njobs:\n"
+ CHANGES % ("docker", "docker", "docker", "'corpus.version'")
+ " jvm:\n needs: [changes]\n if: needs.changes.outputs.docker == 'true' || github.event_name == 'workflow_dispatch'\n steps: []\n",
"marola-corpus/.github/workflows/ci.yml": "on: {push: {branches: [main]}}\njobs:\n"
+ CHANGES % ("pins", "pins", "pins", "'resources.version'")
+ " x:\n needs: [changes]\n if: needs.changes.outputs.pins-x == 'true'\n steps: []\n",
"marola-corpus/.github/workflows/pr.yml": "on: {pull_request: {}}\njobs:\n"
+ CHANGES % ("docker", "docker", "docker", "'corpus.version'")
+ " b:\n needs: changes\n if: needs.changes.outputs.docker == 'true'\n steps: []\n",
}

API_ROW = "| `api-docs` branch | marola-app `api-docs.yml` (`api-docs.yml@v0.4.1`, on a push to `main`), marola-corpus `api-docs.yml` (`api-docs.yml@v9.9.9` (resolved at v0.6.0), on a push to `main`) | — | marola `scripts/fetch-api-docs.sh` |"
Expand Down Expand Up @@ -632,6 +682,9 @@ def check(w: Wiring, block: str, path: Path | None, allow: str) -> list[str]:
"own_repo_pr_dropped": "!self-pr.yml",
"image_reader_forms": "| `ghcr.io/marola-dev/marola-corpus` image | marola-corpus `docker.yml` (on a push to `main`) | — | marola-site `docker-compose.yml`, marola-ml `Dockerfile`, marola-ml `docker-compose.yml` |",
"commented_pin_not_a_reader": "!marola-ml `justfile`",
"paths_filter_gating_a_job_is_a_pin_trigger": "| marola-ml `corpus.version` | marola-ml `ci.yml` |",
"paths_filter_gating_no_job_adds_no_row": "!marola-corpus `resources.version`",
"paths_filter_without_push_adds_no_row": "!marola-corpus `corpus.version`",
}


Expand Down
Loading