Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 201 additions & 6 deletions .github/workflows/base-image.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,121 @@ env:
REGISTRY: ghcr.io

jobs:
# Keep one declarative publisher configuration while giving each base image
# an independently observable matrix job and registry cache namespace.
# Build OpenClaw on native runners because its complete Perl regression suite
# is prohibitively slow under QEMU. Publish only immutable per-platform
# digests here; the dependent manifest job updates user-facing tags atomically.
build-openclaw-platforms:
name: Build OpenClaw base image (${{ matrix.arch }})
if: github.repository == 'NVIDIA/NemoClaw'
runs-on: ${{ matrix.runner }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include:
- agent: openclaw
arch: amd64
platform: linux/amd64
runner: ubuntu-24.04
dockerfile: Dockerfile.base
image: nvidia/nemoclaw/sandbox-base
- agent: openclaw
arch: arm64
platform: linux/arm64
runner: ubuntu-24.04-arm
dockerfile: Dockerfile.base
image: nvidia/nemoclaw/sandbox-base
steps:
- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

- name: Log in to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
env:
DOCKER_METADATA_SHORT_SHA_LENGTH: 8
with:
images: ${{ env.REGISTRY }}/${{ matrix.image }}
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=ref,event=tag
type=sha,prefix=,format=short

- name: Validate production Docker build args
id: production-build-args
env:
AGENT: ${{ matrix.agent }}
OPENCLAW_VERSION_INPUT: ${{ inputs.openclaw_version }}
run: |
set -euo pipefail
build_args=()
openclaw_build_arg=""
if [ "$AGENT" = "openclaw" ] && [ -n "${OPENCLAW_VERSION_INPUT}" ]; then
openclaw_build_arg="OPENCLAW_VERSION=${OPENCLAW_VERSION_INPUT}"
build_args+=(--build-arg "$openclaw_build_arg")
fi
if [ "${#build_args[@]}" -gt 0 ]; then
scripts/check-production-build-args.sh "${build_args[@]}"
else
scripts/check-production-build-args.sh
fi
if [ "$AGENT" = "openclaw" ] && [ -n "${OPENCLAW_VERSION_INPUT}" ]; then
if [[ "$OPENCLAW_VERSION_INPUT" == *$'\r'* || "$OPENCLAW_VERSION_INPUT" == *$'\n'* ]]; then
echo "ERROR: OpenClaw version must not contain CR or LF characters." >&2
exit 1
fi
if [[ ! "$OPENCLAW_VERSION_INPUT" =~ ^[0-9]+([.][0-9]+)*$ ]]; then
echo "ERROR: OpenClaw version must be a whole decimal dotted version (for example, 2026.6.10)." >&2
exit 1
fi
fi
printf 'openclaw_build_arg=%s\n' "$openclaw_build_arg" >> "$GITHUB_OUTPUT"

- name: Build and push platform digest
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
file: ${{ matrix.dockerfile }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.REGISTRY }}/${{ matrix.image }},push-by-digest=true,name-canonical=true,push=true
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache-${{ matrix.arch }}
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache-${{ matrix.arch }},mode=max
build-args: ${{ steps.production-build-args.outputs.openclaw_build_arg }}

