Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 47 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ permissions:
env:
GO_VERSION: "1.26"
IMAGE: ghcr.io/labstack/fanout
DOCKERHUB_IMAGE: docker.io/labstack/fanout

jobs:
verify:
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"`,
Expand Down Expand Up @@ -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` |
Expand Down
17 changes: 12 additions & 5 deletions docs/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<version>` and `latest` multi-architecture images;
- canonical `ghcr.io/labstack/fanout:<version>` and `latest`
multi-architecture images;
- byte-identical `labstack/fanout:<version>` 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.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading