name: ci / docker publish # Build the Odysseus image and publish to GHCR. # push to main -> :latest, :X.Y.Z (curated release; main is fast-forwarded at releases) # push to dev -> :dev, :X.Y.Z-dev. (rolling dev + an immutable, traceable pin) # Multi-arch (linux/amd64 + linux/arm64): each arch builds on its own native # runner and pushes by digest, then a merge job stitches the digests into one # manifest list and applies the tags (faster + cleaner than QEMU emulation). # Registry: ghcr.io//. on: push: branches: [dev, main] paths-ignore: - '**.md' - 'docs/**' - '.github/ISSUE_TEMPLATE/**' concurrency: group: docker-publish-${{ github.ref }} cancel-in-progress: true permissions: contents: read env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build: name: build (${{ matrix.arch }}) runs-on: ${{ matrix.runner }} permissions: contents: read packages: write strategy: fail-fast: false matrix: include: - platform: linux/amd64 arch: amd64 runner: ubuntu-latest - platform: linux/arm64 arch: arm64 runner: ubuntu-24.04-arm steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Set up Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Log in to GHCR uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push by digest id: build uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: context: . platforms: ${{ matrix.platform }} outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true cache-from: type=gha,scope=${{ matrix.arch }} cache-to: type=gha,mode=max,scope=${{ matrix.arch }} - name: Export digest run: | mkdir -p /tmp/digests digest="${{ steps.build.outputs.digest }}" touch "/tmp/digests/${digest#sha256:}" - name: Upload digest uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: digest-${{ matrix.arch }} path: /tmp/digests/* if-no-files-found: error retention-days: 1 merge: name: merge manifest + tag runs-on: ubuntu-latest needs: build permissions: contents: read packages: write steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: persist-credentials: false - name: Read APP_VERSION + short sha id: ver run: | v=$(grep -E '^APP_VERSION' src/constants.py | head -1 | sed -E 's/.*"([^"]+)".*/\1/') [ -n "$v" ] || { echo "APP_VERSION not found"; exit 1; } echo "version=$v" >> "$GITHUB_OUTPUT" echo "short=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT" - name: Download digests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: /tmp/digests pattern: digest-* merge-multiple: true - name: Set up Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Log in to GHCR uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Compute tags id: meta uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} type=raw,value=${{ steps.ver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }} type=raw,value=dev,enable=${{ github.ref == 'refs/heads/dev' }} type=raw,value=${{ steps.ver.outputs.version }}-dev.${{ steps.ver.outputs.short }},enable=${{ github.ref == 'refs/heads/dev' }} - name: Create manifest list + push tags working-directory: /tmp/digests run: | tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") digests=$(printf "${REGISTRY}/${IMAGE_NAME}@sha256:%s " *) # word-splitting is intended: $tags and $digests each expand to multiple args # shellcheck disable=SC2086 docker buildx imagetools create $tags $digests env: REGISTRY: ${{ env.REGISTRY }} IMAGE_NAME: ${{ env.IMAGE_NAME }} - name: Inspect run: | if [ "$GITHUB_REF" = "refs/heads/main" ]; then ref=latest; else ref=dev; fi docker buildx imagetools inspect "${REGISTRY}/${IMAGE_NAME}:${ref}" env: REGISTRY: ${{ env.REGISTRY }} IMAGE_NAME: ${{ env.IMAGE_NAME }}