mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 09:01:33 +00:00
* fix(release): validate candidates before Docker publication * fix(release): complete Docker publication gates * fix(release): serialize Docker publication
915 lines
36 KiB
YAML
915 lines
36 KiB
YAML
name: Docker Release
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
tag:
|
|
description: Immutable stable, extended-stable, or beta release tag
|
|
required: true
|
|
type: string
|
|
release_sha:
|
|
description: Full immutable commit SHA resolved from tag
|
|
required: true
|
|
type: string
|
|
secrets:
|
|
DOCKERHUB_USERNAME:
|
|
required: true
|
|
DOCKERHUB_TOKEN:
|
|
required: true
|
|
|
|
concurrency:
|
|
# Alias promotion checks are read-then-write; serialize every Docker publish
|
|
# so an older stable run cannot overwrite a newer latest/main promotion.
|
|
group: docker-release-publish
|
|
cancel-in-progress: false
|
|
queue: max
|
|
|
|
env:
|
|
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
|
|
REGISTRY: ghcr.io
|
|
IMAGE_NAME: ${{ github.repository }}
|
|
DOCKERHUB_REGISTRY: docker.io
|
|
DOCKERHUB_IMAGE_NAME: openclaw/openclaw
|
|
|
|
jobs:
|
|
validate_release_identity:
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- name: Validate immutable tag and SHA inputs
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
RELEASE_SHA: ${{ inputs.release_sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ "${RELEASE_TAG}" == *"-alpha."* ]]; then
|
|
echo "Docker alpha image publishing is disabled."
|
|
exit 1
|
|
fi
|
|
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
|
|
if [[ ! "${RELEASE_SHA}" =~ ^[a-f0-9]{40}$ ]]; then
|
|
echo "Release SHA must be a full lowercase commit SHA."
|
|
exit 1
|
|
fi
|
|
|
|
- name: Checkout immutable release tag
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: refs/tags/${{ inputs.tag }}
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: Verify tag, SHA, and package identity agree
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
RELEASE_SHA: ${{ inputs.release_sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
tag_sha="$(git rev-parse "refs/tags/${RELEASE_TAG}^{commit}")"
|
|
checkout_sha="$(git rev-parse HEAD)"
|
|
package_version="$(node -p "require('./package.json').version")"
|
|
if [[ "${tag_sha}" != "${RELEASE_SHA}" || "${checkout_sha}" != "${RELEASE_SHA}" ]]; then
|
|
echo "Release tag ${RELEASE_TAG} must resolve to supplied SHA ${RELEASE_SHA}; got ${tag_sha}." >&2
|
|
exit 1
|
|
fi
|
|
if [[ "v${package_version}" != "${RELEASE_TAG}" && ! "${RELEASE_TAG}" =~ ^v${package_version}-[1-9][0-9]*$ ]]; then
|
|
echo "Release tag ${RELEASE_TAG} does not match package.json version ${package_version} or its correction-tag form." >&2
|
|
exit 1
|
|
fi
|
|
|
|
resolve_release_policy:
|
|
needs: validate_release_identity
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
version: ${{ steps.policy.outputs.version }}
|
|
channel: ${{ steps.policy.outputs.channel }}
|
|
steps:
|
|
- name: Checkout trusted workflow helpers
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
path: workflow-source
|
|
persist-credentials: false
|
|
|
|
- name: Resolve release channel policy
|
|
id: policy
|
|
shell: bash
|
|
env:
|
|
SOURCE_REF: ${{ format('refs/tags/{0}', inputs.tag) }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ "${SOURCE_REF}" != refs/tags/v* ]]; then
|
|
echo "::error::Docker releases require a v-prefixed release tag; got ${SOURCE_REF}."
|
|
exit 1
|
|
fi
|
|
version="${SOURCE_REF#refs/tags/v}"
|
|
policy="$(node workflow-source/scripts/lib/docker-release-policy.mjs "${version}")"
|
|
channel="$(jq -r '.channel' <<< "${policy}")"
|
|
{
|
|
echo "version=${version}"
|
|
echo "channel=${channel}"
|
|
} >> "$GITHUB_OUTPUT"
|
|
{
|
|
echo "## Docker release policy"
|
|
echo "- Version: ${version}"
|
|
echo "- Channel: ${channel}"
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
approve_docker_publish:
|
|
name: Approve Docker publication ${{ inputs.tag }}
|
|
needs: [validate_release_identity, resolve_release_policy]
|
|
# Docker publication remains protected even though only release orchestration can call it.
|
|
runs-on: ubuntu-24.04
|
|
environment: docker-release
|
|
permissions: {}
|
|
steps:
|
|
- name: Approve Docker publication
|
|
env:
|
|
RELEASE_TAG: ${{ inputs.tag }}
|
|
run: |
|
|
echo "Approved immutable Docker image publication for ${RELEASE_TAG}"
|
|
|
|
validate_publish_config:
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- name: Validate Docker Hub publish credentials
|
|
env:
|
|
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ -z "${DOCKERHUB_USERNAME}" || -z "${DOCKERHUB_TOKEN}" ]]; then
|
|
echo "::error::Docker Hub publishing requires DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets."
|
|
exit 1
|
|
fi
|
|
echo "Docker Hub publishing configured for ${DOCKERHUB_IMAGE}."
|
|
|
|
resolve_build_provenance:
|
|
needs: [approve_docker_publish, resolve_release_policy, validate_publish_config]
|
|
if: ${{ always() && needs.approve_docker_publish.result == 'success' && needs.resolve_release_policy.result == 'success' && needs.validate_publish_config.result == 'success' }}
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
built_at: ${{ steps.build_provenance.outputs.built_at }}
|
|
source_sha: ${{ steps.build_provenance.outputs.source_sha }}
|
|
steps:
|
|
- name: Checkout selected source
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ inputs.release_sha }}
|
|
fetch-depth: 0
|
|
|
|
- name: Resolve shared build provenance
|
|
id: build_provenance
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
echo "source_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
|
echo "built_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
|
|
|
# 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_docker_publish, validate_publish_config, resolve_build_provenance]
|
|
if: ${{ always() && needs.approve_docker_publish.result == 'success' && needs.validate_publish_config.result == 'success' && needs.resolve_build_provenance.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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ needs.resolve_build_provenance.outputs.source_sha }}
|
|
fetch-depth: 0
|
|
|
|
- &buildkit_prepull_step
|
|
name: Pre-pull BuildKit image
|
|
shell: bash
|
|
env:
|
|
BUILDKIT_IMAGE: moby/buildkit:buildx-stable-1
|
|
run: |
|
|
set -euo pipefail
|
|
for attempt in 1 2 3 4; do
|
|
if docker pull "${BUILDKIT_IMAGE}"; then
|
|
exit 0
|
|
fi
|
|
if [[ "${attempt}" == "4" ]]; then
|
|
echo "::error::Failed to pull ${BUILDKIT_IMAGE} after ${attempt} attempts"
|
|
exit 1
|
|
fi
|
|
sleep_seconds=$((attempt * 10))
|
|
echo "BuildKit image pull failed; retrying in ${sleep_seconds}s (${attempt}/4)."
|
|
sleep "${sleep_seconds}"
|
|
done
|
|
|
|
- name: Set up Docker Builder
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
|
|
|
- name: Login to GitHub Container Registry
|
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.repository_owner }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Login to Docker Hub
|
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
with:
|
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
|
|
- name: Resolve image tags (amd64)
|
|
id: tags
|
|
shell: bash
|
|
env:
|
|
GHCR_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}
|
|
SOURCE_REF: ${{ format('refs/tags/{0}', inputs.tag) }}
|
|
run: |
|
|
set -euo pipefail
|
|
tags=()
|
|
slim_tags=()
|
|
browser_tags=()
|
|
images=("${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}")
|
|
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}"
|
|
for image in "${images[@]}"; do
|
|
tags+=("${image}:${version}-amd64")
|
|
slim_tags+=("${image}:${version}-slim-amd64")
|
|
if [[ "${browser_supported}" == "1" ]]; then
|
|
browser_tags+=("${image}:${version}-browser-amd64")
|
|
fi
|
|
done
|
|
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:
|
|
BUILD_TIMESTAMP: ${{ needs.resolve_build_provenance.outputs.built_at }}
|
|
SOURCE_SHA: ${{ needs.resolve_build_provenance.outputs.source_sha }}
|
|
SOURCE_REF: ${{ format('refs/tags/{0}', inputs.tag) }}
|
|
run: |
|
|
set -euo pipefail
|
|
source_sha="${SOURCE_SHA}"
|
|
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="${BUILD_TIMESTAMP}"
|
|
{
|
|
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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.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: |
|
|
GIT_COMMIT=${{ needs.resolve_build_provenance.outputs.source_sha }}
|
|
OPENCLAW_BUILD_TIMESTAMP=${{ needs.resolve_build_provenance.outputs.built_at }}
|
|
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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.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: |
|
|
GIT_COMMIT=${{ needs.resolve_build_provenance.outputs.source_sha }}
|
|
OPENCLAW_BUILD_TIMESTAMP=${{ needs.resolve_build_provenance.outputs.built_at }}
|
|
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
|
|
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_docker_publish, validate_publish_config, resolve_build_provenance]
|
|
if: ${{ always() && needs.approve_docker_publish.result == 'success' && needs.validate_publish_config.result == 'success' && needs.resolve_build_provenance.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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ needs.resolve_build_provenance.outputs.source_sha }}
|
|
fetch-depth: 0
|
|
|
|
- *buildkit_prepull_step
|
|
- name: Set up Docker Builder
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
|
|
|
- name: Login to GitHub Container Registry
|
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.repository_owner }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Login to Docker Hub
|
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
with:
|
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
|
|
- name: Resolve image tags (arm64)
|
|
id: tags
|
|
shell: bash
|
|
env:
|
|
GHCR_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}
|
|
SOURCE_REF: ${{ format('refs/tags/{0}', inputs.tag) }}
|
|
run: |
|
|
set -euo pipefail
|
|
tags=()
|
|
slim_tags=()
|
|
browser_tags=()
|
|
images=("${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}")
|
|
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}"
|
|
for image in "${images[@]}"; do
|
|
tags+=("${image}:${version}-arm64")
|
|
slim_tags+=("${image}:${version}-slim-arm64")
|
|
if [[ "${browser_supported}" == "1" ]]; then
|
|
browser_tags+=("${image}:${version}-browser-arm64")
|
|
fi
|
|
done
|
|
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:
|
|
BUILD_TIMESTAMP: ${{ needs.resolve_build_provenance.outputs.built_at }}
|
|
SOURCE_SHA: ${{ needs.resolve_build_provenance.outputs.source_sha }}
|
|
SOURCE_REF: ${{ format('refs/tags/{0}', inputs.tag) }}
|
|
run: |
|
|
set -euo pipefail
|
|
source_sha="${SOURCE_SHA}"
|
|
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="${BUILD_TIMESTAMP}"
|
|
{
|
|
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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.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: |
|
|
GIT_COMMIT=${{ needs.resolve_build_provenance.outputs.source_sha }}
|
|
OPENCLAW_BUILD_TIMESTAMP=${{ needs.resolve_build_provenance.outputs.built_at }}
|
|
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@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.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: |
|
|
GIT_COMMIT=${{ needs.resolve_build_provenance.outputs.source_sha }}
|
|
OPENCLAW_BUILD_TIMESTAMP=${{ needs.resolve_build_provenance.outputs.built_at }}
|
|
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
|
|
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_docker_publish,
|
|
resolve_release_policy,
|
|
validate_publish_config,
|
|
resolve_build_provenance,
|
|
build-amd64,
|
|
build-arm64,
|
|
]
|
|
if: ${{ always() && needs.approve_docker_publish.result == 'success' && needs.validate_publish_config.result == 'success' && needs.build-amd64.result == 'success' && needs.build-arm64.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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ needs.resolve_build_provenance.outputs.source_sha }}
|
|
fetch-depth: 0
|
|
|
|
- name: Login to GitHub Container Registry
|
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.repository_owner }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Login to Docker Hub
|
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
with:
|
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
|
|
- name: Resolve manifest tags
|
|
id: tags
|
|
shell: bash
|
|
env:
|
|
GHCR_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}
|
|
SOURCE_REF: ${{ format('refs/tags/{0}', inputs.tag) }}
|
|
run: |
|
|
set -euo pipefail
|
|
tags=()
|
|
slim_tags=()
|
|
browser_tags=()
|
|
dockerhub_tags=()
|
|
dockerhub_slim_tags=()
|
|
dockerhub_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+=("${GHCR_IMAGE}:${version}")
|
|
slim_tags+=("${GHCR_IMAGE}:${version}-slim")
|
|
dockerhub_tags+=("${DOCKERHUB_IMAGE}:${version}")
|
|
dockerhub_slim_tags+=("${DOCKERHUB_IMAGE}:${version}-slim")
|
|
if [[ "${browser_supported}" == "1" ]]; then
|
|
browser_tags+=("${GHCR_IMAGE}:${version}-browser")
|
|
dockerhub_browser_tags+=("${DOCKERHUB_IMAGE}:${version}-browser")
|
|
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"
|
|
echo "dockerhub<<EOF"
|
|
printf "%s\n" "${dockerhub_tags[@]}" "${dockerhub_slim_tags[@]}"
|
|
echo "EOF"
|
|
echo "dockerhub_browser<<EOF"
|
|
printf "%s\n" "${dockerhub_browser_tags[@]}"
|
|
echo "EOF"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Create and push manifest
|
|
shell: bash
|
|
env:
|
|
DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}
|
|
SOURCE_REF: ${{ format('refs/tags/{0}', inputs.tag) }}
|
|
TAGS: ${{ steps.tags.outputs.value }}
|
|
BROWSER_TAGS: ${{ steps.tags.outputs.browser }}
|
|
DOCKERHUB_TAGS: ${{ steps.tags.outputs.dockerhub }}
|
|
DOCKERHUB_BROWSER_TAGS: ${{ steps.tags.outputs.dockerhub_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}"
|
|
mapfile -t dockerhub_tags <<< "${DOCKERHUB_TAGS}"
|
|
mapfile -t dockerhub_browser_tags <<< "${DOCKERHUB_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 [[ "${SOURCE_REF}" != refs/tags/v* ]]; then
|
|
echo "::error::No Docker Hub source refs resolved for ref ${SOURCE_REF}"
|
|
exit 1
|
|
fi
|
|
version="${SOURCE_REF#refs/tags/v}"
|
|
create_manifest "${DOCKERHUB_IMAGE}:${version}-amd64" "${DOCKERHUB_IMAGE}:${version}-arm64" "${dockerhub_tags[@]}"
|
|
if [[ -n "${BROWSER_TAGS}" ]]; then
|
|
create_manifest "${AMD64_BROWSER_DIGEST}" "${ARM64_BROWSER_DIGEST}" "${browser_tags[@]}"
|
|
fi
|
|
if [[ -n "${DOCKERHUB_BROWSER_TAGS}" ]]; then
|
|
create_manifest \
|
|
"${DOCKERHUB_IMAGE}:${version}-browser-amd64" \
|
|
"${DOCKERHUB_IMAGE}:${version}-browser-arm64" \
|
|
"${dockerhub_browser_tags[@]}"
|
|
fi
|
|
|
|
verify-attestations:
|
|
name: Verify attestations and promote channel
|
|
needs: [resolve_release_policy, resolve_build_provenance, create-manifest]
|
|
if: ${{ always() && needs.create-manifest.result == 'success' }}
|
|
runs-on: ubuntu-24.04
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
|
with:
|
|
ref: ${{ needs.resolve_build_provenance.outputs.source_sha }}
|
|
fetch-depth: 1
|
|
|
|
- *buildkit_prepull_step
|
|
- name: Set up Docker Builder
|
|
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
|
|
|
- name: Login to GitHub Container Registry
|
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.repository_owner }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Login to Docker Hub
|
|
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
|
with:
|
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
|
|
- name: Resolve image refs
|
|
id: refs
|
|
shell: bash
|
|
env:
|
|
GHCR_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}
|
|
SOURCE_REF: ${{ format('refs/tags/{0}', inputs.tag) }}
|
|
run: |
|
|
set -euo pipefail
|
|
multi_refs=()
|
|
slim_multi_refs=()
|
|
amd64_refs=()
|
|
arm64_refs=()
|
|
dockerhub_multi_refs=()
|
|
dockerhub_slim_multi_refs=()
|
|
dockerhub_amd64_refs=()
|
|
dockerhub_arm64_refs=()
|
|
browser_supported=0
|
|
if [[ "${SOURCE_REF}" == refs/tags/v* ]]; then
|
|
if grep -q '^ARG OPENCLAW_INSTALL_BROWSER' Dockerfile; 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+=("${GHCR_IMAGE}:${version}")
|
|
slim_multi_refs+=("${GHCR_IMAGE}:${version}-slim")
|
|
dockerhub_multi_refs+=("${DOCKERHUB_IMAGE}:${version}")
|
|
dockerhub_slim_multi_refs+=("${DOCKERHUB_IMAGE}:${version}-slim")
|
|
amd64_refs+=(
|
|
"${GHCR_IMAGE}:${version}-amd64"
|
|
"${GHCR_IMAGE}:${version}-slim-amd64"
|
|
)
|
|
dockerhub_amd64_refs+=(
|
|
"${DOCKERHUB_IMAGE}:${version}-amd64"
|
|
"${DOCKERHUB_IMAGE}:${version}-slim-amd64"
|
|
)
|
|
arm64_refs+=(
|
|
"${GHCR_IMAGE}:${version}-arm64"
|
|
"${GHCR_IMAGE}:${version}-slim-arm64"
|
|
)
|
|
dockerhub_arm64_refs+=(
|
|
"${DOCKERHUB_IMAGE}:${version}-arm64"
|
|
"${DOCKERHUB_IMAGE}:${version}-slim-arm64"
|
|
)
|
|
if [[ "${browser_supported}" == "1" ]]; then
|
|
multi_refs+=("${GHCR_IMAGE}:${version}-browser")
|
|
dockerhub_multi_refs+=("${DOCKERHUB_IMAGE}:${version}-browser")
|
|
amd64_refs+=("${GHCR_IMAGE}:${version}-browser-amd64")
|
|
dockerhub_amd64_refs+=("${DOCKERHUB_IMAGE}:${version}-browser-amd64")
|
|
arm64_refs+=("${GHCR_IMAGE}:${version}-browser-arm64")
|
|
dockerhub_arm64_refs+=("${DOCKERHUB_IMAGE}:${version}-browser-arm64")
|
|
fi
|
|
fi
|
|
if [[ ${#multi_refs[@]} -eq 0 || ${#amd64_refs[@]} -eq 0 || ${#arm64_refs[@]} -eq 0 || ${#dockerhub_multi_refs[@]} -eq 0 || ${#dockerhub_amd64_refs[@]} -eq 0 || ${#dockerhub_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"
|
|
echo "dockerhub_multi<<EOF"
|
|
printf "%s\n" "${dockerhub_multi_refs[@]}" "${dockerhub_slim_multi_refs[@]}"
|
|
echo "EOF"
|
|
echo "dockerhub_amd64<<EOF"
|
|
printf "%s\n" "${dockerhub_amd64_refs[@]}"
|
|
echo "EOF"
|
|
echo "dockerhub_arm64<<EOF"
|
|
printf "%s\n" "${dockerhub_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 }}
|
|
DOCKERHUB_MULTI_REFS: ${{ steps.refs.outputs.dockerhub_multi }}
|
|
DOCKERHUB_AMD64_REFS: ${{ steps.refs.outputs.dockerhub_amd64 }}
|
|
DOCKERHUB_ARM64_REFS: ${{ steps.refs.outputs.dockerhub_arm64 }}
|
|
run: |
|
|
set -euo pipefail
|
|
mapfile -t multi_refs <<< "${MULTI_REFS}"
|
|
mapfile -t amd64_refs <<< "${AMD64_REFS}"
|
|
mapfile -t arm64_refs <<< "${ARM64_REFS}"
|
|
mapfile -t dockerhub_multi_refs <<< "${DOCKERHUB_MULTI_REFS}"
|
|
mapfile -t dockerhub_amd64_refs <<< "${DOCKERHUB_AMD64_REFS}"
|
|
mapfile -t dockerhub_arm64_refs <<< "${DOCKERHUB_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[@]}"
|
|
node scripts/verify-docker-attestations.mjs \
|
|
--platform linux/amd64 \
|
|
--platform linux/arm64 \
|
|
"${dockerhub_multi_refs[@]}"
|
|
node scripts/verify-docker-attestations.mjs \
|
|
--platform linux/amd64 \
|
|
"${dockerhub_amd64_refs[@]}"
|
|
node scripts/verify-docker-attestations.mjs \
|
|
--platform linux/arm64 \
|
|
"${dockerhub_arm64_refs[@]}"
|
|
|
|
- name: Promote and verify channel aliases
|
|
if: ${{ needs.resolve_release_policy.outputs.channel != 'beta' }}
|
|
env:
|
|
VERSION: ${{ needs.resolve_release_policy.outputs.version }}
|
|
GHCR_IMAGE: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
DOCKERHUB_IMAGE: ${{ env.DOCKERHUB_REGISTRY }}/${{ env.DOCKERHUB_IMAGE_NAME }}
|
|
run: |
|
|
node scripts/docker-channel-promote.mjs \
|
|
--version "${VERSION}" \
|
|
--image "${GHCR_IMAGE}" \
|
|
--image "${DOCKERHUB_IMAGE}"
|