Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
a469668
worker: redirect-only HTTPS front for the GHCR mirror
Azathothas Oct 14, 2026
c1b5e25
worker: v2 redirect front with checked pins, versioned cache, tests a…
Azathothas Oct 14, 2026
8f7a126
status: measure the mirror hourly, render the Pages site and badges, …
Azathothas Oct 14, 2026
bbee3d8
ci: find tests by discovery, pin static curl and bash, check workflow…
Azathothas Oct 14, 2026
6d6cc0e
pipeline: per-target chains for 15 targets, keyring_image and keyring…
Azathothas Oct 14, 2026
630d077
Merge branch 'worktree-wf_804d082c-156-3' into next
Azathothas Oct 14, 2026
8fda069
Merge branch 'worktree-wf_804d082c-156-5' into next
Azathothas Oct 14, 2026
87e7938
Merge branch 'worktree-wf_804d082c-156-4' into next
Azathothas Oct 14, 2026
d4d0946
Merge branch 'worktree-wf_804d082c-156-2' into next
Azathothas Oct 14, 2026
9363966
tests: tag marker on the keyring image pin in 40-lib
Azathothas Oct 14, 2026
c90e807
verify-batch: refuse keyserver lookups inside the verify container
Azathothas Oct 14, 2026
57a97da
workflows: each scheduled workflow starts the others when theirs did …
Azathothas Oct 14, 2026
38b27ba
client: parallel prefetch, direct blob addressing, resume, host black…
Azathothas Oct 14, 2026
6803939
Merge branch 'worktree-wf_804d082c-156-1' into next
Azathothas Oct 14, 2026
fab24f4
client: answer an absent database signature with an empty file under …
Azathothas Oct 14, 2026
2c3b48c
tests: client suite checks the -Sy pause and stored database signatures
Azathothas Oct 14, 2026
e363bab
sync: numeric inputs through fromJSON; accept a files database that l…
Azathothas Oct 14, 2026
59b3412
mirror-file: a 429 from the registry pauses every push of the shard a…
Azathothas Oct 14, 2026
a1cf184
workflows: deploy and restarts not held by skipped jobs; Worker check…
Azathothas Oct 14, 2026
48ad451
docs: ASD-STE100 rewrite; remove narrative history
Azathothas Oct 14, 2026
ee21cc5
lib: the registry credential reaches curl on stdin; staging slug cut …
Azathothas Oct 14, 2026
68a4edb
ci: alpine 3.24 for the busybox unit leg
Azathothas Oct 14, 2026
233876a
docs: no image version or digest that a Renovate update would leave s…
Azathothas Oct 14, 2026
97be6cb
client: fix review findings
Azathothas Oct 14, 2026
b48cdb8
workflows: fix review findings
Azathothas Oct 14, 2026
6aac3b0
pipeline: fix review findings
Azathothas Oct 14, 2026
00e6828
Merge branch 'fix-client' into next
Azathothas Oct 14, 2026
7339c9a
Merge branch 'fix-workflows' into next
Azathothas Oct 14, 2026
3a9caa5
docs: seen-to-fail pairs of the pipeline fixes
Azathothas Oct 14, 2026
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
46 changes: 39 additions & 7 deletions .github/renovate.json
Original file line number Diff line number Diff line change
@@ -1,27 +1,59 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"description": [
"Renovate proposes pin moves; a person merges them. Nothing here merges on its own: the verify image decides what this mirror accepts, so a change to it is reviewed.",
"Pins stay pins: actions by commit hash with the version in a comment, images by digest under a '# tag: <tag>' marker. The repository requires SHA-pinned actions, and tests/static/50-workflow-policy.sh fails a change that turns one back into a tag."
"Pins stay pins: actions by commit hash with the version in a comment, images by digest under a '# tag: <tag>' marker. The repository requires SHA-pinned actions, and tests/static/50-workflow-policy.sh fails a change that turns one back into a tag.",
"Digest, pin, patch and minor updates of actions and images merge on their own, as a squash, when every check of the pull request is green. Renovate reads the checks itself: platformAutomerge is off, because main has no required checks and GitHub's own auto-merge would merge a red pull request. A major update and a keyring image wait for a person.",
"The verify image of the targets and the Arch image of the tests move in one pull request. So CI runs pacman in the image that the targets will verify with, before that image merges.",
"ignorePaths replaces the preset's list, which ignores **/tests/**: the test images are pins too.",
"tests/static/75-renovate.sh fails an image pin that the regex manager below does not match, and an automerge rule for major updates."
],
"extends": ["config:recommended", ":disableDependencyDashboard"],
"extends": ["config:recommended", ":disableDependencyDashboard", "helpers:pinGitHubActionDigests"],
"ignorePaths": ["**/node_modules/**"],
"labels": ["dependencies"],
"platformAutomerge": false,
"automergeType": "pr",
"automergeStrategy": "squash",
"packageRules": [
{
"description": "Actions move weekly in one pull request, and only to a release at least three days old, so a release that is pulled or hijacked upstream has time to be noticed before it lands here.",
"matchManagers": ["github-actions"],
"groupName": "github actions",
"schedule": ["before 6am on monday"],
"schedule": ["* 0-5 * * 1"],
"minimumReleaseAge": "3 days"
},
{
"description": "Small updates of actions and images merge when CI is green.",
"matchManagers": ["github-actions", "custom.regex"],
"matchUpdateTypes": ["digest", "pin", "pinDigest", "patch", "minor"],
"automerge": true
},
{
"description": "The 15 verify_image lines and the tests' Arch image: one pull request.",
"matchManagers": ["custom.regex"],
"matchDepNames": ["docker.io/library/archlinux"],
"groupName": "archlinux base image"
},
{
"description": "The keyring images of the targets (keyring_image): one pull request, which a person merges. Their keyrings are the trust root of the aarch64, armv7h, loong64 and riscv64 targets, and no CI job runs them.",
"matchManagers": ["custom.regex"],
"matchDepNames": ["ghcr.io/pkgforge-dev/archlinux"],
"groupName": "keyring images",
"automerge": false
},
{
"description": "The node image of worker.yml and ci.yml: one pull request, so the Worker is tested in the image that deploys it.",
"matchManagers": ["custom.regex"],
"matchDepNames": ["docker.io/library/node"],
"groupName": "node image"
}
],
"customManagers": [
{
"customType": "regex",
"description": "An image pinned by digest under a '# tag: <tag>' marker: the verify image of each target, and the images the tests run in.",
"managerFilePatterns": ["/^targets/[^/]+\\.conf$/", "/^\\.github/workflows/[^/]+\\.ya?ml$/", "/^tests/integration/[^/]+\\.sh$/"],
"description": "An image pinned by digest under a '# tag: <tag>' marker: the verify and keyring images of each target, and the images the workflows, the scripts and the tests run.",
"managerFilePatterns": ["/^targets/[^/]+\\.conf$/", "/^\\.github/workflows/[^/]+\\.ya?ml$/", "/^scripts/[^/]+$/", "/^tests/.+\\.sh$/"],
"matchStrings": [
"#\\s*tag:\\s*(?<currentValue>[A-Za-z0-9._-]+)\\s*\\n(?:[^\\n]*\\n){0,3}?[^\\n]*?(?<depName>docker\\.io/[a-z0-9._/-]+?)(?::[A-Za-z0-9._-]+)?@(?<currentDigest>sha256:[a-f0-9]{64})"
"#\\s*tag:\\s*(?<currentValue>[A-Za-z0-9._-]+)\\s*\\n(?:[^\\n]*\\n){0,3}?[^\\n]*?(?<depName>(?:docker\\.io|ghcr\\.io)/[a-z0-9._/-]+?)(?::[A-Za-z0-9._-]+)?@(?<currentDigest>sha256:[a-f0-9]{64})"
],
"datasourceTemplate": "docker"
}
Expand Down
203 changes: 161 additions & 42 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
# The test suites, on every push and pull request.
# The test suites, on each push to main and each pull request.
#
# static shellcheck, actionlint and the policy checks (tests/static)
# unit the wrapper and the scripts, under three shells and three awks:
# dash and mawk here, bash and gawk in Arch, busybox in Alpine
# integration the whole mirror against a local registry and upstream, and
# pacman installing through the wrapper (tests/integration)
# debloat scripts/debloat-runner on a real runner, so code that a sync
# rarely needs is still seen to work
# static shellcheck, actionlint, py_compile and the policy checks
# (tests/static)
# unit the wrapper and the scripts under three shells and three
# awks, one job each: dash and mawk on the runner, bash and
# gawk in Arch, busybox in Alpine (tests/unit)
# integration one job per file in tests/integration, found when the run
# starts, so a new file gets its own job with no edit here
# worker the Cloudflare Worker's tests in the node image (tests/worker)
# debloat scripts/debloat-runner on a real runner, measured
#
# ⛔ No job installs a package from a distro mirror. scripts/install-test-tools
# downloads pinned static binaries and checks each sha256. The containers
# mount that directory read-only, last on PATH: a tool fills a gap, and the
# image's own tools stay the ones under test.
#
# Push runs only on main, so a pull request from a branch here runs once, not
# twice. The status branch holds a generated site and never runs this.
name: CI

on:
push:
branches: [main]
pull_request:
branches-ignore: [status]
workflow_dispatch:

permissions: {}

# One group per pull request: a newer push to it cancels the run in progress.
# Any other run has a group of its own (its run id). A group holds one pending
# run, and a newer run cancels it, so a shared group for main would leave a
# commit between two quick pushes with no verdict. No job writes shared state,
# so runs on main need not wait for each other.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
group: ci-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

defaults:
run:
Expand All @@ -34,60 +52,162 @@ jobs:
with:
persist-credentials: false
- name: Static suite
env:
# Here each of these is present, so a test that needs one fails
# rather than skips when it is not.
TESTS_REQUIRE_TOOLS: shellcheck actionlint jq python3
run: |
scripts/install-test-tools "$RUNNER_TEMP/bin"
scripts/install-test-tools "$RUNNER_TEMP/bin" shellcheck actionlint jq
python3 --version
# The runner's own test first, without the runner: a run.sh that
# hides failures would hide this one too.
REPO_ROOT="$PWD" bash tests/static/72-test-runner.sh
PATH="$RUNNER_TEMP/bin:$PATH" tests/run.sh static

unit:
name: unit (${{ matrix.leg }})
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- leg: dash-mawk
image: ''
require: jq curl python3
- leg: bash-gawk
# tag: base
image: docker.io/library/archlinux:base@sha256:3c64b538588416593f69a1aebd2f2ec0f2541b29159f32556fa7def2167147a0
require: jq curl
- leg: busybox
# tag: 3.24
image: docker.io/library/alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6
require: jq curl
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Unit suite, dash and mawk
- name: Test tools
run: scripts/install-test-tools "$RUNNER_TEMP/tools" jq curl bash
- name: Unit suite
env:
LEG: ${{ matrix.leg }}
IMAGE: ${{ matrix.image }}
TESTS_REQUIRE_TOOLS: ${{ matrix.require }}
run: |
# Ubuntu's awk is gawk once gawk is installed; mawk is still there,
# and put first here, and both are asserted so an image change shows.
mkdir -p "$RUNNER_TEMP/mawk"
ln -sf /usr/bin/mawk "$RUNNER_TEMP/mawk/awk"
export PATH="$RUNNER_TEMP/mawk:$PATH"
if [ "$(readlink -f /bin/sh)" != /usr/bin/dash ]; then
echo "::error::/bin/sh is $(readlink -f /bin/sh), not dash"
exit 1
# The shell, the awk and the tools each leg runs, asserted, so a
# change in an image shows here and not as a passing run of the
# wrong thing.
cat > "$RUNNER_TEMP/leg.sh" << 'EOF'
set -eu
PATH="$PATH:$TOOLS"
export PATH
sh_is="$(readlink -f /bin/sh)"
awk_is="$(readlink -f "$(command -v awk)")"
echo "$LEG: sh $sh_is, awk $awk_is, bash $(command -v bash), curl $(command -v curl), jq $(command -v jq)"
case "$LEG:$sh_is:$awk_is" in
dash-mawk:/usr/bin/dash:/usr/bin/mawk) ;;
bash-gawk:/usr/bin/bash:/usr/bin/gawk*) ;;
busybox:/bin/busybox:/bin/busybox) ;;
*)
echo "::error::$LEG runs sh $sh_is and awk $awk_is, which is not what this leg tests"
exit 1
;;
esac
exec bash tests/run.sh unit
EOF
if [ "$LEG" = dash-mawk ]; then
# Ubuntu's awk is gawk once gawk is installed; mawk is still
# there, and goes first on PATH here.
mkdir -p "$RUNNER_TEMP/mawk"
ln -sf /usr/bin/mawk "$RUNNER_TEMP/mawk/awk"
PATH="$RUNNER_TEMP/mawk:$PATH" TOOLS="$RUNNER_TEMP/tools" sh "$RUNNER_TEMP/leg.sh"
else
docker run --rm -v "$PWD:/work:ro" -v "$RUNNER_TEMP/tools:/tools:ro" \
-v "$RUNNER_TEMP/leg.sh:/leg.sh:ro" -w /work \
-e LEG -e TESTS_REQUIRE_TOOLS -e TOOLS=/tools "$IMAGE" sh /leg.sh
fi
if [ "$(readlink -f "$(command -v awk)")" != /usr/bin/mawk ]; then
echo "::error::awk is $(readlink -f "$(command -v awk)"), not mawk"

