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
16 changes: 16 additions & 0 deletions .changeset/watch-debian-base-currency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"ftw": patch
---

Notice when a new Debian stable leaves the container base behind.

The images pin a codename (`debian:trixie-slim`, `python:3.12-slim-trixie`)
rather than `stable`, so a major-version jump can never arrive silently on a
rebuild. Nothing noticed when a new stable shipped, and Dependabot cannot: it
orders numeric tags, and a suite codename has no numeric component to order.

A weekly check now reads the pin out of the three Dockerfiles and compares it
with Debian's own `stable` release, failing when the pin falls behind or when
the three images stop agreeing on one suite.

No runtime behaviour changes.
100 changes: 100 additions & 0 deletions .github/workflows/debian-base-currency.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: debian base currency

# The container images pin a Debian codename (debian:trixie-slim) rather than a
# suite alias, so that a major-version jump can never arrive silently on some
# future rebuild. The cost of pinning is that nothing notices when a new stable
# ships, and a base left behind quietly stops receiving security updates once
# its suite leaves LTS.
#
# This notices. It opens a single issue and keeps editing it, so a suite left
# alone for a year costs one thread rather than fifty-two.

on:
schedule:
# 07:00 UTC on Mondays. Debian stable ships roughly every two years, so a
# daily check would be noise; weekly still surfaces a new release within
# days of it landing, which is far inside the window that matters.
- cron: "0 7 * * 1"
workflow_dispatch:

permissions:
contents: read
issues: write

concurrency:
group: debian-base-currency
cancel-in-progress: false

jobs:
currency:
name: pinned base follows Debian stable
# Forks inherit this schedule as soon as they sync master, and the run can
# only misfire there: the tracking issue belongs on srcfl/ftw, and a fork
# with issues disabled turns the Monday check into a failure email for its
# owner. A manual dispatch is someone asking, so that still runs.
if: github.repository == 'srcfl/ftw' || github.event_name != 'schedule'
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Compare the pinned suite against Debian stable
id: compare
run: |
set +e
output="$(bash scripts/check-debian-base.sh 2>&1)"
status=$?
set -e
echo "$output"
{
echo "status=${status}"
echo "report<<REPORT_EOF"
echo "$output"
echo "REPORT_EOF"
} >> "$GITHUB_OUTPUT"
# A network or parse failure must not read as "the base is current",
# and must not open an issue claiming a new release we never saw.
# 1 means a newer stable (or a split pin), 0 means current.
if [ "$status" != "0" ] && [ "$status" != "1" ]; then
echo "::error::the Debian currency check could not run (exit ${status})"
exit "$status"
fi

