omnigraph/.github/workflows/publish-image.yml
Andrew Altshuler 0c8d769501
Some checks failed
CI / Classify Changes (push) Has been cancelled
CI / Check AGENTS.md Links (push) Has been cancelled
CI / Container Entrypoint (push) Has been cancelled
Release Edge / Prepare edge release (push) Has been cancelled
CI / Test Workspace (push) Has been cancelled
CI / Test omnigraph-server --features aws (push) Has been cancelled
CI / RustFS S3 Integration (cli) (push) Has been cancelled
CI / RustFS S3 Integration (cluster) (push) Has been cancelled
CI / RustFS S3 Integration (engine) (push) Has been cancelled
CI / RustFS S3 Integration (failpoints) (push) Has been cancelled
CI / RustFS S3 Integration (server) (push) Has been cancelled
Release Edge / Build edge omnigraph-linux-arm64 (push) Has been cancelled
Release Edge / Build edge omnigraph-linux-x86_64 (push) Has been cancelled
Release Edge / Build edge omnigraph-macos-arm64 (push) Has been cancelled
Release Edge / Build edge omnigraph-windows-x86_64 (push) Has been cancelled
Release Edge / Smoke Windows installer (push) Has been cancelled
feat(ci): publish omnigraph-server image to Docker Hub alongside GHCR (#341)
* 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).

* 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.
2026-07-08 20:28:28 +03:00

152 lines
6.3 KiB
YAML

name: Publish container image
# Build and publish the omnigraph-server container image to GHCR so
# downstream deployments can pull it without building from source.
#
# Kept separate from release.yml on purpose: an image-publish failure must
# not block the binary release / Homebrew chain, and a manual backfill of an
# old tag must not re-run the four-platform release matrix.
#
# Triggers (same dual pattern as release.yml):
# - push of a v* tag (normal release)
# - workflow_dispatch with an explicit `tag` (publish an image for a past
# tag; resolves the same `${{ inputs.tag || github.ref_name }}`)
#
# The binaries are compiled inside a rust:1-bookworm container, NOT on the
# runner host: the Dockerfile's runtime base is debian bookworm-slim
# (glibc 2.36), and binaries built on ubuntu-latest (glibc 2.39) do not run
# there. Builder and runtime must share the same distro release.
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
tag:
description: "Tag to publish an image for (e.g. v0.8.1). Required for manual dispatches."
required: true
type: string
jobs:
publish_image:
name: Build and push image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
RELEASE_TAG: ${{ inputs.tag || github.ref_name }}
steps:
# The dispatch `tag` input is free-form; without this guard a dispatcher
# could pass a branch name or SHA and publish non-release code under a
# release-looking image tag. Require an existing v* tag ref.
- name: Validate dispatch tag
if: github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ github.token }}
run: |
[[ "${RELEASE_TAG}" == v* ]] \
|| { echo "tag must be a v* release tag, got: ${RELEASE_TAG}" >&2; exit 1; }
gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${RELEASE_TAG}" >/dev/null \
|| { echo "no such tag: ${RELEASE_TAG}" >&2; exit 1; }
- name: Checkout source
uses: actions/checkout@v5.0.1
with:
# Fully qualified so a branch of the same name can never shadow the tag.
ref: refs/tags/${{ env.RELEASE_TAG }}
- name: Build release binaries (bookworm builder)
run: |
docker run --rm -v "$PWD:/work" -w /work rust:1-bookworm bash -euxc '
apt-get update
apt-get install -y --no-install-recommends protobuf-compiler libprotobuf-dev
# --features omnigraph-server/aws: include the AWS Secrets Manager
# bearer-token source so one public image serves both token modes.
cargo build --release --locked -p omnigraph-cli -p omnigraph-server --features omnigraph-server/aws
'
- name: Log in to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" \
| docker login ghcr.io --username "${{ github.actor }}" --password-stdin
- name: Build and push image
run: |
owner="${GITHUB_REPOSITORY_OWNER,,}"
image="ghcr.io/${owner}/omnigraph-server"
docker build -t "${image}:${RELEASE_TAG}" .
docker push "${image}:${RELEASE_TAG}"
# Move `latest` only on real tag pushes — a dispatch backfill of an
# old tag must not repoint `latest` at it.
if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then
docker tag "${image}:${RELEASE_TAG}" "${image}:latest"
docker push "${image}:latest"
fi
# 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,,}"
{
echo "## Container image published"
echo
echo '```'
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