Publish Images #130
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |