Skip to content
Open
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
160 changes: 160 additions & 0 deletions .github/workflows/elixir-ci-reusable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# SPDX-License-Identifier: PMPL-1.0-or-later
# elixir-ci-reusable.yml — Reusable Elixir CI bundle (RSR).
#
# Replaces the per-repo `elixir-ci.yml` template that copy-drifted (and
# in several cases got corrupted) across the estate. Estate audit
# (2026-05-26) found 9 repos shipping their own copy, with 9 unique
# SHAs — 100% drift. One copy (`bofig`) was YAML-broken with literal
# `npermissions:` lines from a botched permissions injection.
#
# Recurring failure modes observed across the estate:
#
# * Elixir version pinned to 1.15 while mix.exs declared ~> 1.17,
# producing `(Mix) … but it has declared in its mix.exs file
# it supports only Elixir ~> 1.17` (tma-mark2 #41 lived this).
# * `mix compile --warnings-as-errors` applied to the whole
# build, so transitive-dep warnings (e.g. rustler's
# `:json.decode` reference needing Elixir 1.18) failed CI even
# when the project's own code was clean.
# * Inconsistent `permissions:` placement (some at top, some
# mid-file, some missing).
#
# This reusable:
# * Pins to the tma-mark2 #41-validated canonical shape.
# * Compiles deps WITHOUT --warnings-as-errors first, then app code
# with the strict flag — so deps warnings don't fail us but our
# own code still gets the hygiene gate.
# * Gates dialyzer behind an opt-in input (~5-minute cold-cache run
# on most repos).
# * Guards every job on `mix.exs` presence so consumers can add
# the wrapper unconditionally.
#
# Caller example:
#
# jobs:
# elixir-ci:
# uses: hyperpolymath/standards/.github/workflows/elixir-ci-reusable.yml@main
#
# With dialyzer + customised versions:
#
# jobs:
# elixir-ci:
# uses: hyperpolymath/standards/.github/workflows/elixir-ci-reusable.yml@main
# with:
# elixir-version: "1.18"
# enable_dialyzer: true
#
# Sub-directory project (mix.exs lives in a subfolder — applies to
# burble's server/, feedback-o-tron, verisimdb):
#
# jobs:
# elixir-ci:
# uses: hyperpolymath/standards/.github/workflows/elixir-ci-reusable.yml@main
# with:
# working_directory: server

name: Elixir CI (reusable)

on:
workflow_call:
inputs:
runs-on:
description: Runner label for the Elixir CI job
type: string
required: false
default: ubuntu-latest
otp-version:
description: OTP version selector for erlef/setup-beam
type: string
required: false
default: "26"
elixir-version:
description: Elixir version selector for erlef/setup-beam
type: string
required: false
default: "1.17"
enable_dialyzer:
description: Run `mix dialyzer` (slow cold-cache; off by default)
type: boolean
required: false
default: false
enable_credo:
description: Run `mix credo --strict`
type: boolean
required: false
default: true
working_directory:
description: |
Directory containing `mix.exs` (relative to the repo root). All
mix invocations and cache lookups consult this directory.
Default `.` keeps single-app repos unchanged. Set to e.g.
`server` for a sub-app layout (burble, feedback-o-tron,
verisimdb at audit time).
type: string
required: false
default: "."

permissions:
contents: read

jobs:
test:
name: Compile + test
runs-on: ${{ inputs.runs-on }}
# Guard on mix.exs so the wrapper is safe to add unconditionally.
if: hashFiles(format('{0}/mix.exs', inputs.working_directory)) != ''
permissions:
contents: read
env:
MIX_ENV: test
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}

- name: Set up BEAM (OTP + Elixir)
uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2
with:
otp-version: ${{ inputs.otp-version }}
elixir-version: ${{ inputs.elixir-version }}

- name: Cache deps
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ${{ inputs.working_directory }}/deps
key: deps-${{ inputs.elixir-version }}-${{ hashFiles(format('{0}/mix.lock', inputs.working_directory)) }}

- name: Cache _build
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ${{ inputs.working_directory }}/_build
key: build-${{ inputs.elixir-version }}-${{ hashFiles(format('{0}/mix.lock', inputs.working_directory)) }}

- name: Install deps
run: mix deps.get

# Compile deps WITHOUT --warnings-as-errors so upstream warnings
# (rustler's :json.decode needing Elixir 1.18, deprecated
# `use Bitwise`, etc.) don't fail the build. Strict mode then
# applies only to the project's own modules in the next step.
- name: Compile dependencies
run: mix deps.compile

- name: Compile project (strict)
run: mix compile --warnings-as-errors

- name: Credo lint
if: ${{ inputs.enable_credo }}
run: mix credo --strict

- name: Dialyzer
if: ${{ inputs.enable_dialyzer }}
run: mix dialyzer

