diff --git a/.github/workflows/_ci-check.yml b/.github/workflows/_ci-check.yml index 0d46bd0..737735e 100644 --- a/.github/workflows/_ci-check.yml +++ b/.github/workflows/_ci-check.yml @@ -85,6 +85,17 @@ on: required: false type: string default: pnpm install --frozen-lockfile + remote-source-cache-command: + description: >- + Command that decides whether, and under which key, the checks' remote + source downloads are cached. It runs after the install and must append + `cacheable`, `key` and a multi-line `paths` to `$GITHUB_OUTPUT` itself + (for duxt, `node ./bin/duxt-cache-key.mjs --github >> "$GITHUB_OUTPUT"`). + Empty, the default, is off: no step runs and nothing is cached. A set + command IS the opt-in, so there is no separate switch to keep in step. + required: false + type: string + default: '' # THE GROUP CARRIES THIS BODY'S NAME, and that is not decoration. In a called # workflow `github.workflow` is the CALLER's name, so two bodies invoked by the @@ -130,6 +141,38 @@ jobs: INSTALL_COMMAND: ${{ inputs.install }} run: bash -c "$INSTALL_COMMAND" + # THE BODY KNOWS THE OUTPUT CONTRACT, NOT THE TOOL. Whether a download is + # safe to cache is the caller's command's call: an Actions cache written on + # a branch is readable by every pull request against it, so a command that + # sees a private or authenticated source answers `cacheable=false` and the + # cache step below skips. `false` is an answer, not a failure. + # + # AN INPUT AND NOT A JOB BESIDE THE CALL. A reusable workflow runs on its + # own runner with its own filesystem, so a restore in the caller's job can + # never reach the directory these checks read. + - name: Resolve the remote-source cache key + id: remote-sources + if: inputs.remote-source-cache-command != '' + env: + REMOTE_SOURCE_CACHE_COMMAND: ${{ inputs.remote-source-cache-command }} + run: bash -c "$REMOTE_SOURCE_CACHE_COMMAND" + + # Restores here and saves in its post step, once the checks have filled the + # paths — and only on a green job, so a half-downloaded tree is never kept. + # + # EXACT KEY, NO `restore-keys`. A near miss would restore sources for a + # different ref and hand the checks content that is not what the repo + # declares. This gate runs on `pull_request` alone, so what it saves serves + # that pull request's re-runs; a hit across pull requests comes from the + # default branch's own writer, which has to run the identical command so + # key and path list match. + - name: Cache the remote sources + if: steps.remote-sources.outputs.cacheable == 'true' + uses: actions/cache@v6 + with: + path: ${{ steps.remote-sources.outputs.paths }} + key: ${{ steps.remote-sources.outputs.key }} + - name: Resolve the checks id: resolve env: diff --git a/README.md b/README.md index f131f9e..c1b2e3c 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,15 @@ A repo whose gate lives under another name passes it: gate-script: verify ``` +A repo whose checks download remote sources — a Nuxt Content site reading `repo` sources — can cache them across runs by naming the command that works out the key: + +```yaml + with: + remote-source-cache-command: node ./bin/duxt-cache-key.mjs --github >> "$GITHUB_OUTPUT" +``` + +The command appends `cacheable`, `key` and a multi-line `paths` to `$GITHUB_OUTPUT`; the body restores and saves those paths with `actions/cache` under that exact key, and skips both unless `cacheable` is `true`. Left empty, which is the default, nothing runs. The gate runs on pull requests only, so a hit across pull requests comes from the default branch's own writer running the identical command. + ### Adding a check needs no workflow change Because the job list comes from the gate script, a repo adds a check by editing `package.json` alone. The `.gitignore` drift check is the worked example: diff --git a/docs/1.guides/1.migrate-a-repo-to-a-stub.md b/docs/1.guides/1.migrate-a-repo-to-a-stub.md index af2ffee..3ebc257 100644 --- a/docs/1.guides/1.migrate-a-repo-to-a-stub.md +++ b/docs/1.guides/1.migrate-a-repo-to-a-stub.md @@ -1,10 +1,9 @@ --- title: 'Migrate a repo to a stub' description: 'Replace a repo copy of a workflow with a call to the central body, and move its branch-protection rule to the check names the call produces.' +icon: 'lucide:git-pull-request-arrow' --- -# Migrate a repo to a stub - One repo, one workflow at a time. The file shrinks to a trigger and a `uses:`, and the checks the repo reports change name in the same run — which is the part that breaks something if it is left for later. ## Steps @@ -91,8 +90,9 @@ jobs: tree: ${{ needs.verified.outputs.tree }} ``` -> [!IMPORTANT] -> A gated job reports as **skipped**, not as passed. Whether a skipped job satisfies a required check is not verified for this estate — confirm it on one repo before making the marker a rule, or a green run will look blocked. +::callout{type="warning"} +A gated job reports as **skipped**, not as passed. Whether a skipped job satisfies a required check is not verified for this estate — confirm it on one repo before making the marker a rule, or a green run will look blocked. +:: ## Checklist diff --git a/docs/1.guides/2.add-a-body.md b/docs/1.guides/2.add-a-body.md index fd4b6fc..7484b33 100644 --- a/docs/1.guides/2.add-a-body.md +++ b/docs/1.guides/2.add-a-body.md @@ -1,10 +1,9 @@ --- title: 'Add a body' description: 'Write a new reusable workflow that other repos call, without giving callers a value they have to keep correct.' +icon: 'lucide:file-plus' --- -# Add a body - A body runs in every repo that calls it, so the question to hold throughout is what a repo on a different branch topology, a different runner or a different stack does with it. ## Steps @@ -15,9 +14,9 @@ A body runs in every repo that calls it, so the question to hold throughout is w 3. **Hardcode `runs-on: ubuntu-latest`.** A caller cannot set it, and making it an input has not been needed. -4. **Derive what the repo can answer; take an input only for what it cannot.** The branch topology, the owner and the check list are properties of the calling repo — read them. A version matrix or a language set is not, so those are inputs. See [ADR-0001](../99.adr/0001-derive-a-bodys-configuration-from-the-repo.md). +4. **Derive what the repo can answer; take an input only for what it cannot.** The branch topology, the owner and the check list are properties of the calling repo — read them. A version matrix or a language set is not, so those are inputs. See [ADR-0001](/adr/0001-derive-a-bodys-configuration-from-the-repo). -5. **Declare each secret the body needs under `secrets:`.** Named, never inherited — see [ADR-0003](../99.adr/0003-name-every-secret-a-stub-passes.md). +5. **Declare each secret the body needs under `secrets:`.** Named, never inherited — see [ADR-0003](/adr/0003-name-every-secret-a-stub-passes). 6. **Choose the job names deliberately.** They are the second half of every caller's check names, so a rename breaks required checks in every calling repo at once. Treat them as a public interface from the first commit, and prefer a name that survives the body growing a job. @@ -45,7 +44,7 @@ A body runs in every repo that calls it, so the question to hold throughout is w This holds for anything the caller supplies and for the mutable parts of the `github` context — `ref_name` and `run_number` among them. CodeQL flags the interpolated form as code injection, and it is right to. -9. **Do not call the repository's composite action from the body.** A relative `uses:` resolves against the caller's workspace — see [ADR-0006](../99.adr/0006-keep-the-composite-action-out-of-the-bodies.md). Duplicate the setup steps instead. +9. **Do not call the repository's composite action from the body.** A relative `uses:` resolves against the caller's workspace — see [ADR-0006](/adr/0006-keep-the-composite-action-out-of-the-bodies). Duplicate the setup steps instead. 10. **Give the concurrency group this body's own name.** In a called workflow `github.workflow` is the _caller's_ name, so two bodies the same caller invokes share one group and cancel each other — the second to start kills the first, and the run reports a job that never ran: diff --git a/docs/1.guides/index.md b/docs/1.guides/index.md index 7fe8b15..d8f59d3 100644 --- a/docs/1.guides/index.md +++ b/docs/1.guides/index.md @@ -1,11 +1,10 @@ --- title: 'Guides' description: 'The two tasks this repository asks of a person: moving a repo onto a body, and writing a new one.' +icon: 'lucide:book-open' --- -# Guides - Both tasks change something outside this repository — the first changes another repo's checks, the second changes what every caller runs. Each page ends with a checklist for that reason. -- [Migrate a repo to a stub](1.migrate-a-repo-to-a-stub.md) — replace a copied workflow with a call, without stranding a branch-protection rule. -- [Add a body](2.add-a-body.md) — write a new reusable workflow that other repos can call. +::page-cards +:: diff --git a/docs/99.adr/0001-derive-a-bodys-configuration-from-the-repo.md b/docs/99.adr/0001-derive-a-bodys-configuration-from-the-repo.md index b1c2bcc..6a85089 100644 --- a/docs/99.adr/0001-derive-a-bodys-configuration-from-the-repo.md +++ b/docs/99.adr/0001-derive-a-bodys-configuration-from-the-repo.md @@ -5,8 +5,6 @@ status: 'accepted' date: '2026-09-01' --- -# ADR-0001 — Derive a body's configuration from the repo - ## Context The bodies replace workflow files that had been copied into every repo and had drifted apart. Counting the copies made them look irreconcilable — the fast-forward queue workflow existed in eight versions — but the differences between them were not eight problems. They were the integration branch name, a per-owner secret identifier, a runner label, and a group of repos that had never received a fix. diff --git a/docs/99.adr/0002-pin-callers-to-a-commit-sha.md b/docs/99.adr/0002-pin-callers-to-a-commit-sha.md index 132b8c7..ec5ff4b 100644 --- a/docs/99.adr/0002-pin-callers-to-a-commit-sha.md +++ b/docs/99.adr/0002-pin-callers-to-a-commit-sha.md @@ -5,8 +5,6 @@ status: 'accepted' date: '2026-09-01' --- -# ADR-0002 — Pin callers to a commit SHA - ## Context A caller has to name a ref. A moving major tag is the conventional choice and the one the marketplace actions themselves offer, and this repository can move such a tag: release-please cuts exact tags, and the release workflow can force-push `v` onto each one. diff --git a/docs/99.adr/0003-name-every-secret-a-stub-passes.md b/docs/99.adr/0003-name-every-secret-a-stub-passes.md index 07cbf98..0577f42 100644 --- a/docs/99.adr/0003-name-every-secret-a-stub-passes.md +++ b/docs/99.adr/0003-name-every-secret-a-stub-passes.md @@ -5,8 +5,6 @@ status: 'accepted' date: '2026-09-01' --- -# ADR-0003 — Name every secret a stub passes - ## Context A caller can hand a reusable workflow everything it holds in one word. That word is shorter than a block naming each secret, and it never needs touching again when a body grows a new requirement. diff --git a/docs/99.adr/0004-compose-ci-from-bodies-that-are-jobs.md b/docs/99.adr/0004-compose-ci-from-bodies-that-are-jobs.md index 09c22c2..452d9ab 100644 --- a/docs/99.adr/0004-compose-ci-from-bodies-that-are-jobs.md +++ b/docs/99.adr/0004-compose-ci-from-bodies-that-are-jobs.md @@ -5,8 +5,6 @@ status: 'accepted' date: '2026-09-01' --- -# ADR-0004 — Compose CI from bodies that are jobs - ## Context CI was the workflow that had drifted furthest: measured across the estate, nearly every repo's copy was unique. Behind that were a handful of families — repos running a single gate, libraries running a typecheck-test-build set, packages running a version matrix against a database, providers running a language toolchain — and a few repos with pipelines that genuinely belong to them, carrying deploy stages and their own short-circuits. diff --git a/docs/99.adr/0005-publish-with-one-body-per-target.md b/docs/99.adr/0005-publish-with-one-body-per-target.md index 65ee5dd..ca08222 100644 --- a/docs/99.adr/0005-publish-with-one-body-per-target.md +++ b/docs/99.adr/0005-publish-with-one-body-per-target.md @@ -5,8 +5,6 @@ status: 'accepted' date: '2026-09-01' --- -# ADR-0005 — Publish with one body per target - ## Context The release workflow and the publish step arrived as one file in every repo that had them. Most repos that cut releases publish nothing at all — they exist to be depended on by their own tags, or they deploy instead. The ones that do publish split by target, and the targets need different credentials: a registry token for one, a signing key out of the vault for another. diff --git a/docs/99.adr/0006-keep-the-composite-action-out-of-the-bodies.md b/docs/99.adr/0006-keep-the-composite-action-out-of-the-bodies.md index 5185fbf..3f26755 100644 --- a/docs/99.adr/0006-keep-the-composite-action-out-of-the-bodies.md +++ b/docs/99.adr/0006-keep-the-composite-action-out-of-the-bodies.md @@ -5,8 +5,6 @@ status: 'accepted' date: '2026-09-01' --- -# ADR-0006 — Keep the composite action out of the bodies - ## Context A reusable workflow is taken whole. A caller cannot replace one of its steps or insert another, so a repo needing "that body plus one thing" has to write its own job — which is why the setup those bodies open with also exists as a composite action it can call. diff --git a/docs/99.adr/0007-run-the-gate-as-one-job-with-a-step-per-check.md b/docs/99.adr/0007-run-the-gate-as-one-job-with-a-step-per-check.md index 44977a7..15112ec 100644 --- a/docs/99.adr/0007-run-the-gate-as-one-job-with-a-step-per-check.md +++ b/docs/99.adr/0007-run-the-gate-as-one-job-with-a-step-per-check.md @@ -5,8 +5,6 @@ status: 'accepted' date: '2026-09-01' --- -# ADR-0007 — Run the gate as one job with a step per check - ## Context For the repos whose CI is a set of independent checks, the copied workflows listed those checks a second time as steps — the same commands the package manifest's gate script already chained, kept in step by hand. diff --git a/docs/99.adr/0008-give-each-reporter-its-own-comment-thread.md b/docs/99.adr/0008-give-each-reporter-its-own-comment-thread.md index ab59eb2..17674a0 100644 --- a/docs/99.adr/0008-give-each-reporter-its-own-comment-thread.md +++ b/docs/99.adr/0008-give-each-reporter-its-own-comment-thread.md @@ -5,8 +5,6 @@ status: 'accepted' date: '2026-09-01' --- -# ADR-0008 — Give each reporter its own comment thread - ## Context Two of the bodies write results back to a pull request: coverage, and the performance audit. The repository they were derived from had collected both into a single comment, and said why — a second reporting job beside it would open a second thread, and a pull request carrying a thread per metric is one nobody reads. diff --git a/docs/99.adr/0009-write-a-body-before-its-second-caller.md b/docs/99.adr/0009-write-a-body-before-its-second-caller.md index f7c67ca..5eeddf6 100644 --- a/docs/99.adr/0009-write-a-body-before-its-second-caller.md +++ b/docs/99.adr/0009-write-a-body-before-its-second-caller.md @@ -5,8 +5,6 @@ status: 'accepted' date: '2026-09-01' --- -# ADR-0009 — Write a body before its second caller - ## Context The case for centralising a workflow is that several repos carry the same one. Where only a single repo has it, that case is absent: a body written for one caller generalises a shape nobody has met twice, and the input it takes may be the wrong seam. diff --git a/docs/99.adr/index.md b/docs/99.adr/index.md index 6247506..83cffdc 100644 --- a/docs/99.adr/index.md +++ b/docs/99.adr/index.md @@ -1,22 +1,21 @@ --- title: 'Architecture decisions' description: 'The decision log — every architecture decision recorded for this repository.' +icon: 'lucide:gavel' --- -# Architecture decisions - A decision earns an ADR when it constrains work that comes later and its reasoning would otherwise be lost: a choice between real alternatives, a convention every part of the project has to follow, a trade-off that looks like a mistake until the reason is known. Records are append-only — a reversed decision is written as a new ADR that supersedes the old one, never as an edit to it. This log holds decisions about **this repository**: what a body may assume, what a caller must state, and where the boundary between the two runs. Decisions about the estate's provisioned state — branch protection, the queue App, the secret mirror — belong to `kirchDev/infrastructure` and are recorded there; the bodies here implement them and cite them by id. -| ADR | Decision | Status | Date | -| :---------------------------------------------------------------- | :-------------------------------------------- | :------- | :--------- | -| [ADR-0001](0001-derive-a-bodys-configuration-from-the-repo.md) | Derive a body's configuration from the repo | Accepted | 2026-09-01 | -| [ADR-0002](0002-pin-callers-to-a-commit-sha.md) | Pin callers to a commit SHA | Accepted | 2026-09-01 | -| [ADR-0003](0003-name-every-secret-a-stub-passes.md) | Name every secret a stub passes | Accepted | 2026-09-01 | -| [ADR-0004](0004-compose-ci-from-bodies-that-are-jobs.md) | Compose CI from bodies that are jobs | Accepted | 2026-09-01 | -| [ADR-0005](0005-publish-with-one-body-per-target.md) | Publish with one body per target | Accepted | 2026-09-01 | -| [ADR-0006](0006-keep-the-composite-action-out-of-the-bodies.md) | Keep the composite action out of the bodies | Accepted | 2026-09-01 | -| [ADR-0007](0007-run-the-gate-as-one-job-with-a-step-per-check.md) | Run the gate as one job with a step per check | Accepted | 2026-09-01 | -| [ADR-0008](0008-give-each-reporter-its-own-comment-thread.md) | Give each reporter its own comment thread | Accepted | 2026-09-01 | -| [ADR-0009](0009-write-a-body-before-its-second-caller.md) | Write a body before its second caller | Accepted | 2026-09-01 | +| ADR | Decision | Status | Date | +| :------------------------------------------------------------------ | :-------------------------------------------- | :------- | :--------- | +| [ADR-0001](/adr/0001-derive-a-bodys-configuration-from-the-repo) | Derive a body's configuration from the repo | Accepted | 2026-09-01 | +| [ADR-0002](/adr/0002-pin-callers-to-a-commit-sha) | Pin callers to a commit SHA | Accepted | 2026-09-01 | +| [ADR-0003](/adr/0003-name-every-secret-a-stub-passes) | Name every secret a stub passes | Accepted | 2026-09-01 | +| [ADR-0004](/adr/0004-compose-ci-from-bodies-that-are-jobs) | Compose CI from bodies that are jobs | Accepted | 2026-09-01 | +| [ADR-0005](/adr/0005-publish-with-one-body-per-target) | Publish with one body per target | Accepted | 2026-09-01 | +| [ADR-0006](/adr/0006-keep-the-composite-action-out-of-the-bodies) | Keep the composite action out of the bodies | Accepted | 2026-09-01 | +| [ADR-0007](/adr/0007-run-the-gate-as-one-job-with-a-step-per-check) | Run the gate as one job with a step per check | Accepted | 2026-09-01 | +| [ADR-0008](/adr/0008-give-each-reporter-its-own-comment-thread) | Give each reporter its own comment thread | Accepted | 2026-09-01 | +| [ADR-0009](/adr/0009-write-a-body-before-its-second-caller) | Write a body before its second caller | Accepted | 2026-09-01 | diff --git a/docs/index.md b/docs/index.md index 89c64b2..f6a94e6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,17 @@ --- title: 'workflows documentation' description: 'How a repo moves onto the central reusable workflow bodies, how a new body is written, and the decisions that govern both.' +icon: 'lucide:book-open-text' +navigation: false --- -# workflows - This repository holds the estate's reusable GitHub Actions workflow bodies. Every other repo carries a thin caller stub instead of its own copy, so a fix lands once and reaches each repo on its next bump. These pages cover the two tasks the repository asks of a person — moving a repo onto a body, and writing a new one — and the decision log behind them. ## Sections -- [Guides](1.guides/) — migrating a repo onto a stub, and adding a body. -- [Architecture decisions](99.adr/) — the decision log. +::page-cards +:: -What the bodies do, how a stub looks and what each one needs is in the [README](../README.md); how to set the repo up and get a PR landed is in [CONTRIBUTING.md](../CONTRIBUTING.md). Decisions about the estate's provisioned state — branch protection, the queue App, the Bitwarden mirror — are recorded in `kirchDev/infrastructure`, not here. +What the bodies do, how a stub looks and what each one needs is in the [README](https://github.com/kirchDev/workflows/blob/main/README.md); how to set the repo up and get a PR landed is in [CONTRIBUTING.md](https://github.com/kirchDev/workflows/blob/main/CONTRIBUTING.md). Decisions about the estate's provisioned state — branch protection, the queue App, the Bitwarden mirror — are recorded in `kirchDev/infrastructure`, not here.