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]*(-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 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 }} 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=() 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" - 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@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 tags: ${{ steps.tags.outputs.value }} labels: ${{ steps.labels.outputs.value }} sbom: true provenance: mode=max push: true # 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 }} 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=() 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" - 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@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 tags: ${{ steps.tags.outputs.value }} labels: ${{ steps.labels.outputs.value }} sbom: true provenance: mode=max 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@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=() 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" - name: Create and push manifest shell: bash env: TAGS: ${{ steps.tags.outputs.value }} AMD64_DIGEST: ${{ needs.build-amd64.outputs.digest }} ARM64_DIGEST: ${{ needs.build-arm64.outputs.digest }} run: | set -euo pipefail mapfile -t tags <<< "${TAGS}" args=() for tag in "${tags[@]}"; do [ -z "$tag" ] && continue args+=("-t" "$tag") done docker buildx imagetools create "${args[@]}" \ "${AMD64_DIGEST}" \ "${ARM64_DIGEST}" 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=() if [[ "${SOURCE_REF}" == "refs/heads/main" ]]; then multi_refs+=("${IMAGE}:main") slim_multi_refs+=("${IMAGE}:main-slim") amd64_refs+=("${IMAGE}:main-amd64" "${IMAGE}:main-slim-amd64") arm64_refs+=("${IMAGE}:main-arm64" "${IMAGE}:main-slim-arm64") 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 [[ "${IS_MANUAL_BACKFILL}" != "1" && "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?$ ]]; then multi_refs+=("${IMAGE}:latest") slim_multi_refs+=("${IMAGE}:slim") 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<> "$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[@]}"