diff --git a/AGENTS.md b/AGENTS.md index c71edf5..7f521a3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,11 +26,23 @@ firehose/ │ ├── lib/ │ └── mix.exs ├── mise.toml # Runtime versions (Elixir, Erlang, Node) +├── Dockerfile # Deploy build; ARG pins must match mise.toml +├── scripts/check-pins.sh # Verifies the pin sync; runs in `make check` └── Makefile # make (test, check) ``` Two blogs are configured: +# Toolchain pins + +`mise.toml` and the `Dockerfile` ARG pins must agree; `scripts/check-pins.sh` +(runs as part of `make check`) enforces this. When bumping versions, change +both files in one commit. Upstream drift (newer Elixir/OTP/Debian releases) +is reported by `dokku-updates firehose check` (dokku-maintenance repo). +Upgrading pins or mix packages is largely unscripted: for such tasks, a Pi +sub agent can be started in this directory — it reads this AGENTS.md and +follows the zero-defects policy above. + | Blog | Route | Description | |------|-------|-------------| | Engineering | `/blog/engineering` | Main blog | diff --git a/Makefile b/Makefile index fb37d7c..4a7adb6 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ check: @echo "Running static analysis..." @make -C app MISE_BIN=mise check + @bash scripts/check-pins.sh # Precommit target for CI/pre-commit hooks precommit: check diff --git a/README.md b/README.md index 84636a6..cd05cb1 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,9 @@ firehose/ ├── blogex/ # Blogex library (multi-blog engine) │ ├── lib/ │ └── mix.exs -└── mise.toml # Runtime versions (Elixir, Erlang, Node) +├── Dockerfile # Deploy build; ARG pins must match mise.toml +├── mise.toml # Runtime versions (Elixir, Erlang, Node) +└── scripts/check-pins.sh # Verifies the pin sync; runs in `make check` ``` Two blogs are configured: @@ -51,6 +53,15 @@ mise install This installs Elixir, Erlang, and Node at the versions specified in `mise.toml`. +## Toolchain pins + +`mise.toml` (local dev toolchain) and the `Dockerfile` `ARG` pins (deploy +builder image) must agree — local builds and deployed images should run the +same Elixir/OTP. `make check` runs `scripts/check-pins.sh` to verify this; +when it reports a MISMATCH, fix the file it names. Upstream drift of these +pins (newer Elixir/OTP/Debian releases) is reported separately by +`dokku-updates firehose check` from the dokku-maintenance repo. + ### 2. Start PostgreSQL You need PostgreSQL running locally with a `postgres` user (password: `postgres`). diff --git a/scripts/check-pins.sh b/scripts/check-pins.sh new file mode 100755 index 0000000..e2b4203 --- /dev/null +++ b/scripts/check-pins.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# check-pins.sh - keep mise.toml and the Dockerfile base-image pins in sync. +# +# The Phoenix build has two sources of truth for toolchain versions: +# - mise.toml [tools]: local dev toolchain (what `mise install` gives you) +# - Dockerfile ARG pins: the hexpm/elixir builder image used for deploys +# They must agree, otherwise local builds and deployed images run different +# Elixir/OTP combinations. Run standalone, or via `make check`. +# +# Also verifies that the BUILDER_IMAGE / RUNNER_IMAGE ARGs still expand to the +# tags composed from the pins (catches hand-edited image references). +# +# Pure shell + sed: no mise, docker or elixir needed, so it runs in CI and in +# sandboxes before the toolchain is installed. +set -euo pipefail + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) +mise_toml="$root/mise.toml" +dockerfile="$root/Dockerfile" +status=0 + +die() { echo "MISMATCH: $*" >&2; status=1; } + +# sed-extract a `key = "value"` (or `ARG KEY=value`) line; dies when absent. +grab() { # grab + local value + value=$(sed -n "s/^$2[[:space:]]*=[[:space:]]*//p" "$3") + if [ -z "$value" ]; then + die "$1 not found in $3 (expected key = value)" + fi + value=${value%\"}; value=${value#\"} # strip optional surrounding quotes + value=$(printf '%s' "$value" | sed 's/[[:space:]]*$//') # trim trailing ws + printf '%s' "$value" +} + +mise_elixir=$(grab 'mise elixir' 'elixir' "$mise_toml") +mise_erlang=$(grab 'mise erlang' 'erlang' "$mise_toml") +d_elixir=$(grab 'Dockerfile ELIXIR_VERSION' 'ARG[[:space:]]\+ELIXIR_VERSION' "$dockerfile") +d_otp=$(grab 'Dockerfile OTP_VERSION' 'ARG[[:space:]]\+OTP_VERSION' "$dockerfile") +d_debian=$(grab 'Dockerfile DEBIAN_VERSION' 'ARG[[:space:]]\+DEBIAN_VERSION' "$dockerfile") + +if [ $status -eq 0 ]; then + mise_elixir_ver=${mise_elixir%%-otp-*} + mise_elixir_otp=${mise_elixir##*-otp-} + d_otp_major=${d_otp%%.*} + + # Elixir: full version must match exactly. + [ "$mise_elixir_ver" = "$d_elixir" ] || + die "Elixir: mise.toml has $mise_elixir_ver, Dockerfile ELIXIR_VERSION is $d_elixir" + + # mise elixir builds embed their OTP major (…-otp-27); must match the + # major of the Dockerfile OTP_VERSION. + [ "$mise_elixir_otp" = "$d_otp_major" ] || + die "OTP major: mise.toml elixir built for otp-$mise_elixir_otp, Dockerfile OTP_VERSION major is $d_otp_major" + + # mise `erlang` IS OTP: must match the Dockerfile pin exactly. + [ "$mise_erlang" = "$d_otp" ] || + die "OTP: mise.toml erlang is $mise_erlang, Dockerfile OTP_VERSION is $d_otp" + + # BUILDER_IMAGE must expand to the tag composed from the pins. + expected_builder="docker.io/hexpm/elixir:${d_elixir}-erlang-${d_otp}-debian-${d_debian}" + d_builder=$(grab 'Dockerfile BUILDER_IMAGE' 'ARG[[:space:]]\+BUILDER_IMAGE' "$dockerfile") + expanded_builder=$(printf '%s' "$d_builder" | + sed -e "s/\${ELIXIR_VERSION}/$d_elixir/" \ + -e "s/\${OTP_VERSION}/$d_otp/" \ + -e "s/\${DEBIAN_VERSION}/$d_debian/") + [ "$expanded_builder" = "$expected_builder" ] || + die "BUILDER_IMAGE expands to $expanded_builder, expected $expected_builder" + + # RUNNER_IMAGE must expand to the Debian pin. + expected_runner="docker.io/debian:${d_debian}" + d_runner=$(grab 'Dockerfile RUNNER_IMAGE' 'ARG[[:space:]]\+RUNNER_IMAGE' "$dockerfile") + expanded_runner=${d_runner/\$\{DEBIAN_VERSION\}/$d_debian} + [ "$expanded_runner" = "$expected_runner" ] || + die "RUNNER_IMAGE expands to $expanded_runner, expected $expected_runner" +fi + +if [ $status -eq 0 ]; then + echo "pins ok: Elixir $d_elixir, OTP $d_otp, Debian $d_debian (mise and Dockerfile agree)" + echo " builder: $expected_builder" + echo " runner: $expected_runner" + echo " (upstream drift of these pins is reported by dokku-updates 'firehose check')" +else + echo >&2 "fix mise.toml and/or the Dockerfile ARG pins so they agree, then re-run." +fi +exit $status