Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/_ci-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions docs/1.guides/1.migrate-a-repo-to-a-stub.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand Down
9 changes: 4 additions & 5 deletions docs/1.guides/2.add-a-body.md
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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:

Expand Down
7 changes: 3 additions & 4 deletions docs/1.guides/index.md
Original file line number Diff line number Diff line change
@@ -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
::
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions docs/99.adr/0002-pin-callers-to-a-commit-sha.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<major>` onto each one.
Expand Down
2 changes: 0 additions & 2 deletions docs/99.adr/0003-name-every-secret-a-stub-passes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions docs/99.adr/0004-compose-ci-from-bodies-that-are-jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions docs/99.adr/0005-publish-with-one-body-per-target.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions docs/99.adr/0008-give-each-reporter-its-own-comment-thread.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 0 additions & 2 deletions docs/99.adr/0009-write-a-body-before-its-second-caller.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
25 changes: 12 additions & 13 deletions docs/99.adr/index.md
Original file line number Diff line number Diff line change
@@ -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 |
10 changes: 5 additions & 5 deletions docs/index.md
Original file line number Diff line number Diff line change
@@ -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.
Loading