Skip to content

ci: stop pulling Docker Hub anonymously — drop the unused pgvector pre-pull, add a digest-pinned GHCR mirror - #6400

Merged
rbuergi merged 3 commits into
mainfrom
ci/dockerhub-pull-through-cache
Oct 10, 2026
Merged

rbuergi merged 3 commits into
mainfrom
ci/dockerhub-pull-through-cache

Conversation

@rbuergi

@rbuergi rbuergi commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Why

Core PR runs went red on Docker Hub's anonymous pull limit. On #6381 (Refs #6381), all six Run tests shards and Chart Gate Bundle registry scripts (executed) failed with toomanyrequests: You have reached your unauthenticated pull rate limit. GitHub-hosted runners share their egress IPs with every other tenant, and the anonymous limit is counted per IP, so no commit can cause this red or cure it. Retrying a window of hours does not help either. The root fix is to stop pulling Docker Hub from pull requests.

Inventory: what pulled Docker Hub anonymously (core + shared node-repo lanes)

site image runs on fix
dotnet-test.yml → Pre-pull Testcontainers images (every shard, every PR) pgvector/pgvector:pg17 hosted, PR deleted (this PR). No core suite starts a container: the PostgreSql suites left core in the test move (#2847 and later), and nothing references Testcontainers.PostgreSql. The pull fetched an image nothing used.
main-cd.yml → control acceptance (control-image-acceptance.sh default) pgvector/pgvector:pg17 hosted, main this PR: --postgres meshweaver.azurecr.io/dockerhub/pgvector/pgvector:pg17. The job is already ACR-logged-in (OIDC), and this is the copy the runner sets' shared Postgres already runs.
chart-gate.yml → Bundle registry scripts (executed) cesanta/docker_auth:1.14.0@sha256:98e0… hosted, PR follow-up PR: GHCR mirror (declared in this PR's list)
chart-gate.yml → Observability values grafana/loki:2.9.3 hosted, PR follow-up PR: GHCR mirror (declared in this PR's list, digest sha256:eb92…)
clients.yml, s1-extract-image.yml, whisper-image.yml image builds (FROM python/node/ubuntu) base images hosted, path-filtered PRs not changed: they run rarely and none failed in the measured window. Moving them changes production image provenance, so it is a separate decision (documented on the page).
shared node-repo-* lanes — self-hosted ARC already pull through ACR (TESTCONTAINERS_HUB_IMAGE_NAME_PREFIX=meshweaver.azurecr.io/dockerhub/ plus docker login in module-pack). Nothing else in them names a Docker Hub image.

Which mirror, and why

  • ACR meshweaver.azurecr.io/dockerhub/ (az acr import copies; there is no ACR cache rule, because az acr cache create needs a Docker Hub credential) is used where a job is already logged in to ACR. In core that is main only: core has no ACR password, and its OIDC federated credentials match main alone (Doc/Architecture/DependabotSecretStore).
  • cr.meshweaver.cloud denies anonymous pulls and holds no Docker Hub images. Reading it from a PR would need the publisher (push) password, so it is not used.
  • GHCR ghcr.io/systemorph/dockerhub/ is used for pull-request jobs. It is the fleet's existing mirror of its own images, the same precedent as satellite-compat-image in main-cd. A job reads it with its own GITHUB_TOKEN, so there is no secret to provision, and forks and Dependabot pull the same way. Every entry is pinned by digest. dockerhub-mirror.yml (main push + daily + dispatch) copies each digest with imagetools create and reads it back. dockerhub-mirror.sh ref is red for an undeclared image and never falls back to docker.io. There are no retries anywhere.

The Chart Gate consumers move to the mirror in a follow-up PR, after this merge has synced the mirror on main. Switching them here would make this PR's own gate depend on packages that do not exist yet.

Verification

  • dockerhub-mirror.sh --self-test: 7/7 pass. Negative control: with a wrong prefix, 2 arms fail and rc=1.
  • shellcheck is clean. actionlint is clean on the new workflow. check-workflow-timeouts.py: 0 violations. check-pr-secret-preflight.py: 0 violations.
  • Both listed digests were resolved with a Docker Hub manifest HEAD, which is not counted against the limit. docker_auth's digest equals the chart example's pin.

Measured frequency

Failed core runs created 2026-10-09 20:46:30–20:58:57Z: 7 of 8 carried toomanyrequests (3 Build and Test runs = 23 test-shard jobs; 4 Chart Gate runs = 5 bundle-registry jobs). The sweep was stopped before reading older logs, because log downloads were feeding the secondary rate limit the concurrent agents were already hitting. So no count is claimed for the rest of the week or for MeshWeaver.Plugins (1,758 and 1,298 failed runs listed, not read). Recorded on Doc/Architecture/DockerHubInCi → Measured.

Pairs-with: none — no public C# surface changes (workflow, script and doc only).

🤖 Generated with Claude Code

…e-pull, add a digest-pinned GHCR mirror

Core PR runs went red on Docker Hub's anonymous pull limit (toomanyrequests),
which is counted per egress IP of the shared hosted-runner pool.

- dotnet-test.yml: delete the per-shard pre-pull of pgvector/pgvector:pg17. No
  core suite starts a container any more (the PostgreSql suites left core in the
  test move), so the pull fetched an unused image and was the only failure.
- main-cd.yml: control acceptance passes --postgres with the ACR dockerhub/ copy;
  the job is already logged in to ACR.
- dockerhub-mirror.{list,yml} + scripts/dockerhub-mirror.sh: copy declared Docker
  Hub images by digest to ghcr.io/systemorph/dockerhub/, read back and compare;
  `ref` resolves a declared image (undeclared = red, no docker.io fallback). PR
  jobs read it with GITHUB_TOKEN, so no secret is needed (forks, Dependabot). The
  Chart Gate consumers move onto it in the follow-up, once main has synced it.
- Doc/Architecture/DockerHubInCi.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 9, 2026 21:14
@github-actions

github-actions Bot commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Test Results

    24 files  +    24      24 suites  +24   50m 44s ⏱️ + 50m 44s
11 287 tests +11 287  11 094 ✅ +11 094  193 💤 +193  0 ❌ ±0 
11 300 runs  +11 300  11 107 ✅ +11 107  193 💤 +193  0 ❌ ±0 

Results for commit 933b647. ± Comparison against base commit 23a8893.

♻️ This comment has been updated with latest results.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Scheduled failures are not reported durably, duplicate declarations are ambiguous, and documentation overstates current coverage.

9 open findings
What changed in this PR

Introduces registry mirrors to reduce Docker Hub rate-limit failures in CI.

Changes:

  • Removes the unused pgvector pre-pull.
  • Adds GHCR mirror synchronization and lookup tooling.
  • Routes control-image acceptance through the existing ACR mirror and documents the strategy.
File Description
.github/​dockerhub-mirror.list Declares digest-pinned mirror images.
.github/​scripts/​dockerhub-mirror.sh Resolves and synchronizes mirror entries.
.github/​workflows/​dockerhub-mirror.yml Adds mirror validation and scheduled synchronization.
.github/​workflows/​dotnet-test.yml Removes the obsolete pgvector pre-pull.
.github/​workflows/​main-cd.yml Uses ACR-hosted pgvector for acceptance tests.
src/​MeshWeaver.Documentation/​Data/​Architecture.md Adds the architecture-page index entry.
src/​MeshWeaver.Documentation/​Data/​Architecture/​DockerHubInCi.md Documents the mirror strategy and exceptions.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +24 to +37
entries() {
[ -f "$LIST" ] || { echo "::error::dockerhub-mirror: no list at $LIST" >&2; return 1; }
local line n=0
while IFS= read -r line || [ -n "$line" ]; do
n=$((n + 1))
line="${line%%#*}"; line="$(printf '%s' "$line" | tr -d '[:space:]')"
[ -n "$line" ] || continue
if [[ ! "$line" =~ ^([a-z0-9]+([._-][a-z0-9]+)*(/[a-z0-9]+([._-][a-z0-9]+)*)*):([A-Za-z0-9_][A-Za-z0-9._-]{0,127})@(sha256:[0-9a-f]{64})$ ]]; then
echo "::error::dockerhub-mirror: $LIST line $n is not <name>:<tag>@sha256:<64 hex>: '$line'" >&2
return 1
fi
printf '%s:%s %s\n' "${BASH_REMATCH[1]}" "${BASH_REMATCH[5]}" "${BASH_REMATCH[6]}"
done < "$LIST"
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 005a011: entries() now refuses a second line for the same <name>:<tag> (red, naming the line), so sync and ref can never disagree. New self-test arm: "a second digest for the same name:tag is RED, never first-wins".

Comment on lines +34 to +38
schedule:
# Daily: a mirror whose package was deleted or whose retention dropped a manifest is red the
# same day, not on the next pull request that needs it.
- cron: "40 4 * * *"
workflow_dispatch:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not adding a tracking-issue reporter in this change. sync is self-restoring: on each daily run it copies any declared digest the mirror is missing. A failure it cannot fix also reaches its consumers immediately, because dockerhub-mirror.sh ref and the pull of the pinned digest go red in the Chart Gate step, naming the mirror line. So nothing stays silently unavailable. A sentinel issue is worth adding if a scheduled red ever goes unnoticed, but it is not needed for the fix.

Comment thread .github/scripts/dockerhub-mirror.sh Outdated
Comment on lines +3 to +4
# dockerhub-mirror.sh — the ONE route by which CI reaches a Docker Hub image
# (Doc/Architecture/DockerHubInCi).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 005a011: the header now scopes the script to the runtime images declared in the list, and names FROM base layers as out of scope.

Comment thread .github/workflows/dockerhub-mirror.yml Outdated
Comment on lines +10 to +12
# red no code change could cause or cure. Retrying is not a fix (the limit is a window, not a
# blip); the fix is to stop asking Docker Hub from pull requests at all. This workflow is now the
# ONLY place CI touches docker.io, once per new digest, from main or the schedule.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 005a011: the workflow header now says "for every image DECLARED in the list", excludes path-filtered FROM base layers, and names the Chart Gate follow-up.

Comment thread .github/workflows/dockerhub-mirror.yml Outdated
Comment on lines +35 to +36
# Daily: a mirror whose package was deleted or whose retention dropped a manifest is red the
# same day, not on the next pull request that needs it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 005a011: the schedule comment and the doc now say the job RESTORES a deleted package or manifest, and is red only when it cannot restore it.

- [Rendering a chart you are not allowed to fully configure](ChartDriftRenderWithoutSecrets) — 39 runs, 39 failures, zero verdicts: the check may hold two of the deploy's three value sources and the chart correctly refuses that subset; the placeholder that unblocks the render, the two-render proof that no compared object depends on it, and the bake gate that was off on both production namespaces the moment a verdict finally appeared
- [Configuring an instance from Aspire](ConfiguringAnInstanceFromAspire)
- [The Dependabot Secret Store](DependabotSecretStore)
- [Docker Hub in CI](DockerHubInCi) — hosted runners pull Docker Hub anonymously against a per-IP limit they share with every tenant; CI never asks it from a pull request: a digest-pinned mirror on GHCR read with the job's own token, or the ACR copy where a job is already logged in

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 005a011: the index line now says the mirror is the route for the runtime images CI jobs declare, and points at the exceptions still out of scope.

Comment on lines +10 to +13
**A CI job never pulls from Docker Hub anonymously.** GitHub-hosted runners reach Docker Hub
from a shared pool of egress IPs. The anonymous pull limit is counted per IP, so other tenants
spend it before we do. A `toomanyrequests` answer is therefore a red that no commit causes and
no commit cures. Retrying does not help: the limit is a window of hours, not a blip of seconds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 005a011: the opening invariant is narrowed to declared runtime images, and What this does not cover now lists the staged Chart Gate follow-up next to the FROM exceptions.

Comment on lines +67 to +68
`dockerhub-mirror.yml` is the only place CI talks to `docker.io`. It runs on `main` and on the
schedule, once for each new digest.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 005a011: the sentence now reads "For a declared image, dockerhub-mirror.yml is the only place CI talks to docker.io".

Comment on lines +83 to +85
The sweep read every failed job log in `Systemorph/MeshWeaver` and `Systemorph/MeshWeaver.Plugins`
over the window the run search returned, and counted the logs that contain `toomanyrequests`. The
counts, and the window each count covers, are in the pull request body.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 005a011: the measured result is now recorded on the page itself, with its exact window: 2026-10-09 20:46:30–20:58:57Z, 7 of 8 failed core runs, 23 test-shard jobs plus 5 Chart Gate jobs. The page says explicitly that the sweep stopped before older logs were read (because of the secondary rate limit), and that it claims no count for the rest of the week or for MeshWeaver.Plugins.

rbuergi and others added 2 commits October 9, 2026 23:24
… scope the claims to declared runtime images

Review: a second line for one name:tag let sync mirror both while ref served the
first — now red. The read-back no longer swallows stderr (workflow-shell gate).
Wording narrowed to what the change enforces: declared images only, Chart Gate
in the follow-up, FROM base layers out of scope, deletion is restored not red,
and the measured window stated with what was NOT read.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants