diff --git a/.github/workflows/elixir-ci-reusable.yml b/.github/workflows/elixir-ci-reusable.yml new file mode 100644 index 00000000..584f7136 --- /dev/null +++ b/.github/workflows/elixir-ci-reusable.yml @@ -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 diff --git a/.github/workflows/rust-ci-reusable.yml b/.github/workflows/rust-ci-reusable.yml new file mode 100644 index 00000000..b61f0118 --- /dev/null +++ b/.github/workflows/rust-ci-reusable.yml @@ -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