From 9c601b38fd1ba31eca5e8a41e3b6828e781154ba Mon Sep 17 00:00:00 2001 From: aaltshuler Date: Wed, 8 Jul 2026 20:14:45 +0300 Subject: [PATCH 1/2] feat(ci): publish omnigraph-server image to Docker Hub alongside GHCR Docker Hub (docker.io/modernrelay/omnigraph-server) becomes the primary anonymous-pull distribution channel; the step no-ops when the DOCKERHUB_* secrets are unset so forks and the GHCR-only path keep working. Same latest-tag policy as GHCR (real tag pushes only, never dispatch backfills). Docs updated in the same PR (ci.md, deployment.md). --- .github/workflows/publish-image.yml | 44 +++++++++++++++++++++++++---- docs/dev/ci.md | 2 +- docs/user/deployment.md | 5 ++-- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index 7c73c53c..0d17e8e0 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -86,17 +86,51 @@ jobs: docker push "${image}:latest" fi - - name: Report pinned digest + # Docker Hub is the primary public distribution channel (anonymous pulls + # for deployments). Skipped when the DOCKERHUB_* secrets are not + # configured, so forks and the GHCR-only path keep working. With an org + # access token, DOCKERHUB_USERNAME is the ORG name — which is also the + # image namespace. + - name: Push to Docker Hub + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} + DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} + run: | + if [[ -z "${DOCKERHUB_USERNAME}" || -z "${DOCKERHUB_TOKEN}" ]]; then + echo "DOCKERHUB_* secrets not configured; skipping Docker Hub publish." + exit 0 + fi + owner="${GITHUB_REPOSITORY_OWNER,,}" + ghcr_image="ghcr.io/${owner}/omnigraph-server" + hub_image="docker.io/${DOCKERHUB_USERNAME}/omnigraph-server" + + echo "${DOCKERHUB_TOKEN}" \ + | docker login docker.io --username "${DOCKERHUB_USERNAME}" --password-stdin + + docker tag "${ghcr_image}:${RELEASE_TAG}" "${hub_image}:${RELEASE_TAG}" + docker push "${hub_image}:${RELEASE_TAG}" + + # Same `latest` policy as GHCR: real tag pushes only, never backfills. + if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then + docker tag "${ghcr_image}:${RELEASE_TAG}" "${hub_image}:latest" + docker push "${hub_image}:latest" + fi + + - name: Report pinned digests + env: + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} run: | owner="${GITHUB_REPOSITORY_OWNER,,}" - image="ghcr.io/${owner}/omnigraph-server" - digest=$(docker inspect --format='{{index .RepoDigests 0}}' "${image}:${RELEASE_TAG}") { echo "## Container image published" echo echo '```' - echo "${image}:${RELEASE_TAG}" - echo "${digest}" + for image in "ghcr.io/${owner}/omnigraph-server" \ + ${DOCKERHUB_USERNAME:+"docker.io/${DOCKERHUB_USERNAME}/omnigraph-server"}; do + echo "${image}:${RELEASE_TAG}" + docker inspect --format='{{range .RepoDigests}}{{.}}{{"\n"}}{{end}}' \ + "${image}:${RELEASE_TAG}" | grep "^${image}@" || true + done echo '```' echo echo "Pin deployments to the digest form." diff --git a/docs/dev/ci.md b/docs/dev/ci.md index f88dfd40..a32ef2c9 100644 --- a/docs/dev/ci.md +++ b/docs/dev/ci.md @@ -12,4 +12,4 @@ - **release-edge.yml**: on every push to main, retags `edge`, builds Linux x86_64 / Linux arm64 / macOS arm64 archives and Windows x86_64 zip + sha256, publishes a rolling prerelease, then smoke-tests the Windows PowerShell installer against `edge`. - **release.yml**: on `v*` tags, builds the Linux x86_64 / Linux arm64 / macOS arm64 archives and Windows x86_64 zip release matrix, updates the Homebrew tap (`scripts/update-homebrew-formula.sh`) by pushing the regenerated formula to `ModernRelay/homebrew-tap`, and smoke-tests the Windows PowerShell installer against the tag. - **package.yml**: manual ECR image build; emits two image tags per commit (``, `-aws`) via CodeBuild. -- **publish-image.yml**: builds and pushes the public `omnigraph-server` container image to GHCR (`ghcr.io/modernrelay/omnigraph-server:`) on `v*` tag pushes, plus manual `workflow_dispatch` with an explicit existing `v*` tag to backfill an image for a past release. Compiles the binaries inside a `rust:1-bookworm` builder container to match the Dockerfile's bookworm-slim runtime glibc (host-built ubuntu binaries do not run there), builds with `--features omnigraph-server/aws`, and moves `latest` only on real tag pushes — never on backfill dispatches. Separate from release.yml so an image-publish failure cannot block the binary release / Homebrew chain. +- **publish-image.yml**: builds and pushes the public `omnigraph-server` container image to GHCR (`ghcr.io/modernrelay/omnigraph-server:`) and to Docker Hub (`docker.io/modernrelay/omnigraph-server:`, the primary anonymous-pull channel; skipped when the `DOCKERHUB_*` secrets are unset) on `v*` tag pushes, plus manual `workflow_dispatch` with an explicit existing `v*` tag to backfill an image for a past release. Compiles the binaries inside a `rust:1-bookworm` builder container to match the Dockerfile's bookworm-slim runtime glibc (host-built ubuntu binaries do not run there), builds with `--features omnigraph-server/aws`, and moves `latest` only on real tag pushes — never on backfill dispatches. Separate from release.yml so an image-publish failure cannot block the binary release / Homebrew chain. diff --git a/docs/user/deployment.md b/docs/user/deployment.md index 53617533..6c5503f2 100644 --- a/docs/user/deployment.md +++ b/docs/user/deployment.md @@ -172,11 +172,12 @@ endpoint and credentials. CI exercises this path against containerized RustFS. ## Container Deployment -Pull the prebuilt public image (published to GHCR for every `v*` release by +Pull the prebuilt public image (published for every `v*` release by `publish-image.yml`; built with the `aws` feature, linux/amd64): ```bash -docker pull ghcr.io/modernrelay/omnigraph-server:v0.8.1 +docker pull modernrelay/omnigraph-server:v0.8.1 # Docker Hub +# or: docker pull ghcr.io/modernrelay/omnigraph-server:v0.8.1 ``` Or build it yourself: From 2c56a67507626ae6df17639d2c8c5eda92d7ab28 Mon Sep 17 00:00:00 2001 From: aaltshuler Date: Wed, 8 Jul 2026 20:23:05 +0300 Subject: [PATCH 2/2] review fixes: preserve digest summary on Hub failure, tolerate unprefixed RepoDigests - Docker Hub push is continue-on-error with an explicit fail-gate AFTER the digest report, so a Hub failure still fails the run but can no longer suppress the summary for the already-pushed GHCR image. - RepoDigests may store Hub images without the docker.io/ prefix depending on Docker version; the digest grep now accepts both forms. --- .github/workflows/publish-image.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index 0d17e8e0..de74852f 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -91,7 +91,12 @@ jobs: # configured, so forks and the GHCR-only path keep working. With an org # access token, DOCKERHUB_USERNAME is the ORG name — which is also the # image namespace. + # continue-on-error so a Hub failure can't suppress the digest summary + # for the already-pushed GHCR image; the fail-gate step below still + # fails the run afterwards. - name: Push to Docker Hub + id: dockerhub + continue-on-error: true env: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} @@ -128,10 +133,20 @@ jobs: for image in "ghcr.io/${owner}/omnigraph-server" \ ${DOCKERHUB_USERNAME:+"docker.io/${DOCKERHUB_USERNAME}/omnigraph-server"}; do echo "${image}:${RELEASE_TAG}" + # RepoDigests may store Hub images with or without the + # docker.io/ prefix depending on the Docker version. docker inspect --format='{{range .RepoDigests}}{{.}}{{"\n"}}{{end}}' \ - "${image}:${RELEASE_TAG}" | grep "^${image}@" || true + "${image}:${RELEASE_TAG}" | grep -E "^(docker\.io/)?${image#docker.io/}@" || true done echo '```' echo echo "Pin deployments to the digest form." } >> "$GITHUB_STEP_SUMMARY" + + # The summary above always runs; a Docker Hub failure still fails the + # run here so operators notice. + - name: Fail if Docker Hub push failed + if: steps.dockerhub.outcome == 'failure' + run: | + echo "Docker Hub push failed (GHCR image was pushed; see summary)." >&2 + exit 1