- name: Export platform digest
env:
DIGEST: ${{ steps.build.outputs.digest }}
run: |
set -euo pipefail
if [[ ! "$DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "ERROR: build did not return a valid sha256 digest: $DIGEST" >&2
exit 1
fi
mkdir -p "$RUNNER_TEMP/digests"
touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}"

- name: Upload platform digest
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: openclaw-base-digest-${{ matrix.arch }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1

# Hermes and Deep Agents Code still use the existing QEMU multi-platform
# publisher. Their build behavior and timeout remain unchanged.
build-and-push:
name: Build and push ${{ matrix.display_name }} base image
if: github.repository == 'NVIDIA/NemoClaw'
Expand All @@ -69,10 +182,6 @@ jobs:
fail-fast: false
matrix:
include:
- agent: openclaw
display_name: OpenClaw
dockerfile: Dockerfile.base
image: nvidia/nemoclaw/sandbox-base
- agent: hermes
display_name: Hermes
dockerfile: agents/hermes/Dockerfile.base
Expand Down Expand Up @@ -152,3 +261,89 @@ jobs:
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache,mode=max
build-args: ${{ steps.production-build-args.outputs.openclaw_build_arg }}

# Preserve the established required-check name while making tag publication
# contingent on both native platform builds succeeding.
build-and-push-openclaw:
name: Build and push OpenClaw base image
if: github.repository == 'NVIDIA/NemoClaw'
needs: build-openclaw-platforms
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Download platform digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: openclaw-base-digest-*
path: ${{ runner.temp }}/digests
merge-multiple: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0

- name: Log in to GHCR
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
env:
DOCKER_METADATA_SHORT_SHA_LENGTH: 8
with:
images: ${{ env.REGISTRY }}/nvidia/nemoclaw/sandbox-base
tags: |
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
type=ref,event=tag
type=sha,prefix=,format=short

- name: Create and verify multi-platform manifest
env:
IMAGE: ${{ env.REGISTRY }}/nvidia/nemoclaw/sandbox-base
TAGS: ${{ steps.meta.outputs.tags }}
run: |
set -euo pipefail
shopt -s nullglob
digest_files=("$RUNNER_TEMP"/digests/*)
if [ "${#digest_files[@]}" -ne 2 ]; then
echo "ERROR: expected exactly two platform digests, found ${#digest_files[@]}." >&2
exit 1
fi

sources=()
for digest_file in "${digest_files[@]}"; do
digest="$(basename "$digest_file")"
if [[ ! "$digest" =~ ^[0-9a-f]{64}$ ]]; then
echo "ERROR: invalid platform digest artifact: $digest" >&2
exit 1
fi
sources+=("$IMAGE@sha256:$digest")
done

mapfile -t tags <<< "$TAGS"
tag_args=()
for tag in "${tags[@]}"; do
if [ -n "$tag" ]; then
tag_args+=(--tag "$tag")
fi
done
if [ "${#tag_args[@]}" -eq 0 ]; then
echo "ERROR: metadata did not produce any publication tags." >&2
exit 1
fi

docker buildx imagetools create "${tag_args[@]}" "${sources[@]}"
first_tag="${tags[0]}"
actual_platforms="$(
docker buildx imagetools inspect "$first_tag" --raw \
| jq -r '.manifests[] | select(.platform.os == "linux") | .platform.architecture' \
| sort -u \
| paste -sd, -
)"
if [ "$actual_platforms" != "amd64,arm64" ]; then
echo "ERROR: published manifest has unexpected platforms: $actual_platforms" >&2
exit 1
fi
12 changes: 6 additions & 6 deletions Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \

WORKDIR /tmp/perl-source

# Perl Configure's d_syscallproto probe compiles and runs a target test through
# Buildx/QEMU. The QEMU-backed build returns a false negative for trixie's libc
# declaration, so pin the known target result instead of synthesizing a
# conflicting prototype. Remove this override when the QEMU-backed multi-arch
# build in .github/workflows/base-image.yaml passes without it.
# Pin the reviewed d_syscallproto result for trixie's libc so both native
# architectures use the same known declaration instead of relying on a
# Configure probe that previously returned a false negative under QEMU.
# Remove this override only after the pinned base image and Perl release report
# d_syscallproto=define from native Configure probes on amd64 and arm64.
# Perl's test_harness schedules independent upstream tests through TEST_JOBS.
# This preserves the full suite without serial QEMU execution.
# This preserves the full suite while using each native runner efficiently.
RUN curl --proto '=https' --tlsv1.2 -fsSL \
--retry 5 --retry-all-errors --retry-delay 2 --connect-timeout 15 --max-time 120 \
-o /tmp/perl.tar.xz "https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.xz" \
Expand Down
Loading
Loading