From 1407270ed64fd76d3c6eebcdb74ae7aba68bb8d6 Mon Sep 17 00:00:00 2001 From: Julien Martel Date: Sat, 12 Sep 2026 04:39:31 -0500 Subject: [PATCH 1/2] A branch with no PR can still be checked, and path filters are a no we wrote down MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two leftovers from the CI pass. Narrowing `push` to `main` took away the only way a branch with no PR yet ever got checked. haus's check.yml already hands that back with `workflow_dispatch`; workshop's test.yml did not, and neither did the canonical trigger block in docs/ci.md's rule 2. Both now match haus. factory's test.yml had the same gap and takes the same one line in hausfold/factory#23. The path-filter question is now measured and closed. Against a real ignore list over each repo's last 40 merged PRs, the share that would skip is haus 1, trill 6, scruff 10, perch 11, nebelung 12 — two or three minutes saved on a quarter of PRs at best. Not enough to buy a second silent copy of "which files does this job protect?", which rots; and scruff's release path reaches its gate through `workflow_call`, so a filter that skips a job on a docs path skips it for a tag too. It goes under "What we deliberately don't do" with the numbers and with the method, because the loose version of that measurement (which says 80%) is how the idea keeps coming back. The same section's "no hosted binary cache" now says what it does not rule out: a public read-only substituter for something nixpkgs doesn't carry needs no account and no token, and fails soft to building from source. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019v1reBfZd8d4euMNyXry6B --- .github/workflows/test.yml | 3 +++ docs/ci.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a45ce1ed..22724c29 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,6 +25,9 @@ on: push: branches: [main] pull_request: + # Let a branch be checked on demand (e.g. before opening a PR) without + # having to push to main first. + workflow_dispatch: # A second push to a PR branch makes the first run's answer worthless, and the # abandoned run still holds a runner — a runner the next PR then waits diff --git a/docs/ci.md b/docs/ci.md index cfb7db26..2a6e29db 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -60,11 +60,17 @@ on: push: branches: [main] pull_request: + workflow_dispatch: ``` `branches: ['**']` fires the same workflow twice for every push to a PR branch — once as `push`, once as `pull_request` — for one commit and one answer. +`workflow_dispatch` is the third line's whole job: narrowing `push` to `main` +takes away the only way a branch with no PR yet was ever checked, and the +dispatch button hands it back. Drop it and the gate is one run per commit *and* +no run at all for work that isn't a PR. + **3. Cancel a superseded PR run.** A second push to a branch has already made the first run's answer worthless, and the abandoned run still holds its runners. On the macOS ones that is a queue slot the next PR waits behind, which @@ -155,3 +161,31 @@ the fast half. both beat the GitHub cache on a cold store, and both mean an account, a token in nine repos, and a service that can be down when a PR cannot wait. The Actions cache is already there, already free, and already scoped per repo. + +That rules out a cache we *run*. A public, read-only `extra-substituters` entry +for a dependency nixpkgs does not carry is a different trade and is allowed: +no account, no token, and a substituter that is down falls back to building +from source, which is what the run does today anyway. + +**No `paths-ignore` filter on a PR gate.** The idea is that a docs-only PR +should skip the build. Measured against a real ignore list over each repo's +last 40 merged PRs, the share that would actually skip is haus 1, trill 6, +scruff 10, perch 11, nebelung 12 — a quarter at the top of the range and one +PR in forty at the bottom, for a saving of two or three minutes each. + +That is not enough to buy what it costs, because prose here is *guarded* +prose: `embed-skills.sh --check`, the two `check-skills.sh`, the generated +issue forms. A filter is a second, silent copy of the answer to "which files +does this job protect?", and the copy rots — that is `docs/drift.md`'s whole +subject. Worse, scruff's release workflow reaches its gate through +`workflow_call`, so a filter that skips a job on a docs path skips it for a +tag too, publishing to registries that have no undo. + +Cache the slow step instead. It helps every PR rather than one in four, and a +cold-cache miss still runs the real gate. + +The loose version of this measurement is how the idea keeps coming back. A +classifier that counts `modules/**/*.md` and `dist/**/README.md` as docs says +80%, which for haus is wrong by a factor of thirty. Run `gh pr diff +--name-only` over real merged PRs against the actual list you would ship, or +don't quote a number. From 0b57771d8f5f95e13efa4a2aafee5e2e9a5c3daa Mon Sep 17 00:00:00 2001 From: Julien Martel Date: Sat, 12 Sep 2026 04:44:41 -0500 Subject: [PATCH 2/2] The path-filter no is about the denylist, and it says which list it measured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assurance pass on the first commit found three things worth the rewrite. The scruff argument was wrong for the mechanism the heading named. A `paths-ignore:` under `on:` cannot skip anything at a tag — a `workflow_call` invocation does not evaluate the called workflow's own triggers, so release.yml still runs the full gate. The risk is real only for an in-job changed-files filter, which IS evaluated on that run, so the stanza now names that implementation and says where a filter would have to go if one were ever worth it. The section also contradicted the hausfold.co paragraph above it, which calls a `paths:` filter the first thing to reach for. Allowlist and denylist were never distinguished: a `paths:` says what one task workflow is about, a `paths-ignore:` on the gate says what the repo merges unchecked. Only the second is the no, and the heading now says so. The numbers were unreproducible because the ignore list they were measured against was nowhere in the doc. It is in the doc now, and the closing paragraph points at it instead of at an "actual list you would ship" the reader would have to invent. Two smaller ones: the substituter carve-out was answering "a cache we run" when the paragraph above rules out one we rent and push to, and fail-soft is the property that makes the difference, so it says that; and rule 2's block now says it is the floor rather than the whole `on:`, since scruff's gate also needs `workflow_call`. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019v1reBfZd8d4euMNyXry6B --- docs/ci.md | 76 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 53 insertions(+), 23 deletions(-) diff --git a/docs/ci.md b/docs/ci.md index 2a6e29db..86e91f26 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -69,7 +69,12 @@ on: `workflow_dispatch` is the third line's whole job: narrowing `push` to `main` takes away the only way a branch with no PR yet was ever checked, and the dispatch button hands it back. Drop it and the gate is one run per commit *and* -no run at all for work that isn't a PR. +no run at all for work that isn't a PR. Dispatching a branch and then opening a +PR on the same tree does produce two runs, which is the one hole in the rule's +own headline — it takes a deliberate button press, so it stays. + +That block is the floor, not the whole `on:`. `scruff`'s `check.yml` also +carries `workflow_call`, because its release workflow calls the gate at a tag. **3. Cancel a superseded PR run.** A second push to a branch has already made the first run's answer worthless, and the abandoned run still holds its @@ -162,30 +167,55 @@ both beat the GitHub cache on a cold store, and both mean an account, a token in nine repos, and a service that can be down when a PR cannot wait. The Actions cache is already there, already free, and already scoped per repo. -That rules out a cache we *run*. A public, read-only `extra-substituters` entry -for a dependency nixpkgs does not carry is a different trade and is allowed: -no account, no token, and a substituter that is down falls back to building -from source, which is what the run does today anyway. - -**No `paths-ignore` filter on a PR gate.** The idea is that a docs-only PR -should skip the build. Measured against a real ignore list over each repo's -last 40 merged PRs, the share that would actually skip is haus 1, trill 6, -scruff 10, perch 11, nebelung 12 — a quarter at the top of the range and one -PR in forty at the bottom, for a saving of two or three minutes each. - -That is not enough to buy what it costs, because prose here is *guarded* -prose: `embed-skills.sh --check`, the two `check-skills.sh`, the generated -issue forms. A filter is a second, silent copy of the answer to "which files -does this job protect?", and the copy rots — that is `docs/drift.md`'s whole -subject. Worse, scruff's release workflow reaches its gate through -`workflow_call`, so a filter that skips a job on a docs path skips it for a -tag too, publishing to registries that have no undo. +All three grounds are about a cache we would *rent and push to*, which is what +Cachix's OSS tier and FlakeHub sell. Reading someone else's public cache is a +different trade, and it is allowed: a read-only `extra-substituters` entry +needs no account and no token, and a substituter Nix cannot reach is treated as +absent, so the run builds from source exactly as it does today. It cannot turn +a green PR red, only a fast one slow — which is why "can be down when a PR +cannot wait" does not carry over. The trust question is real but bounded to one +signing key for one path, so name the dependency and the key in the PR. + +**No `paths-ignore` on a repo's gate.** This is the denylist, and it is the +opposite of the allowlist `paths:` above: a `paths:` says what one task +workflow is *about*, and a `paths-ignore:` on the gate says which changes the +whole repo will merge unchecked. The second is the one we don't write. + +The idea is that a docs-only PR should skip the build. Measured over each +repo's last 40 merged PRs, against the ignore list a filter would actually +ship — + +``` +docs/** (except **/SKILL.md) +README.md AGENTS.md CLAUDE.md CHANGELOG.md CONTRIBUTING.md +SECURITY.md THANKS.md FOUNDING.md LICENSE +.github/ISSUE_TEMPLATE/** .github/FUNDING* +``` + +— the share that would skip is haus 1, trill 6, scruff 10, perch 11, +nebelung 12. A quarter at the top of the range, one PR in forty at the bottom, +saving two or three minutes each. + +That does not buy what it costs. A filter is a second, silent copy of the +answer to "which files does this job protect?", and the copy rots — that is +`docs/drift.md`'s whole subject. Prose here is *guarded* prose, so the copy has +real work to do and real ways to be wrong: `embed-skills.sh --check`, the two +`check-skills.sh`, the generated issue forms. + +The sharp end is `scruff`, and only for one implementation. A `paths-ignore:` +under its `on:` is harmless at a tag — a `workflow_call` invocation does not +evaluate the called workflow's own triggers, so `release.yml` still runs the +full gate. An in-job changed-files filter (`dorny/paths-filter` plus an `if:`) +is evaluated every time the job runs, including that one, so *there* a docs +path can skip a job for a tag that publishes to npm, PyPI and crates.io, none +of which have an undo. If a filter is ever worth it somewhere, it goes on the +trigger, never in the job. Cache the slow step instead. It helps every PR rather than one in four, and a -cold-cache miss still runs the real gate. +cold miss still runs the real gate. The loose version of this measurement is how the idea keeps coming back. A classifier that counts `modules/**/*.md` and `dist/**/README.md` as docs says -80%, which for haus is wrong by a factor of thirty. Run `gh pr diff ---name-only` over real merged PRs against the actual list you would ship, or -don't quote a number. +80%, which for haus is wrong by a factor of thirty. Re-run it against the list +above with `gh pr list --state merged --limit 40 --json number,files`, or don't +quote a number.