Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,16 @@ name: package
# slice 001 had already proven. It now runs when a slice or release tag is pushed, on a
# change to something packaging actually depends on, or by hand. A slice that touches
# packaging asks for the run with `workflow_dispatch` and cites the run id in its proof.
# Corrected again after slice 062. The tags-only trigger meant a change to a packaging input
# (a dependency, the toolchain, the Tauri shell, config) was first packaged at the tag, which is
# after the owner has approved the slice: slice 062 added `jose` and the first packaging run of
# it was the tag's, which failed. Branches are added beside tags, with the same path filter, so
# a change to those inputs is packaged on the branch that makes it. This is not the guard for a
# prod-only code defect: the filter below does not match `lib/`, and `scripts/prod_check.sh` in
# `mix gate` is what catches that class on every push.
on:
push:
branches: ['**']
tags: ['slice/**', 'v*']
paths:
- mix.exs
Expand Down
8 changes: 7 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ If ROADMAP.md shows no slice `in_progress` and the previous slice is not `approv

A slice is done only when ALL of these are true:

1. `mix gate` passes (format check, compile with warnings-as-errors, credo --strict, boundary, tests, audits).
1. `mix gate` passes (format check, compile with warnings-as-errors, boundary, **the release check**,
credo --strict, sobelow, tests, audits, plan check). The release check is `scripts/prod_check.sh`:
the tree compiles under `MIX_ENV=prod`, the headless release assembles, and it evaluates
`config/runtime.exs`'s prod branch. It exists because `MIX_ENV=test` says nothing about what
ships: a plug's `init/1` is called at compile time under prod, and slice 062 shipped a closure
there that broke the release while every gate stayed green (slice 062 NOTES, F5).
2. Every acceptance criterion has a corresponding proof entry (command + output, or screenshot for UI).
3. New behaviours/public modules have `@moduledoc` and `@doc`; new behaviours have typespecs.
4. Tests exist for new logic (unit for pure code, process tests for GenServers, LiveView tests for UI).
Expand All @@ -43,6 +48,7 @@ A slice is done only when ALL of these are true:

```
mix gate # the full quality gate (defined in Slice 000). Must pass before every commit.
./scripts/prod_check.sh # the release half of the gate, on its own: prod compile, release assemble, config eval
mix test # tests
mix test --cover # coverage (report the line in PROOF.md)
mix credo --strict
Expand Down
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,4 @@ After 012, multiple branches are independent. If running more than one coding ag
| 2026-09-20 | The MCP phase replanned under owner decisions of 2026-09-08, recorded in ADR-0007 decisions 5 to 8: beam_mcp 0.8.0 is the server core; 059 measures the capability gap and probes the `:server` seam (S/M); 060 is Trinity's thin driver (L, the M/L condition decided); the OAuth client role moves from 060 to 062; 061 carries a named blocker on the MRTR wrapper; the four `M5 Always-on` headers on 059 to 062 read M5a Automates, as this file has since 2026-09-08. R14 re-scoped, R15 closed. Re-derived this date: `find slices -name SLICE.md | wc -l` → **36**. |
| 2026-09-20 | Four slices added under the accepted 2026-09-20 plan: 002 (supply chain, early: SBOM, provenance, TLS floor; counted in M9), 003 (FIPS build leg from source; counted in M2 because it runs 024's FIPS properties), 025 (encryption at rest and the key-custody seam) and 026 (store-and-forward receipts; blocked on the external plane's answer). Re-derived this date: `find slices -name SLICE.md | wc -l` → **40**. |
| 2026-09-22 | Slice 062 amended under the owner's decision: the production profile is an OAuth client and resource server against an external authorization server, the embedded authorization server is the non-default personal profile refused under an external authority adapter, and Enterprise Managed Authorization (ID-JAG redemption at Trinity's own server) leaves the slice as a deferral with a lift condition (slice 062 NOTES, "Amendment"). AC3 and AC6 removed; the row's title follows. |
| 2026-09-22 | The gate gained a release check (`scripts/prod_check.sh`: prod compile, headless release assemble, runtime config eval) and `package.yml` gained a `branches:` trigger beside its tags one. Both follow slice 062's F5: a plug's `init/1` is escaped at compile time under prod, so a closure there broke the release build while `mix gate`, which runs in `:test`, stayed green on every leg, and the tags-only packaging trigger meant the first packaging run of the slice was the one after approval. Measured before adopting: the release check costs 52 s cold and 2 s warm, and it is red on the original defect. |
15 changes: 15 additions & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,21 @@ defmodule Trinity.MixProject do
gate: [
"format --check-formatted",
"compile --warnings-as-errors --force",
# The release half, added after slice 062: MIX_ENV=test says nothing about what ships.
# `Plug.Builder` escapes a plug's `init/1` result at compile time under prod, so a
# closure in those options broke the release while every gate stayed green, and the
# three-OS `package` workflow only found it at the tag, after approval (062 NOTES, F5).
# The script compiles prod, assembles the headless release and evaluates its runtime
# config, which is the only thing in the gate that executes runtime.exs's prod branch.
# `cmd`, like the two steps below: its own OS process and its own exit code.
# `env ERL_AFLAGS=` for the same reason `hex.audit` below carries it: on the FIPS leg
# this step runs outside FIPS mode. Compiling prod builds the dependencies in that
# environment, and `tokenizers` fetches a precompiled NIF over TLS, which OTP's ssl
# cannot do in the mode (docs/fips-leg.md, finding 1: the same HelloRetryRequest
# problem that stops Hex reaching hex.pm). Whether the tree compiles for release is not
# a FIPS property, so measuring it outside the mode loses nothing; 024's FIPS
# properties are measured by `mix test --trace test/fips`, which stays in the mode.
"cmd env ERL_AFLAGS= ./scripts/prod_check.sh",
"credo --strict",
"sobelow --exit --skip",
# `cmd` runs it as its own OS process: Hex's tasks are not reliably resolvable from
Expand Down
48 changes: 48 additions & 0 deletions scripts/prod_check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env sh
# SPDX-FileCopyrightText: Sudo Apt Holdings LLC
# SPDX-License-Identifier: Apache-2.0
#
# The release half of the gate. Added after slice 062, for a defect the gate could not see:
# `Plug.Builder` calls a plug's `init/1` at COMPILE TIME when MIX_ENV=prod and escapes what it
# returns into the endpoint, so an anonymous function there breaks the release build and nothing
# else. `mix gate` runs in MIX_ENV=test, where `init/1` is called per request, so every gate was
# green over a tree whose release could not compile; the three-OS `package` workflow found it at
# the tag, which is after approval (slice 062 NOTES, F5).
#
# Three commands, each for a class the one before it cannot see:
#
# 1. the tree compiles in prod — compile-time differences (the defect above)
# 2. the release assembles — the release's application list and boot scripts
# 3. the release evaluates its config — config/runtime.exs's `config_env() == :prod`
# branch, 116 lines that nothing else in the gate
# or in CI ever executes
#
# What it does NOT prove, said plainly: nothing here builds a Burrito binary, loads a NIF under
# musl (slice 013's mdex finding), builds the Tauri shell, or runs on macOS or Windows. That is
# the `package` workflow's job and it stays the `package` workflow's job.
#
# POSIX sh, like scripts/plan_check.sh, and like it not run on a Windows developer's machine.
# The gate invokes it as `cmd env ERL_AFLAGS= ./scripts/prod_check.sh`: on the FIPS leg it runs
# outside FIPS mode, because compiling prod builds the dependencies and `tokenizers` fetches a
# precompiled NIF over TLS, which OTP's ssl cannot do in the mode. Compiling for release is not
# a FIPS property; the FIPS properties are `mix test --trace test/fips`, which stays in the mode.
set -eu
cd "$(dirname "$0")/.."

build_root="${MIX_BUILD_ROOT:-_build}"
rel="$build_root/prod/rel/headless/bin/headless"

echo "== 1. the tree compiles in prod =="
MIX_ENV=prod mix compile --warnings-as-errors

echo "== 2. the headless release assembles =="
# Assets are not deployed here: this step is about the release's own assembly, and
# `mix assets.deploy` is the packaging workflow's concern.
MIX_ENV=prod mix release headless --overwrite --quiet

echo "== 3. the release evaluates its runtime configuration =="
# `eval` runs the release's config providers, so config/runtime.exs's prod branch is executed
# with no environment prepared for it, which is how a packaged binary is first started.
"$rel" eval 'IO.puts("prod boot config ok")'

echo "prod_check: PASS"
Loading