Compare commits

...
Author SHA1 Message Date
Claude f9809550c8 ci: sign every published runner image by digest (zegit#262)
zegit's arc-runner-pin.sh pins a runner image only when cosign verifies
its digest against the runner-image public key (zegit#143), but nothing
signed the images, so the pin refused every day. Sign the digest with
the RUNNER_IMAGE_COSIGN_KEY repository secret after the publish and
verify the signature in the same run. sign_only re-signs an image
published before signing existed.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_0147GvtJgsTjDGcrfvJsx5jS
2026-10-19 14:00:56 +02:00
markus 96ba41dfbf Merge pull request 'ci: read the org-scoped ZEGIT_CI_PACKAGE_TOKEN, not a repo-local name' (#12) from claude/ci/package-token-name into master
build image (gitea) / build (push) Successful in 6s
Reviewed-on: #12
2026-09-08 07:25:02 +00:00
agent b38f4760e7 ci: read the org-scoped ZEGIT_CI_PACKAGE_TOKEN, not a repo-local name
The publish path failed at registry login on every dispatch (runs 18,
20) because it read `secrets.ZEGIT_PACKAGE_TOKEN`, a name that exists
nowhere. The package-write token every zegit-zoo image workflow uses is
ZEGIT_CI_PACKAGE_TOKEN, held above repository scope, so this repository
can read it under that name — no second token, no second copy to rotate.
2026-09-08 09:05:43 +02:00
markus 02cd70092e ci: build without an exporter on validation runs; assert chromium and psql (#11)
build image (gitea) / build (push) Successful in 7m34s
2026-09-08 06:23:51 +00:00
agent c11e2b19f8 ci: build without an exporter on validation runs; assert chromium and psql
buildctl v0.32 has no "cacheonly" exporter, so every push-triggered
validation build since this workflow was added failed with 'exporter
"cacheonly" could not be found' (run 16, 2026-09-08) — unnoticed, since
nothing depended on it. A build with no --output still builds every
stage on both architectures. The post-publish toolchain assertion now
also requires chromium-browser and psql (12.11.1-ci7).
2026-09-08 08:08:17 +02:00
markus cada2273bb Merge pull request 'build: chromium, font-noto and postgresql-client for the browser gates' (#10) from claude/build/ci7-browser-psql into master
build image / build (push) Skipped
build image (gitea) / build (push) Failing after 9s
Reviewed-on: #10
2026-09-08 05:52:37 +00:00
+73 -10
View File
@@ -24,6 +24,11 @@ on:
description: 'Image tag, e.g. 12.11.1-ci6 (upstream runner version + ci revision)'
required: true
type: string
sign_only:
description: 'Sign the already-published tag instead of building it (zegit#262: signs an image published before signing existed)'
required: false
default: false
type: boolean
push:
branches: [master]
paths:
@@ -35,6 +40,7 @@ env:
BUILDKIT_VERSION: v0.32.2
REGISTRY: gitea.langenoja.se
IMAGE_PATH: zegit-zoo/zegit-forgejo-runner
COSIGN_VERSION: v3.1.3
jobs:
build:
@@ -78,6 +84,7 @@ jobs:
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "push=true" >> "$GITHUB_OUTPUT"
echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
echo "sign_only=${{ inputs.sign_only }}" >> "$GITHUB_OUTPUT"
else
echo "push=false" >> "$GITHUB_OUTPUT"
echo "tag=ci-validation" >> "$GITHUB_OUTPUT"
@@ -90,11 +97,11 @@ jobs:
- name: Registry login
if: ${{ steps.meta.outputs.push == 'true' }}
env:
TOKEN: ${{ secrets.ZEGIT_PACKAGE_TOKEN }}
TOKEN: ${{ secrets.ZEGIT_CI_PACKAGE_TOKEN }}
run: |
set -euo pipefail
if [ -z "${TOKEN:-}" ]; then
echo "::error::ZEGIT_PACKAGE_TOKEN is not set, so this dispatch would build an image and push nothing. Issue a package-write token and store it as this secret."
echo "::error::ZEGIT_CI_PACKAGE_TOKEN is not visible to this run. It is the org-scoped package-write token every zegit-zoo image workflow uses (docs: flux-config docs/rotation/forge-secrets.yaml); this repository must be able to read the org secret, and the name must match."
exit 1
fi
mkdir -p "$HOME/.docker"
@@ -103,20 +110,29 @@ jobs:
> "$HOME/.docker/config.json"
- name: Build multi-arch
if: ${{ steps.meta.outputs.sign_only != 'true' }}
run: |
set -euo pipefail
REF="${REGISTRY}/${IMAGE_PATH}:${{ steps.meta.outputs.tag }}"
# No --output on a validation run: buildctl v0.32 has no "cacheonly"
# exporter (run 16, 2026-09-08: 'exporter "cacheonly" could not be
# found'), so every push-triggered validation since this workflow
# was added had failed — silently, since nothing depended on it. A
# build without an exporter still builds every stage on both arches.
if [ "${{ steps.meta.outputs.push }}" = "true" ]; then
out="type=image,name=${REF},push=true"
else
out="type=cacheonly"
fi
buildctl build --progress=plain \
--frontend dockerfile.v0 \
--local context=. \
--local dockerfile=. \
--opt platform=linux/amd64,linux/arm64 \
--output "$out"
--output "type=image,name=${REF},push=true"
else
buildctl build --progress=plain \
--frontend dockerfile.v0 \
--local context=. \
--local dockerfile=. \
--opt platform=linux/amd64,linux/arm64
fi
# The image is a TOOLCHAIN, so "it built" is not the property that matters
# — "it still contains what the gates shell out to" is. build-image.yml
@@ -142,7 +158,7 @@ jobs:
echo "FROM ${REF}"
echo 'RUN set -eu; \'
echo ' python3 -c "import yaml, sys; print(\"python3+yaml\", sys.version.split()[0])"; \'
echo ' for t in gcc node bash make curl tar openssl jq; do \'
echo ' for t in gcc node bash make curl tar openssl jq chromium-browser psql; do \'
echo ' command -v "$t" >/dev/null || { echo "MISSING: $t" >&2; exit 1; }; \'
echo ' echo "ok: $t"; \'
echo ' done; \'
@@ -154,10 +170,57 @@ jobs:
--frontend dockerfile.v0 \
--local context="$work" \
--local dockerfile="$work" \
--opt platform=linux/amd64,linux/arm64 \
--output type=cacheonly
--opt platform=linux/amd64,linux/arm64
echo "✓ toolchain intact on linux/amd64 and linux/arm64"
# zegit#143/#262: a -ci<N> tag is only a name, and ZEGIT_CI_PACKAGE_TOKEN
# (readable by every zegit-zoo branch run) can push one. Consumers pin
# this image only when `cosign verify` accepts its digest with the
# runner-image public key, so every published digest is signed here.
# The private key is a REPOSITORY secret of the runner repos only, never
# an org secret. Classic .sig tag, no transparency log: the registry is
# private and verifiers use --insecure-ignore-tlog with a pinned key.
- name: Sign the published image by digest
if: ${{ steps.meta.outputs.push == 'true' }}
env:
COSIGN_KEY_PEM: ${{ secrets.RUNNER_IMAGE_COSIGN_KEY }}
COSIGN_PASSWORD: ${{ secrets.RUNNER_IMAGE_COSIGN_PASSWORD }}
TOKEN: ${{ secrets.ZEGIT_CI_PACKAGE_TOKEN }}
run: |
set -euo pipefail
if [ -z "${COSIGN_KEY_PEM:-}" ] || [ -z "${COSIGN_PASSWORD:-}" ]; then
echo "::error::RUNNER_IMAGE_COSIGN_KEY / RUNNER_IMAGE_COSIGN_PASSWORD are not visible to this run (repository secrets, zegit#262). An unsigned runner image is never pinned."
exit 1
fi
case "$(uname -m)" in
x86_64) arch=amd64; sum=4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71;;
aarch64) arch=arm64; sum=c5d324e091826b0d7a78eb16fef316450b4eb9aaec045611c08ba06f5e73220a;;
*) echo "::error::unsupported arch $(uname -m)"; exit 1;;
esac
bin="$GITHUB_WORKSPACE/.bin/cosign"
curl -fsSL -o "$bin" "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-${arch}"
echo "${sum} ${bin}" | sha256sum -c -
chmod +x "$bin"
tag="${{ steps.meta.outputs.tag }}"
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
# Credentials go through 0600 curl configs, never argv (zegit#139).
( umask 077; printf 'user = "zegit-zoo:%s"\n' "$TOKEN" > "$tmp/basic.cfg" )
tok=$(curl -fsS -K "$tmp/basic.cfg" "https://${REGISTRY}/v2/token?scope=repository:${IMAGE_PATH}:pull&service=container_registry" |
python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
( umask 077; printf 'header = "Authorization: Bearer %s"\n' "$tok" > "$tmp/bearer.cfg" )
digest=$(curl -fsS -I -K "$tmp/bearer.cfg" \
-H 'Accept: application/vnd.oci.image.index.v1+json, application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' \
"https://${REGISTRY}/v2/${IMAGE_PATH}/manifests/${tag}" | tr -d '\r' |
awk -F': ' 'tolower($1)=="docker-content-digest"{print $2}')
[[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "::error::no digest for ${IMAGE_PATH}:${tag}"; exit 1; }
ref="${REGISTRY}/${IMAGE_PATH}@${digest}"
printf '%s\n' "$COSIGN_KEY_PEM" > "$tmp/cosign.key"; chmod 600 "$tmp/cosign.key"
"$bin" sign --yes --key "$tmp/cosign.key" --tlog-upload=false \
--new-bundle-format=false --use-signing-config=false "$ref"
"$bin" public-key --key "$tmp/cosign.key" > "$tmp/cosign.pub"
"$bin" verify --key "$tmp/cosign.pub" --insecure-ignore-tlog "${REGISTRY}/${IMAGE_PATH}:${tag}@${digest}" >/dev/null
echo "✓ signed and verified ${REGISTRY}/${IMAGE_PATH}:${tag}@${digest}"
- name: Summary
run: |
if [ "${{ steps.meta.outputs.push }}" = "true" ]; then