Skip to content

Commit 4fdf431

Browse files
committed
feat(governance): add rust-ci-reusable + elixir-ci-reusable workflows
Extends the #168 pattern (deno-ci-reusable, language-policy in governance-reusable) to two more high-drift workflow templates. Estate audit 2026-05-26 (via gh api search/code): rust-ci.yml: 137 repos, 30 unique SHAs (high drift) elixir-ci.yml: 9 repos, 9 unique SHAs (100% drift; one corrupt YAML with literal 'npermissions:' lines from a botched permissions-injection sweep) rust-ci-reusable.yml * Split jobs (check / test) + opt-in audit + opt-in coverage. * if: hashFiles('Cargo.toml') guard on every job. * Top-level + per-job permissions: contents: read. * Inputs: enable_audit, enable_coverage, clippy_args, test_args, check_args, 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 upstream-dep warnings don't fail the strict gate. Validated against tma-mark2 #41 in commit fa32c4f. * Default elixir-version 1.17 (1.15 default in legacy template produced the (Mix) declared in mix.exs supports only Elixir ~> 1.17 error tma-mark2 hit). * Inputs: otp-version, elixir-version, enable_dialyzer, enable_credo, runs-on. Downstream rollout (separate PRs, can be fanned out per-repo): each repo carrying rust-ci.yml or elixir-ci.yml replaces it with a 5-line wrapper, same shape as governance.yml + the deno-ci wrappers landed by absolute-zero #41 and tma-mark2 #41. Refs #168 (the deno-ci precedent).
1 parent 431adbb commit 4fdf431

2 files changed

