name: Docker Release

on:
  push:
    branches:
      - main
    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: docker-release-${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && inputs.tag || github.ref }}
  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]*(-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
    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 images (default + slim share the build stage cache)
  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 }}
      slim-digest: ${{ steps.build-slim.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@v4

      - name: Login to GitHub Container Registry
        uses: docker/login-action@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=()
          if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
            tags+=("${IMAGE}:main-amd64")
            slim_tags+=("${IMAGE}:main-slim-amd64")
          fi
          if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
            version="${SOURCE_REF#refs/tags/v}"
            tags+=("${IMAGE}:${version}-amd64")
            slim_tags+=("${IMAGE}:${version}-slim-amd64")
          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[@]}"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"
          {
            echo "slim<<EOF"
            printf "%s\n" "${slim_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@v6
        with:
          context: .
          platforms: linux/amd64
          cache-from: type=gha,scope=docker-release-amd64
          cache-to: type=gha,mode=max,scope=docker-release-amd64
          tags: ${{ steps.tags.outputs.value }}
          labels: ${{ steps.labels.outputs.value }}
          provenance: false
          push: true

      - name: Build and push amd64 slim image
        id: build-slim
        # WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
        uses: docker/build-push-action@v6
        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_VARIANT=slim
          tags: ${{ steps.tags.outputs.slim }}
          labels: ${{ steps.labels.outputs.value }}
          provenance: false
          push: true

  # Build arm64 images (default + slim share the build stage cache)
  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 }}
      slim-digest: ${{ steps.build-slim.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@v4

      - name: Login to GitHub Container Registry
        uses: docker/login-action@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=()
          if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
            tags+=("${IMAGE}:main-arm64")
            slim_tags+=("${IMAGE}:main-slim-arm64")
          fi
          if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
            version="${SOURCE_REF#refs/tags/v}"
            tags+=("${IMAGE}:${version}-arm64")
            slim_tags+=("${IMAGE}:${version}-slim-arm64")
          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[@]}"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"
          {
            echo "slim<<EOF"
            printf "%s\n" "${slim_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@v6
        with:
          context: .
          platforms: linux/arm64
          cache-from: type=gha,scope=docker-release-arm64
          cache-to: type=gha,mode=max,scope=docker-release-arm64
          tags: ${{ steps.tags.outputs.value }}
          labels: ${{ steps.labels.outputs.value }}
          provenance: false
          push: true

      - name: Build and push arm64 slim image
        id: build-slim
        # WARNING: KEEP THE OFFICIAL DOCKER ACTION HERE; DO NOT SWITCH THIS BACK TO BLACKSMITH BLINDLY.
        uses: docker/build-push-action@v6
        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_VARIANT=slim
          tags: ${{ steps.tags.outputs.slim }}
          labels: ${{ steps.labels.outputs.value }}
          provenance: false
          push: true

  # 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@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=()
          if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then
            tags+=("${IMAGE}:main")
            slim_tags+=("${IMAGE}:main-slim")
          fi
          if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
            version="${SOURCE_REF#refs/tags/v}"
            tags+=("${IMAGE}:${version}")
            slim_tags+=("${IMAGE}:${version}-slim")
            # 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")
              slim_tags+=("${IMAGE}:slim")
            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[@]}"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"
          {
            echo "slim<<EOF"
            printf "%s\n" "${slim_tags[@]}"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"

      - name: Create and push default manifest
        shell: bash
        run: |
          set -euo pipefail
          mapfile -t tags <<< "${{ steps.tags.outputs.value }}"
          args=()
          for tag in "${tags[@]}"; do
            [ -z "$tag" ] && continue
            args+=("-t" "$tag")
          done
          docker buildx imagetools create "${args[@]}" \
            ${{ needs.build-amd64.outputs.digest }} \
            ${{ needs.build-arm64.outputs.digest }}

      - name: Create and push slim manifest
        shell: bash
        run: |
          set -euo pipefail
          mapfile -t tags <<< "${{ steps.tags.outputs.slim }}"
          args=()
          for tag in "${tags[@]}"; do
            [ -z "$tag" ] && continue
            args+=("-t" "$tag")
          done
          docker buildx imagetools create "${args[@]}" \
            ${{ needs.build-amd64.outputs.slim-digest }} \
            ${{ needs.build-arm64.outputs.slim-digest }}
