check_workflow_make_targets¶
rhiza_hooks.check_workflow_make_targets
¶
Check that every make target CI invokes actually exists.
A workflow calling a target the Makefile no longer defines fails only when that
workflow next runs — which, for a scheduled or path-filtered job, can be weeks
after the commit that broke it. The template has already produced this failure:
make validate existed up to rhiza v1.1.3 and was removed by v1.2.1, so
anything still naming it reported healthy repos as broken.
check-makefile-targets asserts that a handful of recommended targets exist.
This hook checks the opposite direction — that the targets actually invoked are
defined — which is what catches a removal or a rename.
Both sides come from files in the repo, so the check is offline and mechanical:
- targets — the root
Makefileplus everything itincludes, transitively, globs expanded (rhiza's own layout isMakefile→local.mk, and wasMakefile→.rhiza/rhiza.mk→.rhiza/make.d/*.mkbefore v1.4.0); read by :mod:rhiza_hooks._makefile, shared withcheck-makefile-targets; - invocations — the shell snippets of every CI definition:
run:in GitHub workflows,script:/before_script:/after_script:in.gitlab-ci.yml.
A catch-all rule silences this check, and honestly so: with %: in the
Makefile every name resolves, so an invocation of a target that does not exist is no
longer distinguishable from one that does — make itself cannot tell either, which is
why the rhiza-task shim leaves "unknown task" to the CLI. The comparison is skipped
rather than guessed at, and the run says so in its summary; the alternative, reading
the CLI's task list, would mean running it from a pre-commit hook.
Invocations are read out of parsed YAML rather than raw text, so name: make sure
the cache is warm cannot be mistaken for an invocation of a target called
sure. An invocation naming a target through a variable or a matrix expression
(make ${{ matrix.task }}) is unresolvable, and is skipped rather than reported:
a false positive here blocks every commit, which is worse than a missed check.
Only inline shell steps are read. A workflow that delegates to a reusable one::
jobs:
ci:
uses: jebel-quant/rhiza/.github/workflows/rhiza_ci.yml@v1.5.1
keeps every command in another repository, behind a pinned ref. Nothing in the checkout holds them, so this hook cannot see them — and since that is the shape of every rhiza-managed repo, the check can pass while inspecting nothing at all. That is indistinguishable, from the outside, from passing on merit.
Two things make the difference visible. The run always reports what it worked from
(inspected N CI file(s), found M resolvable make target invocation(s)), and
--require-invocations turns a zero into a failure, for a repo that believes it
has inline invocations to check. The flag is opt-in because zero is the correct,
permanent answer for a repo whose CI is entirely delegated: making it fatal by
default would report every such repo as broken, which is the mistake this hook
already refuses to make for unresolvable target names.
Following uses: into the reusable workflow would restore real coverage, but it
means network access from a pre-commit hook against a pinned ref — a much larger
change, and one better answered in the template's own CI, where a removed target is
reachable without leaving the repo that removed it.
Exit codes
0 - every resolvable invocation names a defined target
1 - at least one invocation names a target nothing defines, or
--require-invocations was given and there were no invocations to check
check_workflow_make_targets(repo_root)
¶
Report every CI invocation of a make target that nothing defines.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
repo_root
|
Path
|
Root directory of the repository. |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
List of error messages, one per (file, missing target) pair. Empty when |
list[str]
|
there is no Makefile — with no targets to compare against, every invocation |
list[str]
|
would be reported, which is noise rather than signal. |
Source code in rhiza_hooks/check_workflow_make_targets.py
invoked_targets(snippet)
¶
Return the resolvable make targets a shell snippet invokes.
sorted(invoked_targets("make fmt && make test")) ['fmt', 'test']
Flags are dropped, including the value of a flag that takes one, so the
argument of -C is never read as a target:
sorted(invoked_targets("make -C sub build")) ['build']
VAR=value overrides are not targets either:
sorted(invoked_targets("make CFLAGS=-O2 release")) ['release']
A word only make or a shell can resolve abandons the whole command, rather than contributing invented target names:
sorted(invoked_targets("make ${{ matrix.task }}")) []
Source code in rhiza_hooks/check_workflow_make_targets.py
main(argv=None)
¶
Run the hook and return a process exit code.
Source code in rhiza_hooks/check_workflow_make_targets.py
summarize(ci_file_count, invocations, *, catch_all=False)
¶
Return the one-line account of what a run actually inspected.
A guard that finds no input passes exactly like one that passes on merit, so the
run says what it read. The count is of distinct (file, target) pairs, which is
what the check compares — make fmt test contributes two, and a target named
twice in one file contributes one:
summarize(4, {"ci.yml": {"fmt", "test"}, ".gitlab-ci.yml": {"test"}}) 'inspected 4 CI file(s), found 3 resolvable
maketarget invocation(s)'
Zero is the answer worth being able to see. It is what a repo whose CI is entirely delegated to reusable workflows gets, and it means the check compared nothing:
summarize(8, {}) 'inspected 8 CI file(s), found 0 resolvable
maketarget invocation(s)'
A catch-all rule is the other way to compare nothing, and the more surprising one, because the invocations are right there and every one of them passes:
line = summarize(2, {"ci.yml": {"test"}}, catch_all=True) line.startswith("inspected 2 CI file(s), found 1 resolvable") True line.endswith("a catch-all rule (
%:) defines every name, so none was compared") True