Skip to content

Commit cc5a372

Browse files
feat(governance): add rust-ci-reusable + elixir-ci-reusable workflows (#174)
## Why — extends #168 to the next two template-drift bug classes \`#168\` (merged) consolidated language-policy into governance-reusable and added deno-ci-reusable. That pattern — single source-of-truth reusable in standards consumed via a thin wrapper — kills the "every copy drifts independently" failure mode. Estate audit done with \`gh api search/code\` (2026-05-26): | template | repos | unique SHAs | drift | |---------------------|-------|-------------|--------| | \`rust-ci.yml\` | **137** | **30** | high | | \`elixir-ci.yml\` | **9** | **9** | 100% | | \`rescript-deno-ci.yml\` | 1+ | 2+ | fixed in #168 | \`elixir-ci.yml\` is especially bad — every copy is unique, and at least one (\`bofig\`) is YAML-broken with literal \`npermissions:\` lines from a botched estate sweep. ## What changes Two new reusable workflows in \`.github/workflows/\`: ### \`rust-ci-reusable.yml\` - Split jobs: \`check\` (cargo check + fmt + clippy) and \`test\` (cargo test). \`test\` \`needs: check\`. - \`if: hashFiles('Cargo.toml')\` guard on every job so the wrapper is safe to add to non-Rust repos. - Top-level + per-job \`permissions: contents: read\`. - Opt-in inputs: - \`enable_audit\` (default false — \`cargo install cargo-audit\` adds ~3-4 min per run) - \`enable_coverage\` (default false — tarpaulin + codecov upload) - \`clippy_args\` / \`test_args\` / \`check_args\` — per-repo overrides - \`runs-on\` ### \`elixir-ci-reusable.yml\` - Two-cache layers (\`deps\` + \`_build\`), keyed by \`elixir-version\` + \`mix.lock\` hash. - **\`mix deps.compile\`** step BEFORE \`mix compile --warnings-as-errors\` so transitive-dep warnings (e.g. rustler's \`:json.decode\` requiring Elixir 1.18, deprecated \`use Bitwise\`) don't fail the strict gate. Validated against tma-mark2 #41 in commit \`fa32c4f\`. - Default \`elixir-version: \"1.17\"\` — the \`1.15\` default in the legacy template produced the \`(Mix) … declared in its mix.exs file it supports only Elixir ~> 1.17\` error tma-mark2 #41 lived. - Opt-in inputs: - \`enable_dialyzer\` (default false — slow cold-cache) - \`enable_credo\` (default true) - \`otp-version\` / \`elixir-version\` overrides - Guards every job on \`hashFiles('mix.exs')\`. ## Downstream rollout After merge, each of the 137 (Rust) / 9 (Elixir) repos can replace its local copy with a 5-line wrapper: \`\`\`yaml # .github/workflows/rust-ci.yml name: Rust CI on: push: { branches: [main, master] } pull_request: permissions: contents: read jobs: rust-ci: uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@main \`\`\` Same shape as \`governance.yml\` → \`governance-reusable.yml\`, and the absolute-zero #41 + tma-mark2 #41 \`deno-ci.yml\` wrappers landed yesterday. Rollout can be fanned out per-repo. ## Test plan - [ ] CI green on this PR (the reusables are workflow_call-only; their content gets exercised via the next PR that adopts them). - [ ] After merge, file 1-2 wrapper PRs against repos with known-red rust-ci CI as smoke tests (e.g., repos in the inbox triage that fail on \`Workflow security linter\` complaining about missing \`permissions:\`). - [ ] Confirm \`enable_audit: true\` works on a repo that opts in. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 74ce78a commit cc5a372

2 files changed

Lines changed: 77 additions & 9 deletions

File tree

.github/workflows/elixir-ci-reusable.yml

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,15 @@
4343
# with:
4444
# elixir-version: "1.18"
4545
# enable_dialyzer: true
46+
#
47+
# Sub-directory project (mix.exs lives in a subfolder — applies to
48+
# burble's server/, feedback-o-tron, verisimdb):
49+
#
50+
# jobs:
51+
# elixir-ci:
52+
# uses: hyperpolymath/standards/.github/workflows/elixir-ci-reusable.yml@main
53+
# with:
54+
# working_directory: server
4655

4756
name: Elixir CI (reusable)
4857

@@ -74,6 +83,16 @@ on:
7483
type: boolean
7584
required: false
7685
default: true
86+
working_directory:
87+
description: |
88+
Directory containing `mix.exs` (relative to the repo root). All
89+
mix invocations and cache lookups consult this directory.
90+
Default `.` keeps single-app repos unchanged. Set to e.g.
91+
`server` for a sub-app layout (burble, feedback-o-tron,
92+
verisimdb at audit time).
93+
type: string
94+
required: false
95+
default: "."
7796

7897
permissions:
7998
contents: read
@@ -83,11 +102,14 @@ jobs:
83102
name: Compile + test
84103
runs-on: ${{ inputs.runs-on }}
85104
# Guard on mix.exs so the wrapper is safe to add unconditionally.
86-
if: hashFiles('mix.exs') != ''
105+
if: hashFiles(format('{0}/mix.exs', inputs.working_directory)) != ''
87106
permissions:
88107
contents: read
89108
env:
90109
MIX_ENV: test
110+
defaults:
111+
run:
112+
working-directory: ${{ inputs.working_directory }}
91113
steps:
92114
- name: Checkout repository
93115
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -104,14 +126,14 @@ jobs:
104126
- name: Cache deps
105127
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
106128
with:
107-
path: deps
108-
key: deps-${{ inputs.elixir-version }}-${{ hashFiles('mix.lock') }}
129+
path: ${{ inputs.working_directory }}/deps
130+
key: deps-${{ inputs.elixir-version }}-${{ hashFiles(format('{0}/mix.lock', inputs.working_directory)) }}
109131

110132
- name: Cache _build
111133
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
112134
with:
113-
path: _build
114-
key: build-${{ inputs.elixir-version }}-${{ hashFiles('mix.lock') }}
135+
path: ${{ inputs.working_directory }}/_build
136+
key: build-${{ inputs.elixir-version }}-${{ hashFiles(format('{0}/mix.lock', inputs.working_directory)) }}
115137

116138
- name: Install deps
117139
run: mix deps.get

.github/workflows/rust-ci-reusable.yml

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,25 @@
2929
# with:
3030
# enable_audit: true
3131
# enable_coverage: true
32+
#
33+
# Sub-crate / monorepo workspace (Cargo.toml lives in a subdirectory):
34+
#
35+
# jobs:
36+
# rust-ci-cli:
37+
# uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@main
38+
# with:
39+
# working_directory: crates/cli
40+
# rust-ci-server:
41+
# uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@main
42+
# with:
43+
# working_directory: crates/server
44+
#
45+
# Out-of-scope (left bespoke per-repo): multi-OS matrices, cross-compile
46+
# (`cross build`), multi-Rust-version matrices. Each has too much per-repo
47+
# variance to share a single abstraction cleanly (verified against the 5
48+
# matrix-using repos as of 2026-05-26: julia-the-viper / verisimdb /
49+
# verisimiser / reasonably-good-token-vault use four different matrix
50+
# dimensions).
3251

3352
name: Rust CI (reusable)
3453

@@ -65,6 +84,17 @@ on:
6584
type: string
6685
required: false
6786
default: "--all-targets"
87+
working_directory:
88+
description: |
89+
Directory containing `Cargo.toml` (relative to the repo root). All
90+
cargo invocations cd into this directory; `hashFiles()` guards also
91+
consult it. Default `.` keeps single-crate repos unchanged. Set
92+
to e.g. `crates/server` for a sub-crate, or pass a different
93+
value per wrapper call when running the reusable in a workspace
94+
via separate jobs.
95+
type: string
96+
required: false
97+
default: "."
6898

6999
permissions:
70100
contents: read
@@ -76,9 +106,12 @@ jobs:
76106
check:
77107
name: Cargo check + clippy + fmt
78108
runs-on: ${{ inputs.runs-on }}
79-
if: hashFiles('Cargo.toml') != ''
109+
if: hashFiles(format('{0}/Cargo.toml', inputs.working_directory)) != ''
80110
permissions:
81111
contents: read
112+
defaults:
113+
run:
114+
working-directory: ${{ inputs.working_directory }}
82115
steps:
83116
- name: Checkout repository
84117
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -93,6 +126,8 @@ jobs:
93126

94127
- name: Cache cargo registry and build
95128
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
129+
with:
130+
workspaces: ${{ inputs.working_directory }}
96131

97132
- name: Cargo check
98133
run: cargo check ${{ inputs.check_args }}
@@ -107,9 +142,12 @@ jobs:
107142
name: Cargo test
108143
runs-on: ${{ inputs.runs-on }}
109144
needs: check
110-
if: hashFiles('Cargo.toml') != ''
145+
if: hashFiles(format('{0}/Cargo.toml', inputs.working_directory)) != ''
111146
permissions:
112147
contents: read
148+
defaults:
149+
run:
150+
working-directory: ${{ inputs.working_directory }}
113151
steps:
114152
- name: Checkout repository
115153
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -122,6 +160,8 @@ jobs:
122160

123161
- name: Cache cargo registry and build
124162
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
163+
with:
164+
workspaces: ${{ inputs.working_directory }}
125165

126166
- name: Run tests
127167
run: cargo test ${{ inputs.test_args }}
@@ -139,9 +179,12 @@ jobs:
139179
audit:
140180
name: Cargo audit (security)
141181
runs-on: ${{ inputs.runs-on }}
142-
if: ${{ inputs.enable_audit && hashFiles('Cargo.toml') != '' }}
182+
if: ${{ inputs.enable_audit && hashFiles(format('{0}/Cargo.toml', inputs.working_directory)) != '' }}
143183
permissions:
144184
contents: read
185+
defaults:
186+
run:
187+
working-directory: ${{ inputs.working_directory }}
145188
steps:
146189
- name: Checkout repository
147190
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -164,9 +207,12 @@ jobs:
164207
coverage:
165208
name: Coverage (tarpaulin + codecov)
166209
runs-on: ${{ inputs.runs-on }}
167-
if: ${{ inputs.enable_coverage && hashFiles('Cargo.toml') != '' }}
210+
if: ${{ inputs.enable_coverage && hashFiles(format('{0}/Cargo.toml', inputs.working_directory)) != '' }}
168211
permissions:
169212
contents: read
213+
defaults:
214+
run:
215+
working-directory: ${{ inputs.working_directory }}
170216
steps:
171217
- name: Checkout repository
172218
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

0 commit comments

Comments
 (0)