diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml new file mode 100644 index 00000000..7c73c53c --- /dev/null +++ b/.github/workflows/publish-image.yml @@ -0,0 +1,103 @@ +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" diff --git a/docs/dev/ci.md b/docs/dev/ci.md index aed0eed5..f88dfd40 100644 --- a/docs/dev/ci.md +++ b/docs/dev/ci.md @@ -12,3 +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. diff --git a/docs/user/deployment.md b/docs/user/deployment.md index 1772b9ac..53617533 100644 --- a/docs/user/deployment.md +++ b/docs/user/deployment.md @@ -172,7 +172,14 @@ endpoint and credentials. CI exercises this path against containerized RustFS. ## Container Deployment -Build the image: +Pull the prebuilt public image (published to GHCR 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 +``` + +Or build it yourself: ```bash docker build -t omnigraph-server:local .