Skip to content

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 Makefile plus everything it includes, transitively, globs expanded (rhiza's own layout is Makefile.rhiza/rhiza.mk.rhiza/make.d/*.mk); read by :mod:rhiza_hooks._makefile, shared with check-makefile-targets;
  • invocations — the shell snippets of every CI definition: run: in GitHub workflows, script:/before_script:/after_script: in .gitlab-ci.yml.

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.

Exit codes

0 - every resolvable invocation names a defined target 1 - at least one invocation names a target nothing defines

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
def check_workflow_make_targets(repo_root: Path) -> list[str]:
    """Report every CI invocation of a make target that nothing defines.

    Args:
        repo_root: Root directory of the repository.

    Returns:
        List of error messages, one per (file, missing target) pair. Empty when
        there is no Makefile — with no targets to compare against, every invocation
        would be reported, which is noise rather than signal.
    """
    defined = collect_targets(repo_root)
    if not defined:
        return []

    return [
        f"{filename} runs `make {target}`, but no Makefile or include defines that target."
        for filename, invoked in _ci_invocations(repo_root).items()
        for target in sorted(invoked - defined)
    ]

invoked_targets(snippet)

Return the resolvable make targets a shell snippet invokes.

Source code in rhiza_hooks/check_workflow_make_targets.py
def invoked_targets(snippet: str) -> set[str]:
    """Return the resolvable make targets a shell snippet invokes."""
    targets: set[str] = set()
    for line in snippet.splitlines():
        words = line.replace(";", " ; ").replace("&&", " && ").split()
        for index, word in enumerate(words):
            if word == "make":
                targets.update(_targets_in_command(words[index + 1 :]))
    return targets

main(argv=None)

Run the hook and return a process exit code.

Source code in rhiza_hooks/check_workflow_make_targets.py
def main(argv: list[str] | None = None) -> int:
    """Run the hook and return a process exit code."""
    parser = argparse.ArgumentParser(description="Check every make target invoked by CI exists")
    parser.add_argument(
        "filenames",
        nargs="*",
        help="Filenames (ignored: a target removal must be caught as well as a workflow edit)",
    )
    parser.parse_args(argv)  # validate/consume pre-commit's filename args; result unused

    errors = check_workflow_make_targets(find_repo_root())

    for error in errors:
        print(f"ERROR: {error}")

    return 1 if errors else 0