diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 0c7fe35..f450520 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 243e7a4..44dc7fa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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). @@ -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 diff --git a/ROADMAP.md b/ROADMAP.md index 94b905c..4e18bb2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. | diff --git a/mix.exs b/mix.exs index 1109814..2c70311 100644 --- a/mix.exs +++ b/mix.exs @@ -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 diff --git a/scripts/prod_check.sh b/scripts/prod_check.sh new file mode 100755 index 0000000..e545b19 --- /dev/null +++ b/scripts/prod_check.sh @@ -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"