Skip to content

fix: the seven hex.audit advisories, and seven image justifications the scan retired #796

fix: the seven hex.audit advisories, and seven image justifications the scan retired

fix: the seven hex.audit advisories, and seven image justifications the scan retired #796

Workflow file for this run

# 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 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.
- name: The FIPS tests by name, and this leg's coverage
run: |
mix test --trace test/fips
mix test --cover | grep -E 'Result:|\| *Total'