# The matrix of the next job, from tests/run.sh --list.
integration-files:
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
outputs:
files: ${{ steps.list.outputs.files }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: List the integration test files
id: list
run: |
names="$(tests/run.sh --list integration)"
json=""
while IFS= read -r f; do
[ -n "$f" ] || continue
case "$f" in
*[!A-Za-z0-9._-]*)
echo "::error::tests/integration/$f: a test file name is letters, digits, '.', '_' and '-' only"
exit 1
;;
esac
json="${json:+$json,}\"$f\""
done <<< "$names"
if [ -z "$json" ]; then
echo "::error::tests/run.sh --list integration found no test file"
exit 1
fi
tests/run.sh unit
- name: Unit suite, bash and gawk (Arch)
run: |
# tag: base
image=docker.io/library/archlinux:base@sha256:3c64b538588416593f69a1aebd2f2ec0f2541b29159f32556fa7def2167147a0
docker run --rm -v "$PWD:/work:ro" -w /work "$image" bash -c 'pacman -Sy --noconfirm --needed jq > /dev/null && bash tests/run.sh unit'
- name: Unit suite, busybox (Alpine)
run: |
# tag: 3.22
image=docker.io/library/alpine:3.22@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8
docker run --rm -v "$PWD:/work:ro" -w /work "$image" sh -c 'apk add --no-cache bash curl jq > /dev/null && bash tests/run.sh unit'
echo "files=[$json]" | tee -a "$GITHUB_OUTPUT"

