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.13) 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<> "$GITHUB_OUTPUT" { echo "slim<> "$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<> "$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 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 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<> "$GITHUB_OUTPUT" { echo "slim<> "$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<> "$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 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 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<> "$GITHUB_OUTPUT" { echo "slim<> "$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 }}