Skip to content

Publish Images

Publish Images #133

Workflow file for this run

name: Publish Images
# The automatic path is CI -> Publish Images -> Deploy Delta, chained on
# workflow_run. That chain has a single point of failure outside our control:
# when GitHub throttles webhook delivery during an Actions incident, no
# workflow_run event fires and nothing publishes, so Deploy Delta has no image
# to deploy and main sits undeployed with no way to force it. workflow_dispatch
# goes through the REST API instead, which stays up independently, so this is
# the manual door for exactly that case.
on:
workflow_run:
workflows:
- CI
branches:
- main
types:
- completed
workflow_dispatch:
inputs:
sha:
description: Full commit SHA on main to build (defaults to the tip of main)
required: false
concurrency:
group: publish-${{ github.event.workflow_run.head_sha || inputs.sha || github.sha }}
cancel-in-progress: false
permissions:
contents: read
packages: write
jobs:
publish:
# Both paths publish only main. The automatic one gets that from the
# workflow_run fields; the manual one is pinned to the main ref here and
# then has its resolved SHA checked for ancestry below, so a dispatch
# cannot publish a commit that never landed on main.
if: >
(github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') ||
(
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.name == 'CI' &&
github.event.workflow_run.head_branch == 'main' &&
github.event.workflow_run.event == 'push'
)
runs-on: ubuntu-latest
steps:
- name: Resolve SHA
id: sha
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
sha="${{ inputs.sha }}"
if [[ -z "$sha" ]]; then
sha="${{ github.sha }}"
fi
else
sha="${{ github.event.workflow_run.head_sha }}"
fi
if [[ ! "$sha" =~ ^[0-9a-fA-F]{40}$ ]]; then
echo "invalid full commit SHA: $sha" >&2
exit 1
fi
# A hand-supplied SHA is the one input here that is not already
# constrained to main, so confirm it is on main before we build an
# image Deploy Delta would treat as a production release. Comparing
# main to the commit reports "identical" when it is the tip and
# "behind" when it is an earlier commit on main; anything else means
# it lives on some other branch.
if [[ "${{ github.event_name }}" == "workflow_dispatch" && -n "${{ inputs.sha }}" ]]; then
status="$(gh api "repos/${GITHUB_REPOSITORY}/compare/main...${sha}" --jq .status)"
if [[ "$status" != "identical" && "$status" != "behind" ]]; then
echo "refusing to publish $sha: not a commit on main (compare status: $status)" >&2
exit 1
fi
fi
echo "value=$sha" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v4
with:
ref: ${{ steps.sha.outputs.value }}
- name: Compute image names
id: image
run: |
repo="${GITHUB_REPOSITORY,,}"
echo "backend=ghcr.io/${repo}-backend" >> "$GITHUB_OUTPUT"
echo "frontend=ghcr.io/${repo}-frontend" >> "$GITHUB_OUTPUT"
echo "embeddings=ghcr.io/${repo}-embeddings" >> "$GITHUB_OUTPUT"
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# The sidecar image is tagged by content rather than by commit: the git
# tree hash of embeddings/ changes only when something in that directory
# changes. deploy/scripts/deploy.sh derives the identical value, so the two
# cannot disagree.
- name: Compute the embeddings tag
id: embeddings_tag
run: echo "value=$(git rev-parse HEAD:embeddings)" >> "$GITHUB_OUTPUT"
# This image bakes in ~130MB of model weights, so building it is by far the
# slowest step here -- and it was rebuilt on every push to main, including
# the overwhelming majority that touch nothing but Go or TypeScript. A tag
# that already exists is by definition built from identical content.
- name: Check whether the embeddings image already exists
id: embeddings_exists
run: |
if docker manifest inspect "${{ steps.image.outputs.embeddings }}:${{ steps.embeddings_tag.outputs.value }}" >/dev/null 2>&1; then
echo "skipping the embeddings build: ${{ steps.embeddings_tag.outputs.value }} is already published"
echo "value=true" >> "$GITHUB_OUTPUT"
else
echo "value=false" >> "$GITHUB_OUTPUT"
fi
- name: Build and publish backend
uses: docker/build-push-action@v6
with:
context: ./server
file: ./server/Dockerfile
push: true
tags: ${{ steps.image.outputs.backend }}:${{ steps.sha.outputs.value }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ steps.sha.outputs.value }}
cache-from: type=gha,scope=backend
cache-to: type=gha,mode=max,scope=backend
- name: Build and publish frontend
uses: docker/build-push-action@v6
with:
context: ./frontend
file: ./frontend/Dockerfile
push: true
tags: ${{ steps.image.outputs.frontend }}:${{ steps.sha.outputs.value }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ steps.sha.outputs.value }}
cache-from: type=gha,scope=frontend
cache-to: type=gha,mode=max,scope=frontend
- name: Build and publish embeddings sidecar
if: steps.embeddings_exists.outputs.value != 'true'
uses: docker/build-push-action@v6
with:
context: ./embeddings
file: ./embeddings/Dockerfile
push: true
tags: ${{ steps.image.outputs.embeddings }}:${{ steps.embeddings_tag.outputs.value }}
labels: |
org.opencontainers.image.source=https://github.com/${{ github.repository }}
org.opencontainers.image.revision=${{ steps.sha.outputs.value }}
cache-from: type=gha,scope=embeddings
cache-to: type=gha,mode=max,scope=embeddings