diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index 7c73c53c..de74852f 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -86,18 +86,67 @@ 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. + # 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 }} + 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}" + # 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 -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 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: