diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5e6ba8bd..83f34656 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,7 @@ permissions: env: GO_VERSION: "1.26" IMAGE: ghcr.io/labstack/fanout + DOCKERHUB_IMAGE: docker.io/labstack/fanout jobs: verify: @@ -189,7 +190,7 @@ jobs: docker buildx imagetools inspect "${IMAGE}:${version}" anonymous-image: - name: anonymous image pull + name: anonymous GHCR pull needs: manifest runs-on: ubuntu-24.04 steps: @@ -206,9 +207,53 @@ jobs: echo "release image is not anonymously pullable" >&2 exit 1 + dockerhub: + name: mirror image to Docker Hub + needs: manifest + runs-on: ubuntu-24.04 + steps: + - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 + - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Copy the accepted multi-architecture manifest + run: | + set -euo pipefail + version="${GITHUB_REF_NAME#v}" + docker buildx imagetools create \ + --tag "${DOCKERHUB_IMAGE}:${version}" \ + --tag "${DOCKERHUB_IMAGE}:latest" \ + "${IMAGE}:${version}" + source_digest="$(docker buildx imagetools inspect "${IMAGE}:${version}" --format '{{.Manifest.Digest}}')" + mirror_digest="$(docker buildx imagetools inspect "${DOCKERHUB_IMAGE}:${version}" --format '{{.Manifest.Digest}}')" + [ "${mirror_digest}" = "${source_digest}" ] || { + echo "Docker Hub digest ${mirror_digest} does not match GHCR ${source_digest}" >&2 + exit 1 + } + + anonymous-dockerhub: + name: anonymous Docker Hub pull + needs: dockerhub + runs-on: ubuntu-24.04 + steps: + - name: Verify the public mirror + run: | + set -euo pipefail + version="${GITHUB_REF_NAME#v}" + for attempt in 1 2 3 4 5; do + if docker pull "${DOCKERHUB_IMAGE}:${version}"; then + exit 0 + fi + echo "anonymous Docker Hub pull attempt ${attempt} failed; retrying" >&2 + sleep 10 + done + echo "Docker Hub release image is not anonymously pullable" >&2 + exit 1 + release: name: publish release - needs: [binaries, manifest, anonymous-image] + needs: [binaries, manifest, anonymous-image, anonymous-dockerhub] runs-on: ubuntu-24.04 permissions: attestations: write diff --git a/README.md b/README.md index 2a807c10..62a54e4e 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ choose the installation directory. docker run --name fanout -p 7520:7520 -p 4317:4317 -p 4318:4318 \ -v fanout-data:/var/lib/fanout/data \ -e FANOUT_AUTH_CODE_SECRET=$(openssl rand -hex 32) \ - ghcr.io/labstack/fanout:latest + labstack/fanout:latest ``` Open the one-time setup URL printed by the container and create the first @@ -135,7 +135,7 @@ the externally reachable HTTP and OTLP bind addresses: cp fanout.docker.yaml fanout.yaml # Add the remaining settings, then mount it over the image document: docker run -v ./fanout.yaml:/etc/fanout/fanout.yaml:ro \ - ghcr.io/labstack/fanout:latest + labstack/fanout:latest ``` A replacement document must set `server.http_addr: ":7520"`, @@ -276,10 +276,13 @@ docs/diagrams/ d2 sources and rendered SVG Versions are CalVer — `v{YYYY.M}.{N}`, numbered from 0 within each month, so `v2026.8.1` is the second release of August 2026. Pushing a tag publishes the -matching image and moves `latest`: +same release manifest to Docker Hub and GHCR and moves both `latest` tags. +GHCR remains the canonical registry and also carries development images: | Image tag | Points at | | --- | --- | +| `labstack/fanout:latest` | the newest release, mirrored on Docker Hub | +| `labstack/fanout:2026.8.0` | that exact release, mirrored on Docker Hub | | `ghcr.io/labstack/fanout:latest` | the newest release | | `ghcr.io/labstack/fanout:2026.8.0` | that exact release | | `ghcr.io/labstack/fanout:main` | the tip of `main` | diff --git a/docs/release.md b/docs/release.md index c926195f..2acbb70b 100644 --- a/docs/release.md +++ b/docs/release.md @@ -3,7 +3,9 @@ Fanout publishes one release from a verified commit on `main`. A release contains: - native Linux and macOS archives for amd64 and arm64; -- `ghcr.io/labstack/fanout:` and `latest` multi-architecture images; +- canonical `ghcr.io/labstack/fanout:` and `latest` + multi-architecture images; +- byte-identical `labstack/fanout:` and `latest` Docker Hub mirrors; - SHA-256 checksums and GitHub build-provenance attestations for every archive; - the Apache-2.0 license, project notice, trademark policy, and generated third-party notices in every archive and container image. @@ -12,7 +14,9 @@ Fanout publishes one release from a verified commit on `main`. A release contain Tags use unpadded CalVer: `vYYYY.M.N`. The first release in a month is `.0`. Container tags omit the leading `v`, so `v2026.8.0` publishes -`ghcr.io/labstack/fanout:2026.8.0`. +`ghcr.io/labstack/fanout:2026.8.0` and `labstack/fanout:2026.8.0`. +Only release and `latest` tags are mirrored to Docker Hub; `main` and commit +tags remain GHCR-only. Onebox consumes that public container reference. Its deployment planner resolves the tag to an immutable registry digest before release, so production manifests @@ -31,9 +35,11 @@ Run `just release` from a clean, current `main`. The script: The tag workflow then independently validates tag syntax, ancestry, and ordering; builds every target on its native runner; publishes the multi-architecture image; -and creates the GitHub release only after an anonymous image pull succeeds. A -final job downloads every release asset without credentials, verifies checksums, -inspects the legal payload, exercises the installer, and verifies provenance. +copies that accepted manifest to Docker Hub; requires the registry digests to +match; and creates the GitHub release only after anonymous pulls from both GHCR +and Docker Hub succeed. A final job downloads every release asset without +credentials, verifies checksums, inspects the legal payload, exercises the +installer, and verifies provenance. Fanout does not currently use GoReleaser. DuckDB requires CGO and the supported targets are built on four native runners. GoReleaser's supported split-and-merge @@ -59,6 +65,7 @@ For containers, pin or record the digest returned by the registry: ```sh docker buildx imagetools inspect ghcr.io/labstack/fanout:2026.8.0 +docker buildx imagetools inspect docker.io/labstack/fanout:2026.8.0 ``` ## Supply-chain scope diff --git a/justfile b/justfile index 6919db2f..dfb00cfa 100644 --- a/justfile +++ b/justfile @@ -63,7 +63,7 @@ docker TAG="local": # Tags are CalVer: v{YYYY.M}.{N}, numbered from 0 within each month. Pushing # the tag triggers release.yml, which builds the multi-architecture image, -# publishes native archives, and moves `latest`. +# mirrors it to Docker Hub, publishes native archives, and moves `latest`. # Tag the next CalVer release and push it. release: