Repository navigation
420 lines (387 loc) · 23.2 KB
/
Copy pathgate.yml
File metadata and controls
420 lines (387 loc) · 23.2 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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# SPDX-FileCopyrightText: Sudo Apt Holdings LLC
# SPDX-License-Identifier: Apache-2.0
name: gate
on:
push:
pull_request:
# Least privilege for the workflow's token, added 2026-09-23 after OpenSSF Scorecard scored this
# repository 0 on Token-Permissions: neither this workflow nor `package.yml` declared any
# top-level permissions, so both ran with whatever the repository default happened to be. Every
# job here reads the tree and writes nothing back, so the whole workflow gets read and any job
# that later needs more has to say so itself and be seen saying it in review.
permissions:
contents: read
jobs:
gate:
runs-on: ubuntu-latest
env:
MIX_ENV: test
TRINITY_DB: sqlite
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0 # the DCO check below reads the whole history
# On a pull request the default checkout is a merge commit GitHub makes on the fly,
# authored by nobody and signed off by nobody, which the DCO check below rightly
# refuses. The branch head is what was written and signed, so that is what the gate
# reads. The repository ruleset requires a branch to be current with main before it
# can merge, so the head is also what main will contain.
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with:
version-file: .tool-versions
version-type: strict
install-hex: false
# Hex is pinned to the archive the image pins (ci/ironbank/hardening_manifest.yaml), by the
# SHA-512 builds.hex.pm publishes for it in installs/hex.csv. setup-beam's own install takes
# the newest Hex, and the bill of materials names the running Hex, so a Hex release turned
# `trinity.third_party_licenses --check` red on every branch (2.5.3, 2026-10-14) while
# nothing in the tree had changed.
- name: Hex 2.5.1, pinned as the image pins it
run: |
curl -fsSL -o /tmp/hex-2.5.1-otp-28.ez https://builds.hex.pm/installs/1.20.0/hex-2.5.1-otp-28.ez
echo "690f12f6933088bb32f1d7125485fb13426318c03cfeabcbd3cce55824cddf2a701104b09b7f4cd7092fe7f76d5b669e391a072428ec4526879b36f62820ae5d /tmp/hex-2.5.1-otp-28.ez" | sha512sum -c -
mix archive.install /tmp/hex-2.5.1-otp-28.ez --force
mix hex.info | head -1
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-${{ hashFiles('mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- run: mix deps.get
# Developer Certificate of Origin: every commit carries a sign-off (ADR-0012).
# Slice 128. This used to concatenate every message in the range and grep for one
# `Signed-off-by:` anywhere, under this same name: a range of five commits where one was
# signed passed a step called "on every commit". Now it asks per commit and prints the ones
# that fail.
#
# `--no-merges` for the reason `test/dco_test.exs` gives at length: a sign-off certifies the
# right to submit work, and a forge's merge commit introduces none. That test checks the whole
# history and runs on every leg; this step is the fast one that fails a pull request early,
# and the two agree on what they mean by "every commit".
# Slice 128. The owner's rule is that no assistant attribution appears anywhere, and a pull
# request description is somewhere: it is the first thing a reader of this project sees, and
# seven of them carried one on 2026-09-26 before a person noticed. The hook covers commit
# messages and plan_check covers history; both look at the tree, and a description is not in
# it.
#
# The body goes through `env` rather than into the script: `${{ }}` interpolated into a shell
# command is a script injection, and a pull request description is attacker-controlled text.
- name: No assistant attribution in the pull request description
if: github.event_name == 'pull_request'
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
if printf '%s' "$PR_BODY" | grep -qiE 'Co-Authored-By: Claude|Claude-Session:|Generated with \[Claude'; then
echo "::error::the pull request description carries an assistant attribution; remove it"
exit 1
fi
- name: DCO sign-off on every commit that carries work
run: |
range="${{ github.event.pull_request.base.sha || 'HEAD~1' }}..HEAD"
unsigned=$(git log --no-merges --format='%H%x09%(trailers:key=Signed-off-by,valueonly)' "$range" \
| awk -F'\t' '$2 == "" { print $1 }')
if [ -n "$unsigned" ]; then
for c in $unsigned; do
echo "::error::commit $c has no Signed-off-by line: $(git log -1 --format=%s "$c")"
done
exit 1
fi
- run: mix gate
# Slice 137, AC2: a real boot on an Erlang runtime below the CVE-2026-55737 floor refuses.
# OTP 28.5.0.3 (erts 16.4.0.3) is the release just before the fix, prebuilt by Hex for this
# runner's OS and pinned by the sha256 builds.hex.pm publishes in builds.txt. The tree is the
# one `mix gate` just compiled on the pinned OTP; only the runtime is older. `--only` with a
# tag that selects nothing makes `mix test` exit non-zero, so this cannot pass by running
# no test.
- name: A boot on OTP 28.5.0.3, below the floor, refuses
run: |
curl -fsSL -o /tmp/otp-28.5.0.3.tar.gz https://builds.hex.pm/builds/otp/ubuntu-24.04/OTP-28.5.0.3.tar.gz
echo "d4116a141da5081b849bd057a30679b48f229fb41dbe88da171739883a4797fb /tmp/otp-28.5.0.3.tar.gz" | sha256sum -c -
mkdir -p /tmp/otp-28.5.0.3
tar -xzf /tmp/otp-28.5.0.3.tar.gz -C /tmp/otp-28.5.0.3 --strip-components=1
(cd /tmp/otp-28.5.0.3 && ./Install -minimal "$PWD" > /dev/null)
TRINITY_BELOW_FLOOR_OTP=/tmp/otp-28.5.0.3/bin mix test --only below_floor_otp test/trinity/toolchain_node_test.exs
# Slice 141: the headless release's floor is OTP 28.5.0.8. OTP 28.5.0.7 is the release just
# before it: a headless node on it must be refused, naming erts and ssl, while a desktop node
# on the same runtime boots. Prebuilt by Hex for this runner's OS,
# pinned by the sha256 in builds.txt (matched a download on 2026-10-20). The untagged case
# beside it runs in `mix gate` on this job's own runtime, where a headless node refuses.
- name: A headless boot on OTP 28.5.0.7, below the headless floor, refuses
run: |
curl -fsSL -o /tmp/otp-28.5.0.7.tar.gz https://builds.hex.pm/builds/otp/ubuntu-24.04/OTP-28.5.0.7.tar.gz
echo "90f1dfa231e1536069e445e0d212a8b8fb57f2e8601c55062f500b2a42699a3d /tmp/otp-28.5.0.7.tar.gz" | sha256sum -c -
mkdir -p /tmp/otp-28.5.0.7
tar -xzf /tmp/otp-28.5.0.7.tar.gz -C /tmp/otp-28.5.0.7 --strip-components=1
(cd /tmp/otp-28.5.0.7 && ./Install -minimal "$PWD" > /dev/null)
TRINITY_BELOW_HEADLESS_FLOOR_OTP=/tmp/otp-28.5.0.7/bin mix test --only below_headless_floor_otp test/trinity/toolchain_node_test.exs
# Slice 120 AC1. `reuse lint` is a Python tool and this is the only leg that installs one,
# deliberately: `mix gate` has to stay runnable inside the FIPS container and on a developer's
# machine without a Python environment, so the REUSE check is a step here rather than a row in
# the alias. `mix trinity.reuse`, which is in the alias, is the cheaper subset that every leg
# can run: it checks that commentable source files carry an SPDX header. This step checks the
# whole specification, including the files that cannot carry a comment and the LICENSES
# directory, and it is what the badge and a donation review actually ask about.
- name: REUSE compliance
run: |
python3 -m venv /tmp/reuse-venv
/tmp/reuse-venv/bin/pip install --quiet reuse
/tmp/reuse-venv/bin/reuse lint
# Slice 010 AC1 and AC2: the same migrations and the same suite on Postgres. A second job
# rather than a matrix entry so the required check keeps its context name, `gate`. This job
# is not required by the ruleset until it has run green on main once; then it is added.
# Tests tagged :sqlite read SQLite pragmas and are excluded here by tag, never by skip.
postgres:
runs-on: ubuntu-latest
services:
postgres:
# Slice 032: pgvector's image of Postgres 17, so the vector extension exists for the
# semantic memory's Postgres store.
image: pgvector/pgvector:pg17
env:
POSTGRES_USER: trinity
POSTGRES_PASSWORD: trinity
POSTGRES_DB: trinity_test
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U trinity"
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
MIX_ENV: test
TRINITY_DB: postgres
DATABASE_URL: postgres://trinity:trinity@localhost:5432/trinity_test
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
# `test/changelog_test.exs` reads `git tag -l 'slice/*'` and refuses to pass without
# tags, because a test that finds nothing to check is indistinguishable from one that
# found everything in order. The default depth of one fetches no tags.
fetch-depth: 0
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with:
version-file: .tool-versions
version-type: strict
install-hex: false
# Hex is pinned to the archive the image pins (ci/ironbank/hardening_manifest.yaml), by the
# SHA-512 builds.hex.pm publishes for it in installs/hex.csv. setup-beam's own install takes
# the newest Hex, and the bill of materials names the running Hex, so a Hex release turned
# `trinity.third_party_licenses --check` red on every branch (2.5.3, 2026-10-14) while
# nothing in the tree had changed.
- name: Hex 2.5.1, pinned as the image pins it
run: |
curl -fsSL -o /tmp/hex-2.5.1-otp-28.ez https://builds.hex.pm/installs/1.20.0/hex-2.5.1-otp-28.ez
echo "690f12f6933088bb32f1d7125485fb13426318c03cfeabcbd3cce55824cddf2a701104b09b7f4cd7092fe7f76d5b669e391a072428ec4526879b36f62820ae5d /tmp/hex-2.5.1-otp-28.ez" | sha512sum -c -
mix archive.install /tmp/hex-2.5.1-otp-28.ez --force
mix hex.info | head -1
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
deps
_build
key: ${{ runner.os }}-mix-postgres-${{ hashFiles('mix.lock') }}
restore-keys: ${{ runner.os }}-mix-postgres-
- run: mix deps.get
# The adapter is chosen at compile time (config/config.exs); this proves the build
# under TRINITY_DB=postgres links the Postgres adapter and nothing SQLite. `--no-start`
# since slice 050: the check needs the compiled module, not a booted application, and
# Oban refuses to start before its table is migrated (run 35711666503, this step booted
# the application before `ecto.reset`).
- name: The compiled adapter is Postgres
run: mix run --no-start -e 'Ecto.Adapters.Postgres = Trinity.Repo.__adapter__()'
- run: mix ecto.reset
- run: mix test --exclude sqlite
# Slice 003: the same gate on an OTP built from source with --enable-fips, in FIPS mode,
# against the FIPS provider of a UBI9 container (docs/fips-leg.md). A second job rather than
# a matrix entry, as `postgres` is, so `gate` keeps its context name. The image is built by
# fips-image.yml and pulled here by the tag scripts/fips_image_tag.sh derives from the
# files that define it; this job never builds it.
fips-tag:
runs-on: ubuntu-latest
# `packages: read` is not decoration. The workflow-level block above narrows every job's token
# to what it names, and before that block existed these two jobs inherited the repository
# default, which grants read on every scope. Declaring `contents: read` at the top therefore
# took `packages: read` away, and this job's `docker login` to the registry stopped being able
# to see the FIPS image: it polled for the full thirty minutes and failed with "was not
# published", while the fips-image workflow had already reported the tag as present. Two
# checks disagreeing about whether an image exists is what an authorization failure looks like
# from the outside. Measured on this branch before it was merged.
permissions:
contents: read
packages: read
outputs:
ref: ${{ steps.tag.outputs.ref }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- id: tag
run: echo "ref=$(scripts/fips_image_tag.sh --ref)" >> "$GITHUB_OUTPUT"
# The image tag is a hash of the files that define the image, so any change to them -
# including a comment - names an image that has to be built before it can be pulled. The
# `fips` job below takes that image as its container, which GitHub pulls before the job's
# first step runs, so the job cannot retry the pull itself: it fails with `manifest
# unknown` and no output. The fips-image workflow builds and pushes on the same event, so
# the two race, and the loser is always this one.
#
# Waiting here turns that race into a wait. The image is a function of its inputs, so the
# one being waited for is exactly the one this commit needs, and a build from source is
# slow enough that the ceiling is generous. A timeout fails with the reason rather than
# with a pull error, because "the image is still building" and "the image will never
# exist" need different responses from whoever reads it.
- name: Wait for the FIPS image to be published
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ref='${{ steps.tag.outputs.ref }}'
echo "$GH_TOKEN" | docker login ghcr.io -u '${{ github.actor }}' --password-stdin
for attempt in $(seq 1 60); do
if docker manifest inspect "$ref" >/dev/null 2>&1; then
echo "$ref is published (attempt $attempt)"
exit 0
fi
echo "attempt $attempt: $ref is not published yet; waiting 30s"
sleep 30
done
echo "::error::$ref was not published within 30 minutes. It is built by the" \
"fips-image workflow, which runs when ci/fips/Containerfile, .tool-versions," \
"ci/container.tool-versions or" \
"scripts/fips_image_tag.sh changes. Check that workflow's run for this commit."
exit 1
fips:
needs: fips-tag
runs-on: ubuntu-latest
# The runner pulls this container before the job's first step, using the credentials below,
# so without `packages: read` the job fails with `manifest unknown` and no output at all.
permissions:
contents: read
packages: read
container:
image: ${{ needs.fips-tag.outputs.ref }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
env:
MIX_ENV: test
TRINITY_DB: sqlite
# test/test_helper.exs includes the :fips tests under this, and test/fips/mode_test.exs
# asserts the runtime agrees, so a leg that failed to enter the mode is red, not quiet.
TRINITY_FIPS_LEG: "1"
# FIPS mode is entered by OTP's own configuration, read by the crypto NIF when it loads,
# which needs the crypto application loaded first; ERL_AFLAGS is prepended to every
# erl command line, so every `mix` and `elixir` here runs in the mode. Measured at G1:
# ERL_FLAGS alone leaves the mode off (the NIF loads before the application does).
ERL_AFLAGS: "-crypto fips_mode true -eval application:load(crypto)"
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha || github.sha }}
# The checkout action marks the workspace safe under a temporary HOME it removes when
# it is done; every later git call in the container (the names, secrets and
# reuse censuses, all of which read `git ls-files`) then hits git's dubious-ownership
# refusal and reads an empty tree, which makes every census pass over nothing.
- run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: |
deps
_build
key: fips-mix-${{ needs.fips-tag.outputs.ref }}-${{ hashFiles('mix.lock') }}
restore-keys: fips-mix-${{ needs.fips-tag.outputs.ref }}-
# The one step outside the mode. The fetch, because Hex's client offers TLS 1.0 and 1.1
# beside 1.2 and ssl in the mode refuses the set (insufficient_crypto_support); the
# dependency compile, because rustler_precompiled downloads mdex's NIF at compile time
# and OTP's TLS 1.3 client in the mode fails that host's HelloRetryRequest
# (erlang/otp#8470). docs/fips-leg.md findings 1 and 2; neither is what the leg
# measures. The gate's own hex.audit step clears ERL_AFLAGS for the same reason (mix.exs).
- name: Fetch and compile dependencies (FIPS mode off)
env:
ERL_AFLAGS: "-eval application:load(crypto)"
run: |
mix deps.get
mix deps.compile
# AC1, before anything else runs: the runtime is in the mode and names the provider.
- name: The runtime is in FIPS mode
run: |
erl -noshell -eval 'enabled = crypto:info_fips(), io:format("~p~n", [crypto:info()]), halt().'
# AC3, the default half: the listing with the mode off equals the committed file; the
# mode-on half is test/fips/mode_test.exs, inside the suite.
- name: crypto:supports/0 with the mode off matches docs/fips-leg/supports-default.txt
env:
ERL_AFLAGS: "-eval application:load(crypto)"
run: elixir scripts/crypto_supports.exs | diff - docs/fips-leg/supports-default.txt
# Slice 128. This used to concatenate every message in the range and grep for one
# `Signed-off-by:` anywhere, under this same name: a range of five commits where one was
# signed passed a step called "on every commit". Now it asks per commit and prints the ones
# that fail.
#
# `--no-merges` for the reason `test/dco_test.exs` gives at length: a sign-off certifies the
# right to submit work, and a forge's merge commit introduces none. That test checks the whole
# history and runs on every leg; this step is the fast one that fails a pull request early,
# and the two agree on what they mean by "every commit".
# Slice 128. The owner's rule is that no assistant attribution appears anywhere, and a pull
# request description is somewhere: it is the first thing a reader of this project sees, and
# seven of them carried one on 2026-09-26 before a person noticed. The hook covers commit
# messages and plan_check covers history; both look at the tree, and a description is not in
# it.
#
# The body goes through `env` rather than into the script: `${{ }}` interpolated into a shell
# command is a script injection, and a pull request description is attacker-controlled text.
- name: No assistant attribution in the pull request description
if: github.event_name == 'pull_request'
env:
PR_BODY: ${{ github.event.pull_request.body }}
run: |
if printf '%s' "$PR_BODY" | grep -qiE 'Co-Authored-By: Claude|Claude-Session:|Generated with \[Claude'; then
echo "::error::the pull request description carries an assistant attribution; remove it"
exit 1
fi
- name: DCO sign-off on every commit that carries work
run: |
range="${{ github.event.pull_request.base.sha || 'HEAD~1' }}..HEAD"
unsigned=$(git log --no-merges --format='%H%x09%(trailers:key=Signed-off-by,valueonly)' "$range" \
| awk -F'\t' '$2 == "" { print $1 }')
if [ -n "$unsigned" ]; then
for c in $unsigned; do
echo "::error::commit $c has no Signed-off-by line: $(git log -1 --format=%s "$c")"
done
exit 1
fi
- run: mix gate
# AC2 and AC3 by test name with this run's id in the log, and AC5's coverage line for
# this leg. The gate above already ran the suite; this repeats the four FIPS tests with
# their names printed, then the whole suite once more under the coverage tool.
#
# Unpiped since the fix after slice 138. It read `mix test --cover | grep -E 'Result:|Total'`,
# and the runner's default `bash -e {0}` has no pipefail, so the step exited with grep's status:
# on job 118381934625 the suite here was 1388/1389 and the step passed, with the failing test's
# name filtered out. The coverage threshold is 0 (mix.exs), so the suite alone decides the
# exit, and the summary's Total line is in the output. test/workflows_test.exs holds the rule.
- name: The FIPS tests by name, and this leg's coverage
run: |
mix test --trace test/fips
mix test --cover
# Slice 137, AC5 and AC7: ML-DSA-87 with the mode off. This image links OpenSSL 3.5, the only
# runtime in CI that signs ML-DSA-87 (the gate runners link 3.0; in the mode, Red Hat's
# provider refuses it), so the ML-DSA-87 path runs here end to end: the planted-step cases,
# a regulated node that boots with ML-DSA-87 receipts and whose boot receipt verifies, and the
# signer's key shapes. Slice 138 adds N6's COSE_Sign1 signing and verification with ML-DSA-87.
# Slice 139 adds the receipt chains signed under ML-DSA-87, which verify here and nowhere else
# in CI (in the mode, and on the gate runners, they are the no-trust branch).
# TRINITY_REQUIRE_MLDSA87_RUNTIME makes test_helper raise if the runtime
# cannot sign, rather than excluding the cases it came to run.
#
# It runs last, from a fresh suite state. The suite keeps its receipt keys in tmp/test_keys
# and its receipts in trinity_test_receipts.db across runs, so a run with the mode off leaves
# Ed25519 and ML-DSA-87 keys and rows that the mode refuses to verify. Run before `mix gate`,
# it turned five of the gate's tests red on PR #132 (`:notsup`, "Unsupported algorithm in
# FIPS mode"; three receipt keys in one export). Nothing in the mode runs after this step.
- name: ML-DSA-87 end to end (FIPS mode off)
env:
ERL_AFLAGS: "-eval application:load(crypto)"
TRINITY_REQUIRE_MLDSA87_RUNTIME: "1"
run: |
rm -rf tmp trinity_test.db* trinity_test_receipts.db*
mix test test/trinity/crypto/preflight_test.exs test/trinity/toolchain_node_test.exs test/trinity/receipts/signer_test.exs test/trinity/n6/cose_signing_test.exs test/trinity/n6/cose_test.exs test/trinity/receipts/foreign_algorithm_test.exs