Skip to content

feat(make-run): resolve and run a repo's make targets from its own Makefile - #21

Merged
hunterdsp merged 2 commits into
mainfrom
feat/make-target-runner
Aug 6, 2026
Merged

feat(make-run): resolve and run a repo's make targets from its own Makefile#21
hunterdsp merged 2 commits into
mainfrom
feat/make-target-runner

Conversation

@hunterdsp

Copy link
Copy Markdown
Contributor

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:

source what it says make docs runs
mcp-store skills/zensical.md:131 uv run zensical build --clean
standard.mk:353 default uv run --group dev zensical build --clean --strict
live doppler uv run --group docs zensical build --clean --strict

Three 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.sh is a sourced library, so jbx dispatches its functions against whatever repo you point it at:

jbx just-bashit:make-run mk-var -C ~/doppler DOCS_BUILD_CMD
# uv run --group docs zensical build --clean --strict
function purpose
mk-var [-C DIR] NAME one variable, fully expanded after defaults + overrides
mk-vars [-C DIR] [REGEX] the whole command surface in one make invocation
mk-run [-C DIR] TARGET run a target using that repo's own recipe
mk-has / mk-origin the predicates the above are built on

mk-vars deliberately 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:

  • An undefined variable is an error, never an empty string. 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.
  • An unparseable makefile fails loudly. make -pRrq exits 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

  • 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 — the failure mode that passes in a plain shell and breaks in CI.
  • The test fixture builds its makefiles with printf, not a <<-EOF heredoc: <<- strips leading tabs, and a make recipe is defined by its leading tab.

Verification

  • make test — 321/321, including 13 new
  • make lint — clean (shellcheck, shfmt, standard-check, help-check, ghost-check, gates-check)
  • mk-var/mk-vars/mk-run exercised against live doppler, just-makeit, and just-bashit

Once this lands on main, the org Pages mirror picks it up automatically — mirror.yml copies src/just_bashit/*.sh into jbs/ by glob, so nothing needs registering.

🤖 Generated with Claude Code

@hunterdsp

Copy link
Copy Markdown
Contributor Author

Pushed 76b302f — a portability fix found while adding workflow_dispatch in #22.

--eval is GNU make 3.82+ (NEWS, Version 3.82, 28 Jul 2010), and macOS ships 3.81 as /usr/bin/make. The test-macos job installs bash coreutils diffutils bats-core — not make — so make test there runs 3.81 and every query in this library would have failed on that one runner while passing everywhere else.

The expression now goes into a sentinel target in a throwaway makefile, handed to make alongside the repo's own via -f. That option has always existed. -f disables make's automatic makefile search, so the repo's own makefile is named explicitly — otherwise the query resolves against nothing but the sentinel and reports every variable undefined. $(info ...) needed no change; it landed in 3.81.

A test now asserts the library stays off --eval, since the only other thing that would catch a regression is a macOS-only CI failure.

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), make lint clean.

hunterdsp added a commit that referenced this pull request Aug 6, 2026
…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>
@hunterdsp
hunterdsp force-pushed the feat/make-target-runner branch from 76b302f to 2774432 Compare August 6, 2026 22:37
hunterdsp and others added 2 commits August 6, 2026 18:37
…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>
@hunterdsp hunterdsp closed this Aug 6, 2026
@hunterdsp hunterdsp reopened this Aug 6, 2026
@hunterdsp
hunterdsp merged commit a80daba into main Aug 6, 2026
11 checks passed
@hunterdsp
hunterdsp deleted the feat/make-target-runner branch August 6, 2026 22:48
@hunterdsp hunterdsp mentioned this pull request Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant