omnigraph/.github/workflows/publish-image.yml
Andrew Altshuler d2f9b5c5fb
feat(ci): publish omnigraph-server container image to GHCR on release (#340)
* feat(ci): publish omnigraph-server container image to GHCR on release

Build binaries inside a rust:1-bookworm container (matching the
Dockerfile's bookworm-slim runtime glibc) with the aws feature enabled,
then build and push ghcr.io/<owner>/omnigraph-server:<tag>. Separate
from release.yml so an image failure never blocks the binary release,
and a workflow_dispatch backfill of a past tag never re-runs the release
matrix or repoints :latest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* review fixes: validate dispatch tag (existing v* only), document workflow + GHCR image

- Guard workflow_dispatch: the free-form tag input must name an EXISTING
  v* tag (gh api ref check) and checkout is pinned to refs/tags/<tag>,
  so a branch/SHA can never be published under a release-looking image
  tag (greptile P1).
- docs/dev/ci.md: add the publish-image.yml entry (same-PR docs rule).
- docs/user/deployment.md: document pulling the prebuilt GHCR image.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 02:23:15 +03:00

103 lines
4 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
- name: Report pinned digest
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}"
echo '```'
echo
echo "Pin deployments to the digest form."
} >> "$GITHUB_STEP_SUMMARY"