- name: Run tests
run: mix test --cover
235 changes: 235 additions & 0 deletions .github/workflows/rust-ci-reusable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
# SPDX-License-Identifier: PMPL-1.0-or-later
# rust-ci-reusable.yml — Reusable Rust CI bundle (RSR).
#
# Replaces the per-repo `rust-ci.yml` template that copy-drifted across
# the estate. Estate audit (2026-05-26) found:
#
# * 137 repos shipping their own copy of rust-ci.yml
# * 30 unique SHAs — same logical workflow, drifted independently
# * Recurring failure modes across PRs: missing top-level
# `permissions:`, inconsistent `if: hashFiles('Cargo.toml')`
# guards, `cargo audit` re-installing every run, license-header
# drift (PMPL/MPL/AGPL), inconsistent SHA pins.
#
# The reusable bundles the union of features observed across the
# variants and gates the slow extras (audit, coverage) behind opt-in
# inputs so consumers only pay for what they want.
#
# Caller example (single wrapper, mirrors governance.yml + deno-ci.yml):
#
# jobs:
# rust-ci:
# uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@main
#
# With audit + coverage enabled:
#
# jobs:
# rust-ci:
# uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@main
# with:
# enable_audit: true
# enable_coverage: true
#
# Sub-crate / monorepo workspace (Cargo.toml lives in a subdirectory):
#
# jobs:
# rust-ci-cli:
# uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@main
# with:
# working_directory: crates/cli
# rust-ci-server:
# uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@main
# with:
# working_directory: crates/server
#
# Out-of-scope (left bespoke per-repo): multi-OS matrices, cross-compile
# (`cross build`), multi-Rust-version matrices. Each has too much per-repo
# variance to share a single abstraction cleanly (verified against the 5
# matrix-using repos as of 2026-05-26: julia-the-viper / verisimdb /
# verisimiser / reasonably-good-token-vault use four different matrix
# dimensions).

name: Rust CI (reusable)

on:
workflow_call:
inputs:
runs-on:
description: Runner label for all Rust CI jobs
type: string
required: false
default: ubuntu-latest
enable_audit:
description: Run `cargo audit` (slow — installs each run; off by default)
type: boolean
required: false
default: false
enable_coverage:
description: Run `cargo tarpaulin` + upload to codecov (slow; off by default)
type: boolean
required: false
default: false
clippy_args:
description: Args appended to `cargo clippy`
type: string
required: false
default: "--all-targets -- -D warnings"
test_args:
description: Args appended to `cargo test`
type: string
required: false
default: "--all-targets"
check_args:
description: Args appended to `cargo check`
type: string
required: false
default: "--all-targets"
working_directory:
description: |
Directory containing `Cargo.toml` (relative to the repo root). All
cargo invocations cd into this directory; `hashFiles()` guards also
consult it. Default `.` keeps single-crate repos unchanged. Set
to e.g. `crates/server` for a sub-crate, or pass a different
value per wrapper call when running the reusable in a workspace
via separate jobs.
type: string
required: false
default: "."

permissions:
contents: read

jobs:
# Skip the whole reusable when the repo has no Cargo.toml — lets
# consumers add the wrapper unconditionally without worrying about
# repos that don't ship Rust code at the moment.
check:
name: Cargo check + clippy + fmt
runs-on: ${{ inputs.runs-on }}
if: hashFiles(format('{0}/Cargo.toml', inputs.working_directory)) != ''
permissions:
contents: read
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable
with:
components: clippy, rustfmt

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

- name: Cargo check
run: cargo check ${{ inputs.check_args }}

- name: Cargo fmt
run: cargo fmt --all -- --check

- name: Cargo clippy
run: cargo clippy ${{ inputs.clippy_args }}

test:
name: Cargo test
runs-on: ${{ inputs.runs-on }}
needs: check
if: hashFiles(format('{0}/Cargo.toml', inputs.working_directory)) != ''
permissions:
contents: read
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable

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

- name: Run tests
run: cargo test ${{ inputs.test_args }}

- name: Write summary
if: always()
run: |
{
echo "## Rust CI Results"
echo ""
echo "- **cargo check**: ${{ needs.check.result }}"
echo "- **cargo test**: completed"
} >> "$GITHUB_STEP_SUMMARY"

audit:
name: Cargo audit (security)
runs-on: ${{ inputs.runs-on }}
if: ${{ inputs.enable_audit && hashFiles(format('{0}/Cargo.toml', inputs.working_directory)) != '' }}
permissions:
contents: read
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable

- name: Install cargo-audit
# Use the binstall path when available to skip a from-source rebuild
# on every run — was the single biggest contributor to slow CI on
# repos that opted in to audit (~3–4 minute install).
run: cargo install cargo-audit --locked

- name: Security audit
run: cargo audit

coverage:
name: Coverage (tarpaulin + codecov)
runs-on: ${{ inputs.runs-on }}
if: ${{ inputs.enable_coverage && hashFiles(format('{0}/Cargo.toml', inputs.working_directory)) != '' }}
permissions:
contents: read
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable

- name: Install tarpaulin
run: cargo install cargo-tarpaulin --locked

- name: Generate coverage
run: cargo tarpaulin --out Xml

- name: Upload to codecov
uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457 # v3
with:
files: cobertura.xml