name: Publish Docker Hub image on: push: tags: - "v*.*.*" permissions: contents: read env: IMAGE_NAME: gxmandppx/nodeflare concurrency: group: docker-hub-${{ github.ref }} cancel-in-progress: false jobs: metadata: name: Resolve image tags runs-on: ubuntu-latest timeout-minutes: 5 outputs: version: ${{ steps.version.outputs.version }} tags: ${{ steps.version.outputs.tags }} rust_image: ${{ steps.base_images.outputs.rust_image }} alpine_image: ${{ steps.base_images.outputs.alpine_image }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Resolve version and tags id: version shell: bash env: NODEFLARE_VERSION: ${{ github.ref_name }} run: | set -euo pipefail version=$(sh scripts/resolve-version.sh) printf 'version=%s\ntags=%s,latest,sha-%.12s\n' "$version" "$version" "$GITHUB_SHA" >> "$GITHUB_OUTPUT" - name: Log in to Docker Hub uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ vars.DOCKER_USERNAME || secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Resolve latest stable base images id: base_images shell: bash run: | set -euo pipefail # Resolve once so both architectures and the test/push builds use the same images. for item in rust_image=rust:1-alpine alpine_image=alpine:latest; do key=${item%%=*} image=${item#*=} digest=$(docker buildx imagetools inspect "$image" --format '{{json .Manifest}}' | jq -er '.digest') if [[ ! "$digest" =~ ^sha256:[a-f0-9]{64}$ ]]; then echo "Invalid base image digest for $image" >&2 exit 1 fi printf '%s=%s@%s\n' "$key" "$image" "$digest" >> "$GITHUB_OUTPUT" done build: name: Build and verify (${{ matrix.arch }}) needs: metadata runs-on: ${{ matrix.runner }} timeout-minutes: 60 strategy: fail-fast: false matrix: include: - arch: amd64 runner: ubuntu-latest - arch: arm64 runner: ubuntu-24.04-arm steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Log in to Docker Hub uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ vars.DOCKER_USERNAME || secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Build image for smoke test uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . platforms: linux/${{ matrix.arch }} build-args: | NODEFLARE_VERSION=${{ needs.metadata.outputs.version }} RUST_IMAGE=${{ needs.metadata.outputs.rust_image }} ALPINE_IMAGE=${{ needs.metadata.outputs.alpine_image }} tags: nodeflare:smoke load: true provenance: false cache-from: type=gha,scope=nodeflare-${{ matrix.arch }} - name: Verify version, startup and frontend assets env: NODEFLARE_VERSION: ${{ needs.metadata.outputs.version }} run: sh scripts/smoke-test-docker.sh nodeflare:smoke - name: Push verified image by digest id: push uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0 with: context: . platforms: linux/${{ matrix.arch }} build-args: | NODEFLARE_VERSION=${{ needs.metadata.outputs.version }} RUST_IMAGE=${{ needs.metadata.outputs.rust_image }} ALPINE_IMAGE=${{ needs.metadata.outputs.alpine_image }} labels: | org.opencontainers.image.title=NodeFlare org.opencontainers.image.description=NodeFlare server with public and admin frontends org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.version=${{ needs.metadata.outputs.version }} org.opencontainers.image.licenses=MIT outputs: type=image,name=${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true provenance: true sbom: true cache-from: type=gha,scope=nodeflare-${{ matrix.arch }} cache-to: type=gha,mode=max,scope=nodeflare-${{ matrix.arch }} - name: Export image digest env: IMAGE_DIGEST: ${{ steps.push.outputs.digest }} run: | mkdir -p "$RUNNER_TEMP/nodeflare-digests" touch "$RUNNER_TEMP/nodeflare-digests/${IMAGE_DIGEST#sha256:}" - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: docker-digest-${{ matrix.arch }} path: ${{ runner.temp }}/nodeflare-digests/* if-no-files-found: error retention-days: 1 publish: name: Publish multi-platform tags needs: [metadata, build] runs-on: ubuntu-latest timeout-minutes: 10 env: IMAGE_TAGS: ${{ needs.metadata.outputs.tags }} steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: docker-digest-* path: ${{ runner.temp }}/nodeflare-digests merge-multiple: true - name: Log in to Docker Hub uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: username: ${{ vars.DOCKER_USERNAME || secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0 - name: Create and inspect image manifest shell: bash run: | set -euo pipefail IFS=, read -r -a tags <<< "$IMAGE_TAGS" tag_args=() for tag in "${tags[@]}"; do tag_args+=(--tag "$IMAGE_NAME:$tag") done digest_args=() for file in "$RUNNER_TEMP"/nodeflare-digests/*; do digest=${file##*/} if [[ ! "$digest" =~ ^[a-f0-9]{64}$ ]]; then echo "Invalid image digest artifact" >&2 exit 1 fi digest_args+=("$IMAGE_NAME@sha256:$digest") done if [ "${#digest_args[@]}" -ne 2 ]; then echo "Both amd64 and arm64 images are required before publishing tags" >&2 exit 1 fi docker buildx imagetools create "${tag_args[@]}" "${digest_args[@]}" docker buildx imagetools inspect "$IMAGE_NAME:${tags[0]}"