feat(make-run): resolve and run a repo's make targets from its own Makefile - #21
Conversation
|
Pushed
The expression now goes into a sentinel target in a throwaway makefile, handed to make alongside the repo's own via A test now asserts the library stays off Worth noting how this was found: CI is still dark from the webhook throttle, so this would have shipped green-on-my-machine and broken on merge. It came out of reading the matrix by hand rather than from any run. Local verification: 322/322 (14 in this file), |
…ottled `push` and `pull_request` are both webhook-delivered. When GitHub sheds webhooks, this repo has no reachable trigger at all: no run appears, and there is no way to ask for one. That is not hypothetical. During the 2026-08-06 Actions incident GitHub throttled webhook processing to ~15% — "many events such as pushes and pull requests are not triggering workflow runs" — and PR #21 sat with an empty statusCheckRollup and zero runs for its head SHA. `gh workflow run` returned 422: a workflow is only dispatchable when the trigger exists on the DEFAULT branch, so it could not be fixed from the branch that needed it. Sibling repos carrying workflow_dispatch stayed fully drivable throughout the same window. deploy-docs is unaffected: it is already gated on `github.event_name == 'push' && github.ref == 'refs/heads/main'`, so a manual run builds and tests without republishing the site. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
76b302f to
2774432
Compare
…kefile Callers that need a project's command — CI docs, skill docs, tooling — have been transcribing it by hand. Transcriptions drift silently: the mcp-store skill for zensical documents `make docs` as `uv run zensical build --clean`, while standard.mk's default is `uv run --group dev zensical build --clean --strict` and doppler, which overrides ZENSICAL, actually runs `uv run --group docs ...`. Three answers, two of them wrong, and nothing to make them disagree out loud. A repo bases its Makefile on standard.mk, which supplies the sanctioned defaults with `?=`; the repo overrides what it must. This library reads the result of that layering instead of re-deriving it, so there is no second copy to vendor and nothing to keep in sync. mk-var one variable, fully expanded after defaults + overrides mk-vars the whole command surface in one make invocation mk-run run a target using that repo's own recipe mk-has predicate: does the repo define this target? mk-origin where make got a value, via make's own $(origin) mk-vars deliberately carries no built-in list of interesting variables: such a list would be exactly the hand-maintained copy this abolishes. Two properties the tests pin down, both of which make an empty result mean something specific rather than nothing: - An undefined variable is an error, never an empty string. `DOCS_CHECK_CMD ?=` is legitimately empty; a typo'd name is not, and collapsing the two is how a blank command reads as a real one. $(origin) is the only reliable discriminator. - An unparseable makefile fails loudly. `make -pRrq` exits 1 in question mode whenever the default goal is out of date, which is normal, but exits 2 when it cannot read the makefiles at all. Treating those alike hands the caller an empty database and an empty answer. Queries never run a recipe. Values resolve inside a sentinel target's recipe because a `--eval` string is parsed before any makefile is read, so variables referenced there are still empty. The database is captured before any pipe: `make -pRrq`'s exit 1 would otherwise poison every pipeline under `set -o pipefail`, which bats and most CI set — passing in a plain shell and failing in CI. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
macOS ships GNU Make 3.81 as /usr/bin/make, and the macos CI job installs bash, coreutils, diffutils and bats-core — not make — so `make test` there runs 3.81. `--eval` arrived in 3.82 (28 Jul 2010), so every query in this library would have failed on that one runner and passed everywhere else. The expression now goes into a sentinel target inside a throwaway makefile handed to make alongside the repo's own via `-f`, which has always existed. `-f` disables make's automatic makefile search, so the repo's own makefile is named explicitly — otherwise the query would resolve against nothing but the sentinel and report every variable undefined. The sentinel is .PHONY with a no-op command, so make neither runs a real recipe nor prints "is up to date". $(info ...) needs no change: it landed in 3.81. Guarded by a test, because the only thing that would otherwise catch a regression is a macOS-only CI failure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Why
Anything that needs a project's command — CI docs, skill docs, tooling — has been transcribing it by hand, and transcriptions drift silently. One example, found while writing this:
make docsrunsskills/zensical.md:131uv run zensical build --cleanstandard.mk:353defaultuv run --group dev zensical build --clean --strictuv run --group docs zensical build --clean --strictThree answers, two of them wrong, and nothing that makes them disagree out loud.
A repo bases its Makefile on
standard.mk, which supplies the sanctioned defaults with?=; the repo then overrides what it must. This library reads the result of that layering rather than re-deriving it — so there is no second copy to vendor and nothing to keep in sync.What
make-run.shis a sourced library, sojbxdispatches its functions against whatever repo you point it at:mk-var [-C DIR] NAMEmk-vars [-C DIR] [REGEX]mk-run [-C DIR] TARGETmk-has/mk-originmk-varsdeliberately carries no built-in list of interesting variables — such a list would be exactly the hand-maintained copy this abolishes. A test asserts that a variable the library has never heard of still appears.Two properties the tests pin down
Both exist so that an empty result means something specific rather than nothing:
DOCS_CHECK_CMD ?=is legitimately empty; a typo'd name is not. Collapsing the two is how a blank command comes to read as a real one.$(origin)is the only reliable discriminator.make -pRrqexits 1 in question mode whenever the default goal is out of date — normal — but exits 2 when it cannot read the makefiles at all. Treating those alike hands the caller an empty database, and the empty answer then reads as real.Implementation notes
--evalstring is parsed before any makefile is read, so variables referenced there are still empty.make -pRrq's exit 1 would otherwise poison every pipeline underset -o pipefail, which bats and most CI set — the failure mode that passes in a plain shell and breaks in CI.printf, not a<<-EOFheredoc:<<-strips leading tabs, and a make recipe is defined by its leading tab.Verification
make test— 321/321, including 13 newmake lint— clean (shellcheck, shfmt,standard-check,help-check,ghost-check,gates-check)mk-var/mk-vars/mk-runexercised against livedoppler,just-makeit, andjust-bashitOnce this lands on
main, the org Pages mirror picks it up automatically —mirror.ymlcopiessrc/just_bashit/*.shintojbs/by glob, so nothing needs registering.🤖 Generated with Claude Code