Repository navigation
299 lines (281 loc) · 12.1 KB
/
Copy pathelixir-ci-reusable.yml
File metadata and controls
299 lines (281 loc) · 12.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# This workflow is managed by gh actions-lock.
# SPDX-License-Identifier: MPL-2.0
# This workflow is managed by gh actions-lock.
# 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@571cc734cd69fb846032ec77a662aa8ee4fc32cd
#
# With dialyzer + customised versions:
#
# jobs:
# elixir-ci:
# uses: hyperpolymath/standards/.github/workflows/elixir-ci-reusable.yml@571cc734cd69fb846032ec77a662aa8ee4fc32cd
# 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@571cc734cd69fb846032ec77a662aa8ee4fc32cd
# with:
# working_directory: server
#
# Ecto / ArangoDB test suites (the `mix test` alias creates and migrates a
# database before any test runs, so without a server the job dies at
# `ecto.create` — bofig lived this). Pass a DIGEST-PINNED image; the
# database is started on the runner's loopback at its default port with
# authentication off, so whatever test credentials config/test.exs uses
# are accepted:
#
# jobs:
# elixir-ci:
# uses: hyperpolymath/standards/.github/workflows/elixir-ci-reusable.yml@<sha>
# with:
# postgres-image: "postgres:16@sha256:<digest>"
# arangodb-image: "arangodb:3.12@sha256:<digest>"
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"
rebar3-version:
description: >-
rebar3 version for erlef/setup-beam. ONLY set this when a dependency is
rebar-based; `mix` installs the rebar it needs on demand, so most Elixir
repos should leave this empty. Setting it installs rebar3 from GitHub
releases, resolved through api.github.com — pass `github-token` as well
so those lookups are authenticated. NOTE: this does NOT replace the
`mix local.rebar` call setup-beam makes when `install-rebar` is true
(the default); that runs first and is gated only by that input.
type: string
required: false
default: ""
github-token:
description: >-
Optional token forwarded to erlef/setup-beam, used solely to
authenticate its api.github.com lookups. Pass the caller's
`secrets.GITHUB_TOKEN`. Required in practice when
`rebar3-version` is set; harmless when it is empty.
type: string
required: false
default: ""
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: "."
postgres-image:
description: >-
Optional PostgreSQL image (pin it by digest). When set, the server
runs on 127.0.0.1:5432 with trust authentication for the test job.
Empty (the default) starts nothing, so existing callers are
unchanged. Started with `docker run`, not a `services:` block, so
the job stays valid on runners that have no service-container
support (tma-mark2 calls this on a self-hosted runner).
type: string
required: false
default: ""
arangodb-image:
description: >-
Optional ArangoDB image (pin it by digest). When set, the server
runs on 127.0.0.1:8529 with authentication disabled for the test
job. Empty (the default) starts nothing.
type: string
required: false
default: ""
# Only `contents: read` is requested. A reusable workflow may narrow the
# caller's permissions but never widen them: requesting a permission the
# caller has not granted aborts the run as `startup_failure` with ZERO
# jobs — no logs, no red step, just an empty run. The previous
# `actions: read` here was used by no job in this file, so every caller
# granting only `contents: read` (the estate default) failed to start.
permissions:
contents: read
jobs:
# Guard on mix.exs so the wrapper is safe to add unconditionally. This must
# be a checked-out step, NOT a job-level `if: hashFiles(...)`: job `if:` is
# evaluated server-side before any checkout, so hashFiles() sees an empty
# workspace and always returns '' — which silently skipped the test job on
# EVERY repo, Elixir or not. The detect job checks out and exposes a real
# boolean output the test job gates on.
detect:
timeout-minutes: 5
name: Detect mix.exs
runs-on: ${{ inputs.runs-on }}
permissions:
contents: read
outputs:
has_mix: ${{ steps.detect.outputs.has_mix }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}
- name: Detect mix.exs
id: detect
run: |
if [ -f "${{ inputs.working_directory }}/mix.exs" ]; then
echo "has_mix=true" >> "$GITHUB_OUTPUT"
else
echo "has_mix=false" >> "$GITHUB_OUTPUT"
fi
test:
timeout-minutes: 20
name: Compile + test
runs-on: ${{ inputs.runs-on }}
needs: detect
if: ${{ needs.detect.outputs.has_mix == 'true' }}
permissions:
contents: read
env:
MIX_ENV: test
defaults:
run:
working-directory: ${{ inputs.working_directory }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ github.repository }}
ref: ${{ github.ref }}
# Opt-in databases start before the BEAM toolchain is installed so
# they warm up while deps compile. Images arrive through env, never
# interpolated into the script, so a caller value cannot inject shell.
- name: Start PostgreSQL (opt-in)
if: ${{ inputs.postgres-image != '' }}
env:
IMAGE: ${{ inputs.postgres-image }}
run: docker run --detach --name ci-postgres --publish 127.0.0.1:5432:5432 --env POSTGRES_HOST_AUTH_METHOD=trust "$IMAGE"
- name: Start ArangoDB (opt-in)
if: ${{ inputs.arangodb-image != '' }}
env:
IMAGE: ${{ inputs.arangodb-image }}
run: docker run --detach --name ci-arangodb --publish 127.0.0.1:8529:8529 --env ARANGO_NO_AUTH=1 "$IMAGE"
- name: Guard — rebar3 pinning needs an authenticated lookup
if: ${{ inputs.rebar3-version != '' && inputs.github-token == '' }}
run: |
echo "::warning title=rebar3-version set without github-token::setup-beam resolves rebar3-version through unauthenticated api.github.com calls, which are rate-limited on the shared GitHub-hosted runner ranges and can fail this step within seconds. Pass the github-token input (secrets.GITHUB_TOKEN) or drop rebar3-version unless a dependency is rebar-based."
- name: Set up BEAM (OTP + Elixir)
uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with:
otp-version: ${{ inputs.otp-version }}
elixir-version: ${{ inputs.elixir-version }}
rebar3-version: ${{ inputs.rebar3-version }}
github-token: ${{ inputs.github-token }}
- name: Cache deps
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
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@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
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
# A database that never came up must fail here, by name, rather than
# as an opaque connection error inside the test alias.
- name: Wait for opt-in databases
if: ${{ inputs.postgres-image != '' || inputs.arangodb-image != '' }}
env:
WANT_PG: ${{ inputs.postgres-image != '' }}
WANT_ARANGO: ${{ inputs.arangodb-image != '' }}
run: |
for _ in $(seq 1 60); do
pg_ok=true; arango_ok=true
if [ "$WANT_PG" = true ]; then
docker exec ci-postgres pg_isready --host 127.0.0.1 --username postgres >/dev/null || pg_ok=false
fi
if [ "$WANT_ARANGO" = true ]; then
curl --silent --fail --output /dev/null http://127.0.0.1:8529/_api/version || arango_ok=false
fi
if $pg_ok && $arango_ok; then echo "databases ready"; exit 0; fi
sleep 2
done
echo "::error title=database not ready::postgres ready=$pg_ok, arangodb ready=$arango_ok after 120s"
docker ps --all
[ "$WANT_PG" = true ] && docker logs ci-postgres | tail -n 40
[ "$WANT_ARANGO" = true ] && docker logs ci-arangodb | tail -n 40
exit 1
- name: Run tests
run: mix test --cover