name: Build Image (openflare) on: workflow_dispatch: inputs: version: description: "Image version/tag to publish (e.g. v1.0.0-beta). Leave empty to publish as canary." required: false type: string push: tags: ["v*"] branches: ["canary"] # One active run per ref (e.g. canary); newer runs cancel older in-progress builds. concurrency: group: build-image-openflare-${{ github.ref }} cancel-in-progress: true permissions: contents: read packages: write attestations: write id-token: write env: IMAGE_NAME: openflare DOCKERFILE: docker/Dockerfile jobs: # Resolve version / registries once. No checkout: triggers alone determine the tag. prepare: name: Prepare metadata runs-on: ubuntu-latest outputs: version: ${{ steps.prep.outputs.version }} build_date: ${{ steps.prep.outputs.build_date }} image: ${{ steps.prep.outputs.image }} image_names: ${{ steps.prep.outputs.image_names }} images: ${{ steps.prep.outputs.images }} push_dockerhub: ${{ steps.prep.outputs.push_dockerhub }} is_stable: ${{ steps.prep.outputs.is_stable }} is_prerelease: ${{ steps.prep.outputs.is_prerelease }} steps: - name: Resolve version and images id: prep env: INPUT_VERSION: ${{ github.event.inputs.version }} DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} DOCKERHUB_NAMESPACE: ${{ secrets.DOCKERHUB_NAMESPACE }} run: | set -euo pipefail INPUT_VERSION="${INPUT_VERSION//[[:space:]]/}" OWNER="${GITHUB_REPOSITORY_OWNER,,}" BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" if [[ "${GITHUB_REF}" == refs/heads/canary ]]; then VERSION="canary" elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then VERSION="${GITHUB_REF_NAME}" elif [[ -n "$INPUT_VERSION" ]]; then VERSION="$INPUT_VERSION" elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then VERSION="canary" else echo "unable to determine image version/tag" >&2 exit 1 fi if [[ "$VERSION" == "canary" ]]; then IS_STABLE="false" IS_PRERELEASE="false" elif [[ "$VERSION" =~ (alpha|beta|rc) ]]; then IS_STABLE="false" IS_PRERELEASE="true" else IS_STABLE="true" IS_PRERELEASE="false" fi IMAGE="ghcr.io/${OWNER}/${IMAGE_NAME}" IMAGE_NAMES="${IMAGE}" # Newline-separated list for docker/metadata-action IMAGES="${IMAGE}" DOCKERHUB_USERNAME="${DOCKERHUB_USERNAME//[[:space:]]/}" DOCKERHUB_TOKEN="${DOCKERHUB_TOKEN//[[:space:]]/}" DOCKERHUB_NAMESPACE="${DOCKERHUB_NAMESPACE//[[:space:]]/}" PUSH_DOCKERHUB="false" if [[ -n "$DOCKERHUB_USERNAME" && -n "$DOCKERHUB_TOKEN" ]]; then HUB_NS="${DOCKERHUB_NAMESPACE:-$DOCKERHUB_USERNAME}" HUB_NS="${HUB_NS,,}" IMAGE_DOCKERHUB="${HUB_NS}/${IMAGE_NAME}" IMAGE_NAMES="${IMAGE_NAMES},${IMAGE_DOCKERHUB}" IMAGES="${IMAGES}"$'\n'"${IMAGE_DOCKERHUB}" PUSH_DOCKERHUB="true" echo "Docker Hub publish enabled: ${IMAGE_DOCKERHUB}" else echo "Docker Hub secrets not set; publishing to GHCR only." fi { echo "version=${VERSION}" echo "build_date=${BUILD_DATE}" echo "image=${IMAGE}" echo "image_names=${IMAGE_NAMES}" echo "push_dockerhub=${PUSH_DOCKERHUB}" echo "is_stable=${IS_STABLE}" echo "is_prerelease=${IS_PRERELEASE}" echo "images<> "$GITHUB_OUTPUT" echo "Resolved version=${VERSION} build_date=${BUILD_DATE} stable=${IS_STABLE} prerelease=${IS_PRERELEASE}" build: name: Build (${{ matrix.arch }}) needs: prepare strategy: fail-fast: false matrix: include: - arch: amd64 platform: linux/amd64 runner: ubuntu-latest - arch: arm64 platform: linux/arm64 # No ubuntu-latest-arm alias from GitHub; 24.04-arm is the current stable arm64 image. runner: ubuntu-24.04-arm runs-on: ${{ matrix.runner }} steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 1 persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Log into GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log into Docker Hub if: needs.prepare.outputs.push_dockerhub == 'true' uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push id: build uses: docker/build-push-action@v7 with: context: . file: ${{ env.DOCKERFILE }} platforms: ${{ matrix.platform }} outputs: type=image,"name=${{ needs.prepare.outputs.image_names }}",push-by-digest=true,name-canonical=true,push=true build-args: | VERSION=${{ needs.prepare.outputs.version }} BUILD_DATE=${{ needs.prepare.outputs.build_date }} cache-from: type=gha,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }} cache-to: type=gha,mode=max,ignore-error=true,timeout=20m,scope=docker-${{ env.IMAGE_NAME }}-${{ matrix.arch }} - name: Export digest shell: bash run: | mkdir -p "/tmp/${{ env.IMAGE_NAME }}-digests" touch "/tmp/${{ env.IMAGE_NAME }}-digests/${DIGEST#sha256:}" env: DIGEST: ${{ steps.build.outputs.digest }} - name: Upload digest uses: actions/upload-artifact@v4 with: name: ${{ env.IMAGE_NAME }}-digests-${{ matrix.arch }} path: /tmp/${{ env.IMAGE_NAME }}-digests/* if-no-files-found: error retention-days: 1 - name: Generate artifact attestation uses: actions/attest-build-provenance@v3 with: subject-name: ${{ needs.prepare.outputs.image }} subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true merge: name: Merge multi-arch manifest runs-on: ubuntu-latest needs: [prepare, build] steps: # No repo checkout: tags come from prepare + metadata-action. - name: Docker meta id: meta uses: docker/metadata-action@v5 with: images: ${{ needs.prepare.outputs.images }} flavor: | latest=false tags: | type=raw,value=${{ needs.prepare.outputs.version }} type=raw,value=latest,enable=${{ needs.prepare.outputs.is_stable == 'true' }} type=raw,value=beta,enable=${{ needs.prepare.outputs.is_prerelease == 'true' }} - name: Download digests uses: actions/download-artifact@v4 with: path: /tmp/${{ env.IMAGE_NAME }}-digests pattern: ${{ env.IMAGE_NAME }}-digests-* merge-multiple: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v4 - name: Log into GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Log into Docker Hub if: needs.prepare.outputs.push_dockerhub == 'true' uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Create and push manifest list working-directory: /tmp/${{ env.IMAGE_NAME }}-digests shell: bash env: IMAGE: ${{ needs.prepare.outputs.image }} DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }} run: | set -euo pipefail shopt -s nullglob references=() for digest in *; do references+=("${IMAGE}@sha256:${digest}") done if [ ${#references[@]} -eq 0 ]; then echo "No digests found in /tmp/${{ env.IMAGE_NAME }}-digests" >&2 exit 1 fi # shellcheck disable=SC2046 docker buildx imagetools create \ $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ "${references[@]}" - name: Inspect image run: docker buildx imagetools inspect "${{ needs.prepare.outputs.image }}:${{ needs.prepare.outputs.version }}" - name: Trigger webhook env: WEBHOOK_URL: ${{ secrets.WEBHOOK_URL }} run: | if [ -n "$WEBHOOK_URL" ]; then curl -fsSL "$WEBHOOK_URL" else echo "Webhook URL is not set, skipping." fi