- name: Open or update the tracking issue
if: steps.compare.outputs.status == '1'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPORT: ${{ steps.compare.outputs.report }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
title="Container base is behind Debian stable"
body="$(printf '%s\n\n```\n%s\n```\n\n%s\n\nFrom %s\n\nThis issue is rewritten by each run of the `debian base currency` workflow. Closing it without moving the pin means it comes back next Monday.\n' \
"The Debian suite pinned by the container images is no longer the current Debian stable, or the three images have drifted apart. Core, updater and optimizer are pinned to one suite on purpose: they share a single base layer, so a host pulls that rootfs once instead of three times." \
"${REPORT}" \
"Moving the pin means updating the FROM lines in \`Dockerfile\`, \`Dockerfile.updater\` and \`Dockerfile.optimizer\` together, then rebuilding all three and confirming the optimizer still resolves CVXPY and HiGHS wheels on the new suite. Do not move core alone — that splits the shared layer. If the readiness lines above say a tag is not published yet, wait for it rather than splitting the pin." \
"${RUN_URL}")"

existing="$(gh issue list --state open --search "in:title \"${title}\"" \
--json number,title --jq "[.[] | select(.title == \"${title}\")][0].number")"

if [ -n "$existing" ] && [ "$existing" != "null" ]; then
gh issue edit "$existing" --body "$body"
echo "updated issue #${existing}"
else
gh issue create --title "$title" --body "$body"
fi

- name: Close the tracking issue once the pin is current
if: steps.compare.outputs.status == '0'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
title="Container base is behind Debian stable"
existing="$(gh issue list --state open --search "in:title \"${title}\"" \
--json number,title --jq "[.[] | select(.title == \"${title}\")][0].number")"
if [ -n "$existing" ] && [ "$existing" != "null" ]; then
gh issue close "$existing" --comment "Every image now pins the current Debian stable. Closed by the \`debian base currency\` workflow."
echo "closed issue #${existing}"
else
echo "base is current and no issue is open"
fi
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ optimizer-test: optimizer/.venv/.installed
optimizer/.venv/bin/pytest -q optimizer/tests

compose-migration-test:
bash -n scripts/enable-modular-stack.sh scripts/migrate-legacy-compose.sh scripts/install-macos.sh scripts/sync-bundled-drivers.sh scripts/check-driver-versions.sh
bash -n scripts/enable-modular-stack.sh scripts/migrate-legacy-compose.sh scripts/install-macos.sh scripts/sync-bundled-drivers.sh scripts/check-driver-versions.sh scripts/check-debian-base.sh
bash scripts/test-modular-compose.sh

container-boundary-test:
Expand Down
92 changes: 92 additions & 0 deletions scripts/check-debian-base.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Reports whether the Debian suite pinned by the container images is still the
# current Debian stable.
#
# The images pin a codename (debian:trixie-slim) rather than a suite alias
# (debian:stable-slim) so a major-version jump can never arrive silently on a
# rebuild. The cost of pinning is that nothing notices when a new stable ships —
# this is what notices.
#
# Truth comes from Debian's own Release file for the `stable` suite, not from
# registry tag listings: tags are noisy, rate-limited and say nothing about
# which suite Debian considers stable.
#
# Exit codes, matching scripts/sync-bundled-drivers.sh --behind:
# 0 pinned suite is current stable
# 1 a newer stable exists, or the Dockerfiles disagree with each other
# 2 the check could not run (network, parse) — never reported as "fine"
set -euo pipefail

ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
cd "$ROOT"

RELEASE_URL="${DEBIAN_RELEASE_URL:-https://deb.debian.org/debian/dists/stable/Release}"

# Read the pin out of the Dockerfiles instead of hard-coding it here, so this
# check cannot drift away from what actually ships.
pinned_debian() { sed -n 's/^FROM debian:\([a-z][a-z]*\)-slim.*/\1/p' "$1" | head -1; }
pinned_python() { sed -n 's/^FROM python:[0-9.][0-9.]*-slim-\([a-z][a-z]*\).*/\1/p' "$1" | head -1; }

core=$(pinned_debian Dockerfile)
updater=$(pinned_debian Dockerfile.updater)
optimizer=$(pinned_python Dockerfile.optimizer)

for pair in "Dockerfile:$core" "Dockerfile.updater:$updater" "Dockerfile.optimizer:$optimizer"; do
if [ -z "${pair#*:}" ]; then
echo "could not read a Debian suite from ${pair%%:*}" >&2
exit 2
fi
done

echo "pinned suite:"
printf ' %-22s %s\n' "Dockerfile" "$core" "Dockerfile.updater" "$updater" "Dockerfile.optimizer" "$optimizer"

if [ "$core" != "$updater" ] || [ "$core" != "$optimizer" ]; then
echo ""
echo "The three images no longer agree on one Debian suite. Sharing a single"
echo "base layer is the reason they were aligned, and that benefit is lost"
echo "while they differ."
exit 1
fi

release=$(curl -fsSL --max-time 20 "$RELEASE_URL" 2>/dev/null) || {
echo "could not fetch $RELEASE_URL" >&2
exit 2
}
stable=$(printf '%s\n' "$release" | sed -n 's/^Codename: *//p' | head -1)
version=$(printf '%s\n' "$release" | sed -n 's/^Version: *//p' | head -1)
if [ -z "$stable" ]; then
echo "no Codename field in $RELEASE_URL" >&2
exit 2
fi

echo ""
echo "debian stable: $stable${version:+ (}${version}${version:+)}"

if [ "$core" = "$stable" ]; then
echo ""
echo "The pinned suite is current."
exit 0
fi

echo ""
echo "A newer Debian stable is available: $core -> $stable"

# Advisory only. A new Debian stable is tagged in the official images promptly,
# but python:<ver>-slim-<suite> can lag by days, and moving core without the
# optimizer would split the shared base layer. Never fail the check on this —
# it is a readiness note, not the finding.
if command -v docker >/dev/null 2>&1; then
echo ""
echo "image readiness:"
python_tag=$(sed -n 's/^FROM \(python:[0-9.][0-9.]*\)-slim-[a-z][a-z]*.*/\1/p' Dockerfile.optimizer | head -1)
for image in "debian:${stable}-slim" "${python_tag}-slim-${stable}"; do
if docker manifest inspect "$image" >/dev/null 2>&1; then
printf ' %-32s available\n' "$image"
else
printf ' %-32s NOT PUBLISHED YET\n' "$image"
fi
done
fi

exit 1