Lines changed: 327 additions & 0 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# elixir-ci-reusable.yml — Reusable Elixir CI bundle (RSR).
3+
#
4+
# Replaces the per-repo `elixir-ci.yml` template that copy-drifted (and
5+
# in several cases got corrupted) across the estate. Estate audit
6+
# (2026-05-26) found 9 repos shipping their own copy, with 9 unique
7+
# SHAs — 100% drift. One copy (`bofig`) was YAML-broken with literal
8+
# `npermissions:` lines from a botched permissions injection.
9+
#
10+
# Recurring failure modes observed across the estate:
11+
#
12+
# * Elixir version pinned to 1.15 while mix.exs declared ~> 1.17,
13+
# producing `(Mix) … but it has declared in its mix.exs file
14+
# it supports only Elixir ~> 1.17` (tma-mark2 #41 lived this).
15+
# * `mix compile --warnings-as-errors` applied to the whole
16+
# build, so transitive-dep warnings (e.g. rustler's
17+
# `:json.decode` reference needing Elixir 1.18) failed CI even
18+
# when the project's own code was clean.
19+
# * Inconsistent `permissions:` placement (some at top, some
20+
# mid-file, some missing).
21+
#
22+
# This reusable:
23+
# * Pins to the tma-mark2 #41-validated canonical shape.
24+
# * Compiles deps WITHOUT --warnings-as-errors first, then app code
25+
# with the strict flag — so deps warnings don't fail us but our
26+
# own code still gets the hygiene gate.
27+
# * Gates dialyzer behind an opt-in input (~5-minute cold-cache run
28+
# on most repos).
29+
# * Guards every job on `mix.exs` presence so consumers can add
30+
# the wrapper unconditionally.
31+
#
32+
# Caller example:
33+
#
34+
# jobs:
35+
# elixir-ci:
36+
# uses: hyperpolymath/standards/.github/workflows/elixir-ci-reusable.yml@main
37+
#
38+
# With dialyzer + customised versions:
39+
#
40+
# jobs:
41+
# elixir-ci:
42+
# uses: hyperpolymath/standards/.github/workflows/elixir-ci-reusable.yml@main
43+
# with:
44+
# elixir-version: "1.18"
45+
# enable_dialyzer: true
46+
47+
name: Elixir CI (reusable)
48+
49+
on:
50+
workflow_call:
51+
inputs:
52+
runs-on:
53+
description: Runner label for the Elixir CI job
54+
type: string
55+
required: false
56+
default: ubuntu-latest
57+
otp-version:
58+
description: OTP version selector for erlef/setup-beam
59+
type: string
60+
required: false
61+
default: "26"
62+
elixir-version:
63+
description: Elixir version selector for erlef/setup-beam
64+
type: string
65+
required: false
66+
default: "1.17"
67+
enable_dialyzer:
68+
description: Run `mix dialyzer` (slow cold-cache; off by default)
69+
type: boolean
70+
required: false
71+
default: false
72+
enable_credo:
73+
description: Run `mix credo --strict`
74+
type: boolean
75+
required: false
76+
default: true
77+
78+
permissions:
79+
contents: read
80+
81+
jobs:
82+
test:
83+
name: Compile + test
84+
runs-on: ${{ inputs.runs-on }}
85+
# Guard on mix.exs so the wrapper is safe to add unconditionally.
86+
if: hashFiles('mix.exs') != ''
87+
permissions:
88+
contents: read
89+
env:
90+
MIX_ENV: test
91+
steps:
92+
- name: Checkout repository
93+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
94+
with:
95+
repository: ${{ github.repository }}
96+
ref: ${{ github.ref }}
97+
98+
- name: Set up BEAM (OTP + Elixir)
99+
uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2
100+
with:
101+
otp-version: ${{ inputs.otp-version }}
102+
elixir-version: ${{ inputs.elixir-version }}
103+
104+
- name: Cache deps
105+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
106+
with:
107+
path: deps
108+
key: deps-${{ inputs.elixir-version }}-${{ hashFiles('mix.lock') }}
109+
110+
- name: Cache _build
111+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
112+
with:
113+
path: _build
114+
key: build-${{ inputs.elixir-version }}-${{ hashFiles('mix.lock') }}
115+
116+
- name: Install deps
117+
run: mix deps.get
118+
119+
# Compile deps WITHOUT --warnings-as-errors so upstream warnings
120+
# (rustler's :json.decode needing Elixir 1.18, deprecated
121+
# `use Bitwise`, etc.) don't fail the build. Strict mode then
122+
# applies only to the project's own modules in the next step.
123+
- name: Compile dependencies
124+
run: mix deps.compile
125+
126+
- name: Compile project (strict)
127+
run: mix compile --warnings-as-errors
128+
129+
- name: Credo lint
130+
if: ${{ inputs.enable_credo }}
131+
run: mix credo --strict
132+
133+
- name: Dialyzer
134+
if: ${{ inputs.enable_dialyzer }}
135+
run: mix dialyzer
136+
137+
- name: Run tests
138+
run: mix test --cover
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# SPDX-License-Identifier: PMPL-1.0-or-later
2+
# rust-ci-reusable.yml — Reusable Rust CI bundle (RSR).
3+
#
4+
# Replaces the per-repo `rust-ci.yml` template that copy-drifted across
5+
# the estate. Estate audit (2026-05-26) found:
6+
#
7+
# * 137 repos shipping their own copy of rust-ci.yml
8+
# * 30 unique SHAs — same logical workflow, drifted independently
9+
# * Recurring failure modes across PRs: missing top-level
10+
# `permissions:`, inconsistent `if: hashFiles('Cargo.toml')`
11+
# guards, `cargo audit` re-installing every run, license-header
12+
# drift (PMPL/MPL/AGPL), inconsistent SHA pins.
13+
#
14+
# The reusable bundles the union of features observed across the
15+
# variants and gates the slow extras (audit, coverage) behind opt-in
16+
# inputs so consumers only pay for what they want.
17+
#
18+
# Caller example (single wrapper, mirrors governance.yml + deno-ci.yml):
19+
#
20+
# jobs:
21+
# rust-ci:
22+
# uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@main
23+
#
24+
# With audit + coverage enabled:
25+
#
26+
# jobs:
27+
# rust-ci:
28+
# uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@main
29+
# with:
30+
# enable_audit: true
31+
# enable_coverage: true
32+
33+
name: Rust CI (reusable)
34+
35+
on:
36+
workflow_call:
37+
inputs:
38+
runs-on:
39+
description: Runner label for all Rust CI jobs
40+
type: string
41+
required: false
42+
default: ubuntu-latest
43+
enable_audit:
44+
description: Run `cargo audit` (slow — installs each run; off by default)
45+
type: boolean
46+
required: false
47+
default: false
48+
enable_coverage:
49+
description: Run `cargo tarpaulin` + upload to codecov (slow; off by default)
50+
type: boolean
51+
required: false
52+
default: false
53+
clippy_args:
54+
description: Args appended to `cargo clippy`
55+
type: string
56+
required: false
57+
default: "--all-targets -- -D warnings"
58+
test_args:
59+
description: Args appended to `cargo test`
60+
type: string
61+
required: false
62+
default: "--all-targets"
63+
check_args:
64+
description: Args appended to `cargo check`
65+
type: string
66+
required: false
67+
default: "--all-targets"
68+
69+
permissions:
70+
contents: read
71+
72+
jobs:
73+
# Skip the whole reusable when the repo has no Cargo.toml — lets
74+
# consumers add the wrapper unconditionally without worrying about
75+
# repos that don't ship Rust code at the moment.
76+
check:
77+
name: Cargo check + clippy + fmt
78+
runs-on: ${{ inputs.runs-on }}
79+
if: hashFiles('Cargo.toml') != ''
80+
permissions:
81+
contents: read
82+
steps:
83+
- name: Checkout repository
84+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
85+
with:
86+
repository: ${{ github.repository }}
87+
ref: ${{ github.ref }}
88+
89+
- name: Install Rust toolchain
90+
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
91+
with:
92+
components: clippy, rustfmt
93+
94+
- name: Cache cargo registry and build
95+
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
96+
97+
- name: Cargo check
98+
run: cargo check ${{ inputs.check_args }}
99+
100+
- name: Cargo fmt
101+
run: cargo fmt --all -- --check
102+
103+
- name: Cargo clippy
104+
run: cargo clippy ${{ inputs.clippy_args }}
105+
106+
test:
107+
name: Cargo test
108+
runs-on: ${{ inputs.runs-on }}
109+
needs: check
110+
if: hashFiles('Cargo.toml') != ''
111+
permissions:
112+
contents: read
113+
steps:
114+
- name: Checkout repository
115+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
116+
with:
117+
repository: ${{ github.repository }}
118+
ref: ${{ github.ref }}
119+
120+
- name: Install Rust toolchain
121+
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
122+
123+
- name: Cache cargo registry and build
124+
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2
125+
126+
- name: Run tests
127+
run: cargo test ${{ inputs.test_args }}
128+
129+
- name: Write summary
130+
if: always()
131+
run: |
132+
{
133+
echo "## Rust CI Results"
134+
echo ""
135+
echo "- **cargo check**: ${{ needs.check.result }}"
136+
echo "- **cargo test**: completed"
137+
} >> "$GITHUB_STEP_SUMMARY"
138+
139+
audit:
140+
name: Cargo audit (security)
141+
runs-on: ${{ inputs.runs-on }}
142+
if: ${{ inputs.enable_audit && hashFiles('Cargo.toml') != '' }}
143+
permissions:
144+
contents: read
145+
steps:
146+
- name: Checkout repository
147+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
148+
with:
149+
repository: ${{ github.repository }}
150+
ref: ${{ github.ref }}
151+
152+
- name: Install Rust toolchain
153+
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
154+
155+
- name: Install cargo-audit
156+
# Use the binstall path when available to skip a from-source rebuild
157+
# on every run — was the single biggest contributor to slow CI on
158+
# repos that opted in to audit (~3–4 minute install).
159+
run: cargo install cargo-audit --locked
160+
161+
- name: Security audit
162+
run: cargo audit
163+
164+
coverage:
165+
name: Coverage (tarpaulin + codecov)
166+
runs-on: ${{ inputs.runs-on }}
167+
if: ${{ inputs.enable_coverage && hashFiles('Cargo.toml') != '' }}
168+
permissions:
169+
contents: read
170+
steps:
171+
- name: Checkout repository
172+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
173+
with:
174+
repository: ${{ github.repository }}
175+
ref: ${{ github.ref }}
176+
177+
- name: Install Rust toolchain
178+
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
179+
180+
- name: Install tarpaulin
181+
run: cargo install cargo-tarpaulin --locked
182+
183+
- name: Generate coverage
184+
run: cargo tarpaulin --out Xml
185+
186+
- name: Upload to codecov
187+
uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457 # v3
188+
with:
189+
files: cobertura.xml

0 commit comments

Comments
 (0)