#!/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