name: Docker Release

on:
  push:
    tags:
      - "v*"
    paths-ignore:
      - "docs/**"
      - "**/*.md"
      - "**/*.mdx"
      - ".agents/**"
      - "skills/**"
  workflow_dispatch:
    inputs:
      tag:
        description: Existing release tag to backfill (for example v2026.3.22)
        required: true
        type: string

concurrency:
  group: ${{ github.event_name == 'workflow_dispatch' && format('docker-release-manual-{0}', inputs.tag) || format('docker-release-push-{0}', github.run_id) }}
  cancel-in-progress: false

env:
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  validate_manual_backfill:
    if: github.event_name == 'workflow_dispatch'
    runs-on: ubuntu-24.04
    permissions:
      contents: read
    steps:
      - name: Validate tag input format
        env:
          RELEASE_TAG: ${{ inputs.tag }}
        run: |
          set -euo pipefail
          if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-(alpha|beta)\.[1-9][0-9]*)?$ ]]; then
            echo "Invalid release tag: ${RELEASE_TAG}"
            exit 1
          fi

      - name: Checkout selected tag
        uses: actions/checkout@v6
        with:
          ref: refs/tags/${{ inputs.tag }}
          fetch-depth: 0

  approve_manual_backfill:
    if: github.event_name == 'workflow_dispatch'
    needs: validate_manual_backfill
    # WARNING: KEEP MANUAL BACKFILLS GATED BY THE docker-release ENVIRONMENT.
    runs-on: ubuntu-24.04
    environment: docker-release
    permissions: {}
    steps:
      - name: Approve Docker backfill
        env:
          RELEASE_TAG: ${{ inputs.tag }}
        run: echo "Approved Docker backfill for $RELEASE_TAG"

  # KEEP THIS WORKFLOW ON GITHUB-HOSTED RUNNERS.
  # DO NOT MOVE IT BACK TO BLACKSMITH WITHOUT RE-VALIDATING TAG BUILDS AND BACKFILLS.
  # Build amd64 image. Default and slim tags point to the same slim runtime.
  build-amd64:
    needs: [approve_manual_backfill]
    if: ${{ always() && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
    # WARNING: DO NOT REVERT THIS TO A BLACKSMITH RUNNER WITHOUT RE-VALIDATING TAG BACKFILLS.
    runs-on: ubuntu-24.04
    permissions:
      packages: write
      contents: read
    outputs:
      digest: ${{ steps.build.outputs.digest }}
      browser_digest: ${{ steps.build-browser.outputs.digest }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
          fetch-depth: 0

      - name: Set up Docker Builder
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4

      - name: Login to GitHub Container Registry
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Resolve image tags (amd64)
        id: tags
        shell: bash
        env:
          IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
        run: |
          set -euo pipefail
          tags=()
          slim_tags=()
          browser_tags=()
          browser_supported=0
          if grep -q '^ARG OPENCLAW_INSTALL_BROWSER' Dockerfile; then
            browser_supported=1
          fi
          if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
            version="${SOURCE_REF#refs/tags/v}"
            tags+=("${IMAGE}:${version}-amd64")
            slim_tags+=("${IMAGE}:${version}-slim-amd64")
            if [[ "${browser_supported}" == "1" ]]; then
              browser_tags+=("${IMAGE}:${version}-browser-amd64")
            fi
          fi
          if [[ ${#tags[@]} -eq 0 ]]; then
            echo "::error::No amd64 tags resolved for ref ${SOURCE_REF}"
            exit 1
          fi
          {
            echo "value<<EOF"
            printf "%s\n" "${tags[@]}" "${slim_tags[@]}"
            echo "EOF"
            echo "browser<<EOF"
            printf "%s\n" "${browser_tags[@]}"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"

      - name: Resolve OCI labels (amd64)
        id: labels
        shell: bash
        env:
          SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
        run: |
          set -euo pipefail
          source_sha="$(git rev-parse HEAD)"
          version="${source_sha}"
          if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
            version="main"
          fi
          if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
            version="${SOURCE_REF#refs/tags/v}"
          fi
          created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
          {
            echo "value<<EOF"
            echo "org.opencontainers.image.revision=${source_sha}"
            echo "org.opencontainers.image.version=${version}"
            echo "org.opencontainers.image.created=${created}"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"

      - name: Build and push amd64 image
        id: build
        # WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
        with:
          context: .
          platforms: linux/amd64
          cache-from: type=gha,scope=docker-release-amd64
          cache-to: type=gha,mode=max,scope=docker-release-amd64
          build-args: |
            OPENCLAW_EXTENSIONS=diagnostics-otel,codex
          tags: ${{ steps.tags.outputs.value }}
          labels: ${{ steps.labels.outputs.value }}
          sbom: true
          provenance: mode=max
          push: true

      - name: Build and push amd64 browser image
        id: build-browser
        if: steps.tags.outputs.browser != ''
        # WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
        with:
          context: .
          platforms: linux/amd64
          cache-from: |
            type=gha,scope=docker-release-amd64
            type=gha,scope=docker-release-browser-amd64
          cache-to: type=gha,mode=max,scope=docker-release-browser-amd64
          build-args: |
            OPENCLAW_EXTENSIONS=diagnostics-otel,codex
            OPENCLAW_INSTALL_BROWSER=1
          tags: ${{ steps.tags.outputs.browser }}
          labels: ${{ steps.labels.outputs.value }}
          sbom: true
          provenance: mode=max
          push: true

      - name: Smoke test amd64 runtime workspace templates
        shell: bash
        env:
          IMAGE_REFS: ${{ steps.tags.outputs.value }}
        run: |
          set -euo pipefail
          mapfile -t image_refs <<< "${IMAGE_REFS}"
          image_ref="${image_refs[0]}"
          if [[ -z "${image_ref}" ]]; then
            echo "::error::No amd64 image ref resolved for runtime template smoke"
            exit 1
          fi
          docker run --rm --entrypoint /bin/sh "${image_ref}" -lc '
            set -eu
            test -f /app/src/agents/templates/HEARTBEAT.md
            temp_root="$(mktemp -d)"
            trap "rm -rf \"${temp_root}\"" EXIT
            mkdir -p "${temp_root}/home" "${temp_root}/cwd"
            cd "${temp_root}/cwd"
            set +e
            HOME="${temp_root}/home" \
            USERPROFILE="${temp_root}/home" \
            OPENCLAW_HOME="${temp_root}/home" \
            OPENCLAW_NO_ONBOARD=1 \
            OPENCLAW_SUPPRESS_NOTES=1 \
            OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 \
            OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK=1 \
            AWS_EC2_METADATA_DISABLED=true \
            AWS_SHARED_CREDENTIALS_FILE="${temp_root}/home/.aws/credentials" \
            AWS_CONFIG_FILE="${temp_root}/home/.aws/config" \
              node /app/openclaw.mjs agent --message "workspace bootstrap smoke" --session-id "workspace-bootstrap-smoke" --local --timeout 1 --json \
              >"${temp_root}/out.log" 2>&1
            status="$?"
            set -e
            if grep -F "Missing workspace template:" "${temp_root}/out.log"; then
              cat "${temp_root}/out.log"
              exit 1
            fi
            test -f "${temp_root}/home/.openclaw/workspace/HEARTBEAT.md"
            if [ "${status}" -ne 0 ]; then
              cat "${temp_root}/out.log"
            fi
          '

      - name: Smoke test amd64 browser image
        if: steps.tags.outputs.browser != ''
        shell: bash
        env:
          IMAGE_REFS: ${{ steps.tags.outputs.browser }}
        run: |
          set -euo pipefail
          mapfile -t image_refs <<< "${IMAGE_REFS}"
          image_ref="${image_refs[0]}"
          if [[ -z "${image_ref}" ]]; then
            echo "::error::No amd64 browser image ref resolved"
            exit 1
          fi
          docker run --rm --entrypoint /bin/sh "${image_ref}" -lc '
            set -eu
            browser="$(find /home/node/.cache/ms-playwright -maxdepth 5 -type f \( -name chrome -o -name chromium -o -name chrome-headless-shell \) -print | head -1)"
            test -n "${browser}"
            "${browser}" --version
          '

  # Build arm64 image. Default and slim tags point to the same slim runtime.
  build-arm64:
    needs: [approve_manual_backfill]
    if: ${{ always() && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
    # WARNING: DO NOT REVERT THIS TO A BLACKSMITH RUNNER WITHOUT RE-VALIDATING TAG BACKFILLS.
    runs-on: ubuntu-24.04-arm
    permissions:
      packages: write
      contents: read
    outputs:
      digest: ${{ steps.build.outputs.digest }}
      browser_digest: ${{ steps.build-browser.outputs.digest }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
          fetch-depth: 0

      - name: Set up Docker Builder
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4

      - name: Login to GitHub Container Registry
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Resolve image tags (arm64)
        id: tags
        shell: bash
        env:
          IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
        run: |
          set -euo pipefail
          tags=()
          slim_tags=()
          browser_tags=()
          browser_supported=0
          if grep -q '^ARG OPENCLAW_INSTALL_BROWSER' Dockerfile; then
            browser_supported=1
          fi
          if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
            version="${SOURCE_REF#refs/tags/v}"
            tags+=("${IMAGE}:${version}-arm64")
            slim_tags+=("${IMAGE}:${version}-slim-arm64")
            if [[ "${browser_supported}" == "1" ]]; then
              browser_tags+=("${IMAGE}:${version}-browser-arm64")
            fi
          fi
          if [[ ${#tags[@]} -eq 0 ]]; then
            echo "::error::No arm64 tags resolved for ref ${SOURCE_REF}"
            exit 1
          fi
          {
            echo "value<<EOF"
            printf "%s\n" "${tags[@]}" "${slim_tags[@]}"
            echo "EOF"
            echo "browser<<EOF"
            printf "%s\n" "${browser_tags[@]}"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"

      - name: Resolve OCI labels (arm64)
        id: labels
        shell: bash
        env:
          SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
        run: |
          set -euo pipefail
          source_sha="$(git rev-parse HEAD)"
          version="${source_sha}"
          if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
            version="main"
          fi
          if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
            version="${SOURCE_REF#refs/tags/v}"
          fi
          created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
          {
            echo "value<<EOF"
            echo "org.opencontainers.image.revision=${source_sha}"
            echo "org.opencontainers.image.version=${version}"
            echo "org.opencontainers.image.created=${created}"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"

      - name: Build and push arm64 image
        id: build
        # WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
        with:
          context: .
          platforms: linux/arm64
          cache-from: type=gha,scope=docker-release-arm64
          cache-to: type=gha,mode=max,scope=docker-release-arm64
          build-args: |
            OPENCLAW_EXTENSIONS=diagnostics-otel,codex
          tags: ${{ steps.tags.outputs.value }}
          labels: ${{ steps.labels.outputs.value }}
          sbom: true
          provenance: mode=max
          push: true

      - name: Build and push arm64 browser image
        id: build-browser
        if: steps.tags.outputs.browser != ''
        # WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
        uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
        with:
          context: .
          platforms: linux/arm64
          cache-from: |
            type=gha,scope=docker-release-arm64
            type=gha,scope=docker-release-browser-arm64
          cache-to: type=gha,mode=max,scope=docker-release-browser-arm64
          build-args: |
            OPENCLAW_EXTENSIONS=diagnostics-otel,codex
            OPENCLAW_INSTALL_BROWSER=1
          tags: ${{ steps.tags.outputs.browser }}
          labels: ${{ steps.labels.outputs.value }}
          sbom: true
          provenance: mode=max
          push: true

      - name: Smoke test arm64 runtime workspace templates
        shell: bash
        env:
          IMAGE_REFS: ${{ steps.tags.outputs.value }}
        run: |
          set -euo pipefail
          mapfile -t image_refs <<< "${IMAGE_REFS}"
          image_ref="${image_refs[0]}"
          if [[ -z "${image_ref}" ]]; then
            echo "::error::No arm64 image ref resolved for runtime template smoke"
            exit 1
          fi
          docker run --rm --entrypoint /bin/sh "${image_ref}" -lc '
            set -eu
            test -f /app/src/agents/templates/HEARTBEAT.md
            temp_root="$(mktemp -d)"
            trap "rm -rf \"${temp_root}\"" EXIT
            mkdir -p "${temp_root}/home" "${temp_root}/cwd"
            cd "${temp_root}/cwd"
            set +e
            HOME="${temp_root}/home" \
            USERPROFILE="${temp_root}/home" \
            OPENCLAW_HOME="${temp_root}/home" \
            OPENCLAW_NO_ONBOARD=1 \
            OPENCLAW_SUPPRESS_NOTES=1 \
            OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 \
            OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK=1 \
            AWS_EC2_METADATA_DISABLED=true \
            AWS_SHARED_CREDENTIALS_FILE="${temp_root}/home/.aws/credentials" \
            AWS_CONFIG_FILE="${temp_root}/home/.aws/config" \
              node /app/openclaw.mjs agent --message "workspace bootstrap smoke" --session-id "workspace-bootstrap-smoke" --local --timeout 1 --json \
              >"${temp_root}/out.log" 2>&1
            status="$?"
            set -e
            if grep -F "Missing workspace template:" "${temp_root}/out.log"; then
              cat "${temp_root}/out.log"
              exit 1
            fi
            test -f "${temp_root}/home/.openclaw/workspace/HEARTBEAT.md"
            if [ "${status}" -ne 0 ]; then
              cat "${temp_root}/out.log"
            fi
          '

      - name: Smoke test arm64 browser image
        if: steps.tags.outputs.browser != ''
        shell: bash
        env:
          IMAGE_REFS: ${{ steps.tags.outputs.browser }}
        run: |
          set -euo pipefail
          mapfile -t image_refs <<< "${IMAGE_REFS}"
          image_ref="${image_refs[0]}"
          if [[ -z "${image_ref}" ]]; then
            echo "::error::No arm64 browser image ref resolved"
            exit 1
          fi
          docker run --rm --entrypoint /bin/sh "${image_ref}" -lc '
            set -eu
            browser="$(find /home/node/.cache/ms-playwright -maxdepth 5 -type f \( -name chrome -o -name chromium -o -name chrome-headless-shell \) -print | head -1)"
            test -n "${browser}"
            "${browser}" --version
          '

  # Create multi-platform manifests
  create-manifest:
    needs: [approve_manual_backfill, build-amd64, build-arm64]
    if: ${{ always() && needs.build-amd64.result == 'success' && needs.build-arm64.result == 'success' && (github.event_name != 'workflow_dispatch' || needs.approve_manual_backfill.result == 'success') }}
    # WARNING: DO NOT REVERT THIS TO A BLACKSMITH RUNNER WITHOUT RE-VALIDATING TAG BACKFILLS.
    runs-on: ubuntu-24.04
    permissions:
      packages: write
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
          fetch-depth: 0

      - name: Login to GitHub Container Registry
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Resolve manifest tags
        id: tags
        shell: bash
        env:
          IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
          IS_MANUAL_BACKFILL: ${{ github.event_name == 'workflow_dispatch' && '1' || '0' }}
        run: |
          set -euo pipefail
          tags=()
          slim_tags=()
          browser_tags=()
          browser_supported=0
          if grep -q '^ARG OPENCLAW_INSTALL_BROWSER' Dockerfile; then
            browser_supported=1
          fi
          if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
            version="${SOURCE_REF#refs/tags/v}"
            tags+=("${IMAGE}:${version}")
            slim_tags+=("${IMAGE}:${version}-slim")
            if [[ "${browser_supported}" == "1" ]]; then
              browser_tags+=("${IMAGE}:${version}-browser")
            fi
            # Manual backfills should only republish the requested version tags.
            if [[ "${IS_MANUAL_BACKFILL}" != "1" && "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then
              tags+=("${IMAGE}:latest" "${IMAGE}:main")
              slim_tags+=("${IMAGE}:slim" "${IMAGE}:main-slim")
              if [[ "${browser_supported}" == "1" ]]; then
                browser_tags+=("${IMAGE}:latest-browser" "${IMAGE}:main-browser")
              fi
            fi
          fi
          if [[ ${#tags[@]} -eq 0 ]]; then
            echo "::error::No manifest tags resolved for ref ${SOURCE_REF}"
            exit 1
          fi
          {
            echo "value<<EOF"
            printf "%s\n" "${tags[@]}" "${slim_tags[@]}"
            echo "EOF"
            echo "browser<<EOF"
            printf "%s\n" "${browser_tags[@]}"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"

      - name: Create and push manifest
        shell: bash
        env:
          TAGS: ${{ steps.tags.outputs.value }}
          BROWSER_TAGS: ${{ steps.tags.outputs.browser }}
          AMD64_DIGEST: ${{ needs.build-amd64.outputs.digest }}
          ARM64_DIGEST: ${{ needs.build-arm64.outputs.digest }}
          AMD64_BROWSER_DIGEST: ${{ needs.build-amd64.outputs.browser_digest }}
          ARM64_BROWSER_DIGEST: ${{ needs.build-arm64.outputs.browser_digest }}
        run: |
          set -euo pipefail
          mapfile -t tags <<< "${TAGS}"
          mapfile -t browser_tags <<< "${BROWSER_TAGS}"
          create_manifest() {
            local amd64_digest="$1"
            local arm64_digest="$2"
            shift 2
            local args=()
            for tag in "$@"; do
              [ -z "$tag" ] && continue
              args+=("-t" "$tag")
            done
            docker buildx imagetools create "${args[@]}" "$amd64_digest" "$arm64_digest"
          }
          create_manifest "${AMD64_DIGEST}" "${ARM64_DIGEST}" "${tags[@]}"
          if [[ -n "${BROWSER_TAGS}" ]]; then
            create_manifest "${AMD64_BROWSER_DIGEST}" "${ARM64_BROWSER_DIGEST}" "${browser_tags[@]}"
          fi

  verify-attestations:
    needs: [create-manifest]
    if: ${{ always() && needs.create-manifest.result == 'success' }}
    runs-on: ubuntu-24.04
    permissions:
      contents: read
      packages: read
    steps:
      - name: Checkout
        uses: actions/checkout@v6
        with:
          fetch-depth: 1

      - name: Set up Docker Builder
        uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4

      - name: Login to GitHub Container Registry
        uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Resolve image refs
        id: refs
        shell: bash
        env:
          IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          SOURCE_REF: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/{0}', inputs.tag) || github.ref }}
          IS_MANUAL_BACKFILL: ${{ github.event_name == 'workflow_dispatch' && '1' || '0' }}
        run: |
          set -euo pipefail
          multi_refs=()
          slim_multi_refs=()
          amd64_refs=()
          arm64_refs=()
          browser_supported=0
          if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
            tag="${SOURCE_REF#refs/tags/}"
            git fetch --depth=1 origin "refs/tags/${tag}:refs/tags/${tag}"
            if git show "${SOURCE_REF}:Dockerfile" | grep -q '^ARG OPENCLAW_INSTALL_BROWSER'; then
              browser_supported=1
            fi
          elif grep -q '^ARG OPENCLAW_INSTALL_BROWSER' Dockerfile; then
            browser_supported=1
          fi
          if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
            version="${SOURCE_REF#refs/tags/v}"
            multi_refs+=("${IMAGE}:${version}")
            slim_multi_refs+=("${IMAGE}:${version}-slim")
            amd64_refs+=(
              "${IMAGE}:${version}-amd64"
              "${IMAGE}:${version}-slim-amd64"
            )
            arm64_refs+=(
              "${IMAGE}:${version}-arm64"
              "${IMAGE}:${version}-slim-arm64"
            )
            if [[ "${browser_supported}" == "1" ]]; then
              multi_refs+=("${IMAGE}:${version}-browser")
              amd64_refs+=("${IMAGE}:${version}-browser-amd64")
              arm64_refs+=("${IMAGE}:${version}-browser-arm64")
            fi
            if [[ "${IS_MANUAL_BACKFILL}" != "1" && "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then
              multi_refs+=("${IMAGE}:latest" "${IMAGE}:main")
              slim_multi_refs+=("${IMAGE}:slim" "${IMAGE}:main-slim")
              if [[ "${browser_supported}" == "1" ]]; then
                multi_refs+=("${IMAGE}:latest-browser" "${IMAGE}:main-browser")
              fi
            fi
          fi
          if [[ ${#multi_refs[@]} -eq 0 || ${#amd64_refs[@]} -eq 0 || ${#arm64_refs[@]} -eq 0 ]]; then
            echo "::error::No Docker image refs resolved for ref ${SOURCE_REF}"
            exit 1
          fi
          {
            echo "multi<<EOF"
            printf "%s\n" "${multi_refs[@]}" "${slim_multi_refs[@]}"
            echo "EOF"
            echo "amd64<<EOF"
            printf "%s\n" "${amd64_refs[@]}"
            echo "EOF"
            echo "arm64<<EOF"
            printf "%s\n" "${arm64_refs[@]}"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"

      - name: Verify Docker attestations
        shell: bash
        env:
          MULTI_REFS: ${{ steps.refs.outputs.multi }}
          AMD64_REFS: ${{ steps.refs.outputs.amd64 }}
          ARM64_REFS: ${{ steps.refs.outputs.arm64 }}
        run: |
          set -euo pipefail
          mapfile -t multi_refs <<< "${MULTI_REFS}"
          mapfile -t amd64_refs <<< "${AMD64_REFS}"
          mapfile -t arm64_refs <<< "${ARM64_REFS}"

          node scripts/verify-docker-attestations.mjs \
            --platform linux/amd64 \
            --platform linux/arm64 \
            "${multi_refs[@]}"
          node scripts/verify-docker-attestations.mjs \
            --platform linux/amd64 \
            "${amd64_refs[@]}"
          node scripts/verify-docker-attestations.mjs \
            --platform linux/arm64 \
            "${arm64_refs[@]}"
