Reusable CI workflows for the Theokit ecosystem, and the tooling they run. One place to correct a gate instead of eleven that drift.
.github/workflows/dep-check.yml |
The dependency gate. Reusable — a repository calls it. |
.github/workflows/dep-check-report.yml |
The organisation-wide sweep. Scheduled, runs here, opens one standing issue. |
.github/workflows/workflow-lint.yml |
actionlint + zizmor over a caller's workflows. Reusable. |
.github/workflows/secret-scan.yml |
TruffleHog over the diff range. Reusable. |
actions/setup |
Composite action: Node, the declared package manager, a cache that cannot outlive it, a frozen install. |
actions/npm-oidc |
Composite action: the npm that can publish over OIDC, asserted rather than assumed. |
packages/dep-check |
@theokit/dep-check — the tool the workflows invoke. |
# .github/workflows/dep-check.yml
name: Dependency Gate
on:
push:
branches: [main, develop, workspace]
pull_request:
branches: [main, develop]
workflow_dispatch:
inputs:
full: { type: boolean, default: false }
permissions:
contents: read
jobs:
dep-check:
uses: usetheokit/shared-workflows/.github/workflows/dep-check.yml@v1
with:
run-install-check: ${{ github.base_ref == 'main' || inputs.full == true }}
run-floor-check: ${{ github.base_ref == 'main' || inputs.full == true }}
run-impact-check: ${{ github.base_ref == 'main' || inputs.full == true }}Then mark dep-check / dependency gate required in branch protection. A job that runs and
reports but is not required blocks nothing.
A reusable workflow is a whole job: its own runner, its own filesystem. That fits a gate, which is a job — lint the workflows, scan the diff, check the ranges. It does not fit setup, which is a sequence of steps inside somebody else's job. Sharing setup as a reusable workflow would mean handing it the rest of the job as a string input, which is the shape a shared workflow takes when it is asked to be everything.
So actions/setup is a composite action and the gates are reusable workflows. The rule is which
unit the thing actually is, not which mechanism is more familiar.
Every reusable here declares workflow_call and nothing else. The on: block and the
concurrency: group belong to the caller, for a reason that is easy to get wrong: a concurrency
group declared inside a reusable is shared by every repository that calls it, so a run in one
repository would cancel a run in another. github.ref inside a reusable is the caller's ref, which
makes the group look correctly scoped right up until two repositories push at the same time.
This is the part worth reading before changing anything.
The workflow moves with a tag. @v1 is a moving major: a fix to the YAML reaches every caller
on their next run, which is the entire reason a reusable workflow exists. Pinning callers to a
commit SHA would mean eleven pull requests to correct one gate, which is how a central policy stops
being central.
The tool does not move. @theokit/dep-check is pinned to an exact version by the workflow's
dep-check-version input, so a behaviour change in the detector is a version bump somebody
reviewed — not something that arrives overnight because the tool's main branch moved. The pin lives
in one file here rather than in eleven callers.
That split is deliberate. The failure it prevents: a detector silently changing what it detects is indistinguishable from the codebase changing, and the green that follows is believed either way.
The organisation-wide sweep is the exception — it runs the tool from this checkout, not the published version, so a defect in the detector surfaces in our own report before it is pinned into eleven repositories.
Tag dep-check-v<version> after bumping packages/dep-check/package.json. The release workflow
verifies that the tag and the manifest agree before publishing — a tag saying v0.2.0 on a manifest
saying 0.1.0 publishes 0.1.0 and then fails on the next attempt with an error about the wrong
thing, hours after the mistake.
Publishing is npm trusted publishing over OIDC: no secret in this repository, nothing to rotate, and every tarball carries a provenance attestation linking it to the commit and workflow that built it.
The gate lived in usetheokit/.github first. That works — uses: resolves from any repository —
but it is not what .github is for: GitHub inherits community health files from there
(CONTRIBUTING, SECURITY, issue templates, the profile README), and workflows are not among them.
Nothing was inherited; it was an explicit reference to a repository that happened to have that name,
mixed in with the organisation's public profile.
It also had a concrete cost. usetheokit/.github has a single branch, workspace, so the callers
could not use the @main everyone writes by default — and a uses: naming a branch that does not
exist fails at workflow resolution, before any step runs, with an error about the workflow rather
than about dependencies. Eleven repositories nearly shipped a gate that never ran while looking
exactly like a gate that passed.