integration:
needs: integration-files
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
contents: read
strategy:
fail-fast: false
matrix:
file: ${{ fromJSON(needs.integration-files.outputs.files) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Integration suite
- name: Integration test
env:
CONTAINER_RUNTIME: docker
run: tests/run.sh integration
TEST_FILE: ${{ matrix.file }}
run: tests/run.sh integration "$TEST_FILE"

worker:
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Test tools
run: scripts/install-test-tools "$RUNNER_TEMP/tools" jq curl bash
- name: Worker suite
env:
# The node image of .github/workflows/worker.yml. Renovate moves
# the two in one pull request (group "node image").
# tag: 24-alpine
IMAGE: docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401c70e0e762c5b53302f905ec1c1
run: |
docker run --rm -v "$PWD:/work:ro" -v "$RUNNER_TEMP/tools:/tools:ro" -w /work \
-e TESTS_REQUIRE_TOOLS='node bash' "$IMAGE" \
sh -c 'PATH="$PATH:/tools"; export PATH; node --version; exec bash tests/run.sh worker'

# Measured, not judged: a runner image that no longer carries these
# directories frees less, and that is worth a warning, not a red run.
# debloat-runner failing outright still fails the job.
debloat:
runs-on: ubuntu-24.04
timeout-minutes: 15
Expand All @@ -102,10 +222,9 @@ jobs:
before="$(df --output=avail -B1 "$RUNNER_TEMP" | awk 'NR == 2 { print $1 }')"
scripts/debloat-runner --dir "$RUNNER_TEMP"
after="$(df --output=avail -B1 "$RUNNER_TEMP" | awk 'NR == 2 { print $1 }')"
echo "freed $(((after - before) / 1073741824)) GiB"
# Seen to work means it freed something: a runner image that no
# longer carries any of these directories is worth knowing about.
[ "$after" -gt $((before + 1073741824)) ] || {
echo "::error::debloat-runner freed less than 1 GiB; the runner image changed under it"
exit 1
}
freed=$((after - before))
echo "freed $((freed / 1048576)) MiB on $ImageOS $ImageVersion"
echo "debloat-runner freed $((freed / 1048576)) MiB on $ImageOS $ImageVersion" >> "$GITHUB_STEP_SUMMARY"
if [ "$freed" -lt 1073741824 ]; then
echo "::warning::debloat-runner freed $((freed / 1048576)) MiB, less than 1 GiB, on $ImageOS $ImageVersion. The runner image changed under it: compare its TARGETS with du -sh of each on this image."
fi
Loading