Skip to content

Freshness, port keyring pins #3

Freshness, port keyring pins

Freshness, port keyring pins #3

name: Freshness, port keyring pins
# Two ports sign with keys archlinux-keyring does not carry, and each has a pin
# in bootstrap/keyrings/. A pinned keyring package name carries its version, and
# a port removes a superseded package from its mirror, so a pin that falls
# behind stops being fetchable and every build for that port fails. This job
# notices before that happens, applies the bump on a branch, tests it, and opens
# a pull request.
#
# ⛔ One job per pin. Two trust roots moving in one pull request would hide which
# one moved, and the two are tested by building different architectures.
#
# A failing run here is a normal result. It means upstream moved.
on:
workflow_dispatch:
schedule:
- cron: "15 04 * * 1" # Mondays, 04:15 UTC
defaults:
run:
shell: bash
permissions:
contents: read
concurrency:
group: freshness-keyring
cancel-in-progress: false
jobs:
check:
name: Check the ${{ matrix.keyring }} pin
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
strategy:
# One run reports every stale pin. Neither blocks the other.
fail-fast: false
matrix:
include:
# docker_arch is the architecture built to test a bump. Both ARM ports
# share the ARM keyring, so one of the two exercises it.
- keyring: archlinuxarm
pin: bootstrap/keyrings/archlinuxarm.pin
docker_arch: arm64
platform: linux/arm64
- keyring: archlinux-lcpu
pin: bootstrap/keyrings/archlinux-lcpu.pin
docker_arch: loong64
platform: linux/loong64
# All three PowerPC ports share one trust root, so one of the three
# exercises it. ppc64le is the one chosen because it is the only
# PowerPC target the standard QEMU setup already covers, so this job
# needs no emulator registration of its own.
- keyring: archpower
pin: bootstrap/keyrings/archpower.pin
docker_arch: ppc64le
platform: linux/ppc64le
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Compare the pin against the mirror
id: check
env:
PIN: ${{ matrix.pin }}
run: |
set -uo pipefail
# 0 current, 3 behind, 4 gone, 5 the pinned bytes changed, 1 the check
# could not run.
scripts/check-keyring-pin "$PIN" | tee /tmp/check.txt
rc="${PIPESTATUS[0]}"
echo "rc=$rc" >> "$GITHUB_OUTPUT"
case "$rc" in
0) echo "::notice::${PIN} is current" ;;
3) echo "::warning::a newer keyring is published for ${PIN}, preparing a pull request" ;;
4) echo "::error::the package ${PIN} pins is gone from the mirror, that port is broken now" ;;
5) echo "::error::the package ${PIN} pins still answers and its bytes changed" ;;
*) echo "the freshness check could not run" >&2; exit 1 ;;
esac
# ⛔ Reported on every run, including a green one. A trusted key past its
# expiry signs nothing pacman will take, so the count of usable keys falls
# with no upstream change at all and nothing else would say so.
- name: Report the expiry horizon
env:
EXPIRED: ${{ steps.check.outputs.expired_now }}
SOON: ${{ steps.check.outputs.expiring_soon }}
COUNT: ${{ steps.check.outputs.trusted_count }}
KEYRING: ${{ matrix.keyring }}
run: |
set -euo pipefail
echo "${KEYRING}: ${COUNT} trusted fingerprint(s) pinned"
echo " expired today : ${EXPIRED:-none}"
echo " expiring < 1y : ${SOON:-none}"
if [ -n "${SOON:-}" ] && [ "${SOON:-none}" != "none" ]; then
echo "::warning::${KEYRING} has trusted keys expiring within a year: ${SOON}"
fi
- name: Apply the bump on a branch
if: steps.check.outputs.rc != '0'
id: apply
env:
PIN: ${{ matrix.pin }}
KEYRING: ${{ matrix.keyring }}
run: |
set -uo pipefail
branch="freshness/${KEYRING}-$(date -u +%Y%m%d)"
git switch -c "$branch"
scripts/check-keyring-pin --apply "$PIN" | tee /tmp/apply.txt
rc="${PIPESTATUS[0]}"
case "$rc" in
3 | 4 | 5) ;;
*)
echo "applying the bump did not report a bump, refusing to open a pull request" >&2
exit 1
;;
esac
# check-keyring-pin writes the measured values straight to
# GITHUB_OUTPUT when it runs under Actions, so nothing is re-parsed
# out of its human readable output here.
echo "branch=$branch" >> "$GITHUB_OUTPUT"
git diff --stat
- name: Set up QEMU
if: steps.check.outputs.rc != '0'
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0
- name: Test the bump with the tests that guard a normal build
if: steps.check.outputs.rc != '0'
id: test
env:
DOCKER_ARCH: ${{ matrix.docker_arch }}
PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
echo "::group::static suite"
tests/run.sh static
echo "::endgroup::"
# One architecture is built and inspected, because a keyring that
# installs but does not verify produces an empty root rather than a
# build error.
ver="$(date -u +%Y.%m.%d)"
docker build --platform "$PLATFORM" \
--build-arg "IMAGE_VERSION=$ver" \
--build-arg "SOURCE_COMMIT=${{ github.sha }}" \
--build-arg "BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
-t "freshness-check:$DOCKER_ARCH" .
# The databases the build resolved against, so the evidence is not
# joined against a set that moved. HISTORY/evidence-race.md.
docker build --platform "$PLATFORM" --target dbsnapshot \
--build-arg "IMAGE_VERSION=$ver" \
--output "type=local,dest=/tmp/dbsnapshot" .
echo "::group::image suite"
# ⛔ CONTAINER_RUNTIME is not optional. The runner image carries both
# docker and podman, and gen-evidence prefers podman when it finds
# one. The image was built with docker and lives in docker's store, so
# podman tries to pull it from Docker Hub and quay.io and fails.
SOURCE_COMMIT="${{ github.sha }}" BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
DB_SNAPSHOT=/tmp/dbsnapshot CONTAINER_RUNTIME=docker \
scripts/gen-evidence "$DOCKER_ARCH" "freshness-check:$DOCKER_ARCH" "$PLATFORM" \
"/tmp/evidence-$DOCKER_ARCH.json"
IMAGE="freshness-check:$DOCKER_ARCH" PLATFORM="$PLATFORM" CONTAINER_RUNTIME=docker \
EVIDENCE="/tmp/evidence-$DOCKER_ARCH.json" tests/run.sh image
echo "::endgroup::"
pkgs="$(jq -r '.package_count' "/tmp/evidence-$DOCKER_ARCH.json")"
echo "packages=$pkgs" >> "$GITHUB_OUTPUT"
- name: Open a pull request carrying the measurement
if: steps.check.outputs.rc != '0'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH: ${{ steps.apply.outputs.branch }}
PIN: ${{ matrix.pin }}
KEYRING: ${{ matrix.keyring }}
DOCKER_ARCH: ${{ matrix.docker_arch }}
OLD_PACKAGE: ${{ steps.apply.outputs.old_package }}
OLD_SHA: ${{ steps.apply.outputs.old_sha256 }}
NEW_PACKAGE: ${{ steps.apply.outputs.new_package }}
NEW_SHA: ${{ steps.apply.outputs.new_sha256 }}
NEW_SIZE: ${{ steps.apply.outputs.new_size }}
TRUSTED_COUNT: ${{ steps.apply.outputs.trusted_count }}
TRUSTED_ADDED: ${{ steps.apply.outputs.trusted_added }}
TRUSTED_REMOVED: ${{ steps.apply.outputs.trusted_removed }}
EXPIRED: ${{ steps.apply.outputs.expired_now }}
SOON: ${{ steps.apply.outputs.expiring_soon }}
PINNED_HTTP: ${{ steps.apply.outputs.pinned_http }}
PACKAGES: ${{ steps.test.outputs.packages }}
RC: ${{ steps.check.outputs.rc }}
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add "$PIN"
git commit -m "keyring: pin ${NEW_PACKAGE}"
git push --set-upstream origin "$BRANCH"
urgency="a newer keyring is published and the pinned one still answers"
case "$RC" in
4) urgency="the pinned keyring is GONE from the mirror, so ${DOCKER_ARCH} builds fail until this merges" ;;
5) urgency="⛔ the pinned package name still answers and its bytes are not the bytes that were pinned" ;;
esac
# The printf formats are markdown. Backticks inside them are literal
# text for the pull request body, not shell expansions.
# shellcheck disable=SC2016
{
printf '## What moved\n\n'
printf 'The `%s` trust root changed. %s\n\n' "$KEYRING" "$urgency"
printf '| | pinned before | pinned now |\n'
printf '| --- | --- | --- |\n'
printf '| package | `%s` | `%s` |\n' "$OLD_PACKAGE" "$NEW_PACKAGE"
printf '| sha256 | `%s` | `%s` |\n\n' "$OLD_SHA" "$NEW_SHA"
printf 'The old package answered HTTP `%s` at check time. The new package is\n' "$PINNED_HTTP"
printf '`%s` bytes and trusts `%s` fingerprint(s).\n\n' "$NEW_SIZE" "$TRUSTED_COUNT"
printf '| trusted keys | fingerprints |\n'
printf '| --- | --- |\n'
printf '| added | `%s` |\n' "$TRUSTED_ADDED"
printf '| removed | `%s` |\n' "$TRUSTED_REMOVED"
printf '| expired as of today | `%s` |\n' "$EXPIRED"
printf '| expiring within a year | `%s` |\n\n' "$SOON"
printf 'Every value above was re-derived from the package itself by\n'
printf '`scripts/check-keyring-pin`. None was copied from a report. The expiry\n'
printf 'dates come from the key material, so a change to one of them is the\n'
printf 'trust root moving and not a clock ticking.\n\n'
printf '## What the tests did\n\n'
printf -- '- `tests/run.sh static`, the same suite the build job runs first.\n'
printf -- '- A full `%s` build from this branch.\n' "$DOCKER_ARCH"
printf -- '- `scripts/gen-evidence` then `tests/run.sh image` against that build,\n'
printf ' which installed `%s` packages.\n\n' "$PACKAGES"
printf 'Signature verification stayed at `SigLevel = Required` throughout. A\n'
printf 'keyring that installs but does not verify produces an empty root, which\n'
printf 'the image suite fails on rather than passing.\n\n'
printf '## ⚠ What this job did NOT verify\n\n'
printf -- '- **Only `%s` was built.** The other architectures were not.\n' "$DOCKER_ARCH"
printf -- '- **The new fingerprints were not checked against any source other than\n'
printf ' the package that carries them.** That is the trust model these pins use,\n'
printf ' and it is why the sha256 is pinned alongside them.\n'
printf -- '- **No image was published.** The build stayed on the runner.\n'
printf -- '- **The mirror was read once.** A mirror mid sync can serve an older\n'
printf ' listing than another mirror.\n'
printf -- '- **This pull request carries no status check.** A pull request opened\n'
printf ' with the built-in token has its run held at `action_required` until a\n'
printf ' human approves it. The tree was tested inside run `%s`, linked above.\n' "$GITHUB_RUN_ID"
printf ' See `HISTORY/maintainer-actions.md` section 4.\n'
} > /tmp/body.md
title="keyring: pin ${NEW_PACKAGE}"
case "$RC" in
4) title="keyring: pin ${NEW_PACKAGE} (${DOCKER_ARCH} builds are broken until this merges)" ;;
5) title="keyring: ${OLD_PACKAGE} changed under its own name" ;;
esac
gh pr create --base main --head "$BRANCH" --title "$title" --body-file /tmp/body.md