name: CI and Release / Docker on: pull_request: branches: [main] paths: - '.github/workflows/docker.yml' - 'Dockerfile' - '.dockerignore' - 'docker-compose.yml' - 'deploy/docker/**' - 'pyproject.toml' - 'README.md' - 'LICENSE' - 'reme/**' - 'reme_studio/**' - 'scripts/package_studio.py' - 'scripts/test_docker_image.py' push: branches: [main] paths: - '.github/workflows/docker.yml' - 'Dockerfile' - '.dockerignore' - 'docker-compose.yml' - 'deploy/docker/**' - 'pyproject.toml' - 'README.md' - 'LICENSE' - 'reme/**' - 'reme_studio/**' - 'scripts/package_studio.py' - 'scripts/test_docker_image.py' release: types: [published] workflow_dispatch: permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: PUBLISH_IMAGE: ${{ github.event_name == 'release' || (github.event_name != 'pull_request' && github.ref == 'refs/heads/main') }} jobs: build: name: Build and test / ${{ matrix.arch }} runs-on: ${{ matrix.runner }} timeout-minutes: 60 permissions: contents: read packages: write strategy: fail-fast: false matrix: include: - arch: amd64 runner: ubuntu-24.04 - arch: arm64 runner: ubuntu-24.04-arm steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.11' - name: Validate package version env: RELEASE_VERSION: ${{ github.event.release.tag_name }} run: | python -m pip install packaging if [ -n "$RELEASE_VERSION" ]; then python scripts/bump_version.py --check --expected-version "$RELEASE_VERSION" else python scripts/bump_version.py --check fi - name: Normalize image name id: image env: REPOSITORY: ${{ github.repository }} run: echo "name=ghcr.io/${REPOSITORY,,}" >> "$GITHUB_OUTPUT" - name: Validate Compose run: docker compose config --quiet - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4 - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 id: metadata with: images: ${{ steps.image.outputs.name }} flavor: latest=${{ github.event_name == 'release' && !github.event.release.prerelease && 'auto' || 'false' }} tags: | type=raw,value=main,enable=${{ github.ref == 'refs/heads/main' }} type=pep440,pattern={{version}},value=${{ github.event.release.tag_name }},enable=${{ github.event_name == 'release' }} type=sha - name: Build local image uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7 with: context: . platforms: linux/${{ matrix.arch }} load: true tags: reme:smoke labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha,scope=reme-${{ matrix.arch }} cache-to: type=gha,mode=max,scope=reme-${{ matrix.arch }} - name: Test installed image and persistent workspace run: python scripts/test_docker_image.py --image reme:smoke - name: Log in to GHCR if: env.PUBLISH_IMAGE == 'true' uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Publish tested architecture by digest id: publish if: env.PUBLISH_IMAGE == 'true' uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7 with: context: . platforms: linux/${{ matrix.arch }} outputs: type=image,name=${{ steps.image.outputs.name }},push-by-digest=true,name-canonical=true,push=true labels: ${{ steps.metadata.outputs.labels }} cache-from: type=gha,scope=reme-${{ matrix.arch }} provenance: mode=max sbom: true - name: Record image digest if: env.PUBLISH_IMAGE == 'true' env: IMAGE_DIGEST: ${{ steps.publish.outputs.digest }} run: | mkdir -p "$RUNNER_TEMP/digests" touch "$RUNNER_TEMP/digests/${IMAGE_DIGEST#sha256:}" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: env.PUBLISH_IMAGE == 'true' with: name: docker-digest-${{ matrix.arch }} path: ${{ runner.temp }}/digests/* if-no-files-found: error retention-days: 1 manifest: name: Publish multi-platform tags needs: build if: github.event_name == 'release' || (github.event_name != 'pull_request' && github.ref == 'refs/heads/main') runs-on: ubuntu-24.04 timeout-minutes: 15 permissions: contents: read packages: write steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: docker-digest-* merge-multiple: true path: ${{ runner.temp }}/digests - name: Normalize image name id: image env: REPOSITORY: ${{ github.repository }} run: echo "name=ghcr.io/${REPOSITORY,,}" >> "$GITHUB_OUTPUT" - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4 - uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6 id: metadata with: images: ${{ steps.image.outputs.name }} flavor: latest=${{ github.event_name == 'release' && !github.event.release.prerelease && 'auto' || 'false' }} tags: | type=raw,value=main,enable=${{ github.ref == 'refs/heads/main' }} type=pep440,pattern={{version}},value=${{ github.event.release.tag_name }},enable=${{ github.event_name == 'release' }} type=sha - name: Assemble and verify tags env: IMAGE_NAME: ${{ steps.image.outputs.name }} IMAGE_TAGS: ${{ steps.metadata.outputs.tags }} run: | set -euo pipefail image_refs=() for digest_file in "$RUNNER_TEMP"/digests/*; do image_refs+=("${IMAGE_NAME}@sha256:$(basename "$digest_file")") done [ "${#image_refs[@]}" -eq 2 ] tag_args=() while IFS= read -r tag; do tag_args+=(--tag "$tag") done <<< "$IMAGE_TAGS" docker buildx imagetools create "${tag_args[@]}" "${image_refs[@]}" while IFS= read -r tag; do manifest=$(docker buildx imagetools inspect "$tag" --raw) IMAGE_MANIFEST="$manifest" python - <<'PY' import json import os manifest = json.loads(os.environ["IMAGE_MANIFEST"]) platforms = {(item["platform"]["os"], item["platform"]["architecture"]) for item in manifest["manifests"]} assert {("linux", "amd64"), ("linux", "arm64")} <= platforms, platforms PY done <<< "$IMAGE_TAGS"