Skip to content

Build and Push Docker Image #260

Build and Push Docker Image

Build and Push Docker Image #260

Workflow file for this run

name: Build and Push Docker Image
on:
workflow_dispatch:
push:
branches:
- main
tags:
- 'v*'
pull_request:
branches:
- main
schedule:
- cron: '0 2 * * *' # 2am UTC daily for nightly builds
concurrency:
group: docker-${{ github.ref }}
cancel-in-progress: true
env:
REGISTRY: ghcr.io
CONTAINAI_VERBOSE: "1"
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install shellcheck
run: sudo apt-get update && sudo apt-get install -y shellcheck
- name: Lint shell scripts with shellcheck
run: |
echo "Running shellcheck on shell scripts..."
shellcheck -x src/*.sh src/lib/*.sh
- name: Check manifest consistency
run: ./scripts/check-manifest-consistency.sh
- name: Collect ContainAI logs
if: always()
run: |
mkdir -p /tmp/containai-logs
cp -a ~/.config/containai/logs /tmp/containai-logs/ 2>/dev/null || true
cp -a ~/.containai-test-* /tmp/containai-logs/ 2>/dev/null || true
- name: Upload ContainAI logs
if: always()
uses: actions/upload-artifact@v4
with:
name: containai-logs-lint-${{ github.run_id }}
path: /tmp/containai-logs/
retention-days: 7
if-no-files-found: ignore
build-push:
needs: [lint, build-tarballs]
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image_prefix: ${{ steps.build_strategy.outputs.image_prefix }}
tag_suffix: ${{ steps.build_strategy.outputs.tag_suffix }}
push: ${{ steps.build_strategy.outputs.push }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Convert repository name to lowercase
run: |
echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV
- name: Set version from build-tarballs
run: |
echo "NBGV_SemVer2=${{ needs.build-tarballs.outputs.version }}" >> "$GITHUB_ENV"
- name: Download Linux amd64 tarball
uses: actions/download-artifact@v4
with:
name: containai-tarball-amd64
path: /tmp/tarball-amd64
- name: Download Linux arm64 tarball
uses: actions/download-artifact@v4
with:
name: containai-tarball-arm64
path: /tmp/tarball-arm64
- name: Stage ContainAI CLI tarballs
run: |
mkdir -p artifacts/cai-tarballs
cp /tmp/tarball-amd64/containai-*.tar.gz artifacts/cai-tarballs/
cp /tmp/tarball-arm64/containai-*.tar.gz artifacts/cai-tarballs/
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: arm64,amd64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Get build date
id: date
run: echo "value=$(date +%Y%m%d)" >> $GITHUB_OUTPUT
- name: Determine build strategy
id: build_strategy
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
echo "push=true" >> $GITHUB_OUTPUT
echo "load=false" >> $GITHUB_OUTPUT
echo "image_prefix=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT
echo "tag_suffix=pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT
else
echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT
echo "push=true" >> $GITHUB_OUTPUT
echo "load=false" >> $GITHUB_OUTPUT
echo "image_prefix=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}" >> $GITHUB_OUTPUT
# tag_suffix for layer images: latest for tags, nightly for main/schedule
if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
echo "tag_suffix=latest" >> $GITHUB_OUTPUT
else
# Main branch push OR scheduled run
echo "tag_suffix=nightly" >> $GITHUB_OUTPUT
fi
fi
- name: Build and push base image
uses: docker/build-push-action@v6
with:
context: .
file: src/container/Dockerfile.base
platforms: ${{ steps.build_strategy.outputs.platforms }}
push: ${{ steps.build_strategy.outputs.push }}
load: ${{ steps.build_strategy.outputs.load }}
tags: ${{ steps.build_strategy.outputs.image_prefix }}/base:${{ steps.build_strategy.outputs.tag_suffix }}
build-args: |
BUILD_DATE=${{ github.run_started_at }}
VCS_REF=${{ github.sha }}
VERSION=${{ env.NBGV_SemVer2 }}
cache-from: type=gha,scope=base
cache-to: type=gha,mode=max,scope=base
- name: Build and push sdks image
uses: docker/build-push-action@v6
with:
context: .
file: src/container/Dockerfile.sdks
platforms: ${{ steps.build_strategy.outputs.platforms }}
push: ${{ steps.build_strategy.outputs.push }}
load: ${{ steps.build_strategy.outputs.load }}
tags: ${{ steps.build_strategy.outputs.image_prefix }}/sdks:${{ steps.build_strategy.outputs.tag_suffix }}
build-args: |
BASE_IMAGE=${{ steps.build_strategy.outputs.image_prefix }}/base:${{ steps.build_strategy.outputs.tag_suffix }}
BUILD_DATE=${{ github.run_started_at }}
VCS_REF=${{ github.sha }}
VERSION=${{ env.NBGV_SemVer2 }}
cache-from: type=gha,scope=sdks
cache-to: type=gha,mode=max,scope=sdks
- name: Generate container files
run: |
mkdir -p artifacts/container-generated
./src/scripts/gen-dockerfile-symlinks.sh src/manifests artifacts/container-generated/symlinks.sh
./src/scripts/gen-init-dirs.sh src/manifests artifacts/container-generated/init-dirs.sh
./src/scripts/gen-container-link-spec.sh src/manifests artifacts/container-generated/link-spec.json
./src/scripts/gen-agent-wrappers.sh src/manifests artifacts/container-generated/agent-wrappers.sh
cp src/container/link-repair.sh artifacts/container-generated/link-repair.sh
- name: Build and push full image
uses: docker/build-push-action@v6
with:
context: .
file: src/container/Dockerfile.agents
platforms: ${{ steps.build_strategy.outputs.platforms }}
push: ${{ steps.build_strategy.outputs.push }}
load: ${{ steps.build_strategy.outputs.load }}
tags: ${{ steps.build_strategy.outputs.image_prefix }}/agents:${{ steps.build_strategy.outputs.tag_suffix }}
build-args: |
SDKS_IMAGE=${{ steps.build_strategy.outputs.image_prefix }}/sdks:${{ steps.build_strategy.outputs.tag_suffix }}
BUILD_DATE=${{ github.run_started_at }}
VCS_REF=${{ github.sha }}
VERSION=${{ env.NBGV_SemVer2 }}
cache-from: type=gha,scope=full-v2
cache-to: type=gha,mode=max,scope=full-v2
- name: Extract metadata (tags, labels)
id: meta
if: steps.build_strategy.outputs.push == 'true'
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# latest ONLY for tagged releases
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
# Semver for tags only
type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }}
type=semver,pattern={{major}},enable=${{ startsWith(github.ref, 'refs/tags/v') && !startsWith(github.ref, 'refs/tags/v0.') }}
# Nightly for main branch and schedule
type=raw,value=nightly,enable=${{ github.ref == 'refs/heads/main' || github.event_name == 'schedule' }}
type=raw,value=nightly-${{ steps.date.outputs.value }},enable=${{ github.ref == 'refs/heads/main' || github.event_name == 'schedule' }}
# SHA for all builds
type=sha,prefix=sha-
- name: Build and push Docker image
if: steps.build_strategy.outputs.push == 'true'
uses: docker/build-push-action@v6
with:
context: .
file: src/container/Dockerfile
platforms: ${{ steps.build_strategy.outputs.platforms }}
push: ${{ steps.build_strategy.outputs.push }}
load: ${{ steps.build_strategy.outputs.load }}
tags: ${{ steps.meta.outputs.tags }}
build-args: |
AGENTS_IMAGE=${{ steps.build_strategy.outputs.image_prefix }}/agents:${{ steps.build_strategy.outputs.tag_suffix }}
BUILD_DATE=${{ github.run_started_at }}
VCS_REF=${{ github.sha }}
VERSION=${{ env.NBGV_SemVer2 }}
cache-from: type=gha,scope=main
cache-to: type=gha,mode=max,scope=main
- name: Collect ContainAI logs
if: always()
run: |
mkdir -p /tmp/containai-logs
cp -a ~/.config/containai/logs /tmp/containai-logs/ 2>/dev/null || true
cp -a ~/.containai-test-* /tmp/containai-logs/ 2>/dev/null || true
docker info > /tmp/containai-logs/docker-info.log 2>&1 || true
docker buildx ls > /tmp/containai-logs/buildx-ls.log 2>&1 || true
- name: Upload ContainAI logs
if: always()
uses: actions/upload-artifact@v4
with:
name: containai-logs-build-push-${{ github.run_id }}
path: /tmp/containai-logs/
retention-days: 7
if-no-files-found: ignore
# ============================================================================
# Build Test Images (per-arch) - shared across test jobs
# ============================================================================
# ============================================================================
# Build Multi-arch Tarballs
# ============================================================================
# Builds AOT .NET binaries for each architecture and creates release tarballs.
# PR builds upload tarballs as artifacts for E2E testing.
# Release builds are handled by the release workflow.
# ============================================================================
build-tarballs:
needs: lint
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
outputs:
version: ${{ steps.nbgv.outputs.SemVer2 }}
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
tarball_target: linux-x64
- arch: arm64
runner: ubuntu-24.04-arm
tarball_target: linux-arm64
- arch: macos-x64
runner: macos-15-intel
tarball_target: macos-x64
- arch: macos-arm64
runner: macos-26
tarball_target: macos-arm64
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Install build dependencies
run: ./scripts/install-build-dependencies.sh -y
- name: Install and run NBGV
id: nbgv
run: |
dotnet tool restore
echo "SemVer2=$(dotnet nbgv get-version -v SemVer2)" >> "$GITHUB_OUTPUT"
echo "NBGV_SemVer2=$(dotnet nbgv get-version -v SemVer2)" >> "$GITHUB_ENV"
- name: Build release tarball
run: |
echo "============================================="
echo "Creating tarball for ${{ matrix.tarball_target }}"
echo "============================================="
case "${{ matrix.tarball_target }}" in
linux-x64) platform="linux/amd64" ;;
linux-arm64) platform="linux/arm64" ;;
macos-x64) platform="macos/amd64" ;;
macos-arm64) platform="macos/arm64" ;;
*) echo "ERROR: Unknown tarball target: ${{ matrix.tarball_target }}" >&2; exit 1 ;;
esac
if [[ "$(uname -s)" == "Darwin" ]]; then
bash_cmd="$(brew --prefix)/bin/bash"
else
bash_cmd="bash"
fi
"$bash_cmd" ./src/scripts/build-cai-tarballs.sh \
--platforms "$platform" \
--version "$NBGV_SemVer2" \
--output-dir ./artifacts
ls -la ./artifacts/
- name: Upload tarball artifact
uses: actions/upload-artifact@v4
with:
name: containai-tarball-${{ matrix.arch }}
path: ./artifacts/containai-*.tar.gz
retention-days: 7
if-no-files-found: error
- name: Collect ContainAI logs
if: always()
run: |
mkdir -p /tmp/containai-logs
cp -a ~/.config/containai/logs /tmp/containai-logs/ 2>/dev/null || true
cp -a ~/.containai-test-* /tmp/containai-logs/ 2>/dev/null || true
- name: Upload ContainAI logs
if: always()
uses: actions/upload-artifact@v4
with:
name: containai-logs-build-tarballs-${{ matrix.arch }}-${{ github.run_id }}
path: /tmp/containai-logs/
retention-days: 7
if-no-files-found: ignore
# ============================================================================
# Import Tests
# ============================================================================
# Tiered test strategy:
# - Host-side tests (manifest parsing, consistency) run in lint job above
# - Integration tests run here after build, using the built image
# - Full E2E tests (sysbox/systemd containers) require self-hosted runners
# with sysbox installed - see docs/testing.md for manual E2E testing
#
# Test execution:
# - Depends on build-push job (single build stage)
# - Pulls images from registry (no local rebuild)
# - Runs full test suite (standard Docker runtime, no sysbox required)
#
# Note: Images are produced in build-push and pulled by test jobs.
# ============================================================================
test:
needs: build-push
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
permissions:
contents: read
packages: read
env:
IMAGE_PREFIX: ${{ needs.build-push.outputs.image_prefix }}
IMAGE_TAG: ${{ needs.build-push.outputs.tag_suffix }}
IMAGE_NAME: ${{ needs.build-push.outputs.image_prefix }}/agents:${{ needs.build-push.outputs.tag_suffix }}
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
dotnet_rid: linux-x64
- arch: arm64
runner: ubuntu-24.04-arm
dotnet_rid: linux-arm64
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: Pull test images
run: |
docker pull "${IMAGE_PREFIX}/agents:${IMAGE_TAG}"
docker pull "${IMAGE_PREFIX}/base:${IMAGE_TAG}"
- name: Run import integration tests
run: |
echo "============================================="
echo "Running import integration tests (${{ matrix.arch }})"
echo "============================================="
./tests/integration/test-sync-integration.sh
- name: Run manifest parsing unit tests
run: |
echo "============================================="
echo "Running manifest parsing unit tests"
echo "============================================="
./tests/unit/test-manifest-parsing.sh
- name: Run launch wrapper tests
run: |
echo "============================================="
echo "Running launch wrapper tests (${{ matrix.arch }})"
echo "============================================="
./tests/integration/test-launch-wrappers.sh
- name: Run user manifest tests
run: |
echo "============================================="
echo "Running user manifest tests (${{ matrix.arch }})"
echo "============================================="
./tests/integration/test-user-manifests.sh
- name: Run .NET unit tests (if test projects exist)
run: |
echo "============================================="
echo "Running .NET unit tests (${{ matrix.arch }})"
echo "============================================="
if find . -name '*Tests*.csproj' -o -name '*.Tests.csproj' | grep -q .; then
dotnet test --configuration Release
else
echo "No test projects found, skipping dotnet test"
fi
- name: Build ACP Proxy (${{ matrix.arch }})
run: |
echo "============================================="
echo "Building ACP proxy binary (${{ matrix.arch }})"
echo "============================================="
cd src/acp-proxy
dotnet publish -r ${{ matrix.dotnet_rid }} -c Release --self-contained -o publish
mkdir -p ../bin
cp publish/acp-proxy ../bin/acp-proxy
chmod +x ../bin/acp-proxy
- name: Run ACP integration tests
env:
CAI_ACP_TEST_MODE: "1"
CAI_ACP_DIRECT_SPAWN: "1"
run: |
echo "============================================="
echo "Running ACP proxy integration tests (${{ matrix.arch }})"
echo "============================================="
chmod +x ./tests/integration/mock-acp-server
./tests/integration/test-acp-proxy.sh
- name: Collect ContainAI logs
if: always()
run: |
mkdir -p /tmp/containai-logs
cp -a ~/.config/containai/logs /tmp/containai-logs/ 2>/dev/null || true
cp -a ~/.containai-test-* /tmp/containai-logs/ 2>/dev/null || true
docker info > /tmp/containai-logs/docker-info.log 2>&1 || true
- name: Upload ContainAI logs
if: always()
uses: actions/upload-artifact@v4
with:
name: containai-logs-test-${{ matrix.arch }}-${{ github.run_id }}
path: /tmp/containai-logs/
retention-days: 7
if-no-files-found: ignore
# ============================================================================
# E2E Tests (Sysbox/DinD) - GitHub-hosted runners
# ============================================================================
# Attempts to run E2E tests on GitHub-hosted runners. Sysbox requires kernel
# module loading which may not work due to virtualization constraints.
# This job gracefully skips if sysbox installation fails.
#
# Install flow: Downloads tarball artifact from build-tarballs job, tests
# install.sh --local --yes --no-setup to verify the tarball install path works.
# ============================================================================
e2e-test:
needs: [build-push, build-tarballs]
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
permissions:
contents: read
packages: read
env:
IMAGE_PREFIX: ${{ needs.build-push.outputs.image_prefix }}
IMAGE_TAG: ${{ needs.build-push.outputs.tag_suffix }}
CONTAINAI_TEST_IMAGE: ${{ needs.build-push.outputs.image_prefix }}/base:${{ needs.build-push.outputs.tag_suffix }}
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm
steps:
# Always checkout repository - needed for build.sh and test scripts
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Download tarball artifact
uses: actions/download-artifact@v4
with:
name: containai-tarball-${{ matrix.arch }}
path: /tmp/tarball
- name: Extract and install from tarball
run: |
echo "============================================="
echo "Installing from tarball artifact"
echo "============================================="
cd /tmp/tarball
tar xzf containai-*.tar.gz
cd containai-*/
./install.sh --local --yes --no-setup
- name: Run cai setup
env:
CAI_YES: "1"
run: |
echo "============================================="
echo "Running cai setup"
echo "============================================="
export PATH="$HOME/.local/bin:$PATH"
cai setup --verbose
- name: Log in to Container Registry (containai-docker)
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker --context containai-docker login ${{ env.REGISTRY }} -u "${{ github.actor }}" --password-stdin
- name: Smoke test acp-proxy binary
run: |
ACP_PROXY="$HOME/.local/share/containai/acp-proxy"
if [[ -f "$ACP_PROXY" ]]; then
echo "Testing acp-proxy binary..."
if "$ACP_PROXY" --help >/dev/null 2>&1; then
echo "acp-proxy binary works"
else
echo "::error::acp-proxy binary failed to run (may be glibc incompatibility)"
exit 1
fi
else
echo "::error::acp-proxy binary not found at $ACP_PROXY"
exit 1
fi
- name: Check kernel version
id: kernel
run: |
echo "============================================="
echo "Kernel Information"
echo "============================================="
uname -a
echo "KERNEL_VERSION=$(uname -r)" >> "$GITHUB_OUTPUT"
- name: Pull test images into containai-docker
run: |
docker --context containai-docker pull "${CONTAINAI_TEST_IMAGE}"
docker --context containai-docker images | grep -E '^(containai|ghcr.io|REPOSITORY)' || true
- name: Run E2E tests (DinD)
run: |
echo "============================================="
echo "Running Docker-in-Docker E2E tests"
echo "============================================="
./tests/integration/test-dind.sh
- name: Run startup hooks tests (Sysbox)
run: |
echo "============================================="
echo "Running startup hooks tests (requires Sysbox)"
echo "============================================="
./tests/integration/test-startup-hooks.sh
- name: Collect sysbox logs
if: always()
run: |
mkdir -p /tmp/sysbox-logs
journalctl -u sysbox-mgr --no-pager > /tmp/sysbox-logs/sysbox-mgr.log 2>&1 || true
journalctl -u sysbox-fs --no-pager > /tmp/sysbox-logs/sysbox-fs.log 2>&1 || true
sudo cp -r /var/log/sysbox/ /tmp/sysbox-logs/ 2>/dev/null || true
- name: Upload E2E test logs on failure
if: always()
uses: actions/upload-artifact@v4
with:
name: e2e-test-logs-${{ matrix.arch }}-${{ github.run_id }}
path: |
~/.containai-test-*
/tmp/sysbox-logs/
retention-days: 7
if-no-files-found: ignore
- name: Collect ContainAI logs
if: always()
run: |
mkdir -p /tmp/containai-logs
cp -a ~/.config/containai/logs /tmp/containai-logs/ 2>/dev/null || true
cp -a ~/.containai-test-* /tmp/containai-logs/ 2>/dev/null || true
cp -a /tmp/containai-docker-info.log /tmp/containai-logs/ 2>/dev/null || true
docker info > /tmp/containai-logs/docker-info.log 2>&1 || true
docker --context containai-docker info > /tmp/containai-logs/containai-docker-info.log 2>&1 || true
- name: Upload ContainAI logs
if: always()
uses: actions/upload-artifact@v4
with:
name: containai-logs-e2e-${{ matrix.arch }}-${{ github.run_id }}
path: /tmp/containai-logs/
retention-days: 7
if-no-files-found: ignore
# ============================================================================
# E2E Tests (macOS + Lima) - Intel (enabled)
# ============================================================================
e2e-test-macos-intel:
needs: [lint, build-push, build-tarballs]
runs-on: macos-15-intel
timeout-minutes: 60
permissions:
contents: read
packages: read
env:
IMAGE_PREFIX: ${{ needs.build-push.outputs.image_prefix }}
IMAGE_TAG: ${{ needs.build-push.outputs.tag_suffix }}
CONTAINAI_TEST_IMAGE: ${{ needs.build-push.outputs.image_prefix }}/base:${{ needs.build-push.outputs.tag_suffix }}
IMAGE_ARCH: amd64
TARBALL_ARCH: macos-x64
EXPECTED_UNAME: x86_64
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Verify runner architecture
run: |
machine="$(uname -m)"
expected="${EXPECTED_UNAME}"
if [[ "$machine" != "$expected" ]]; then
echo "::error::Runner arch mismatch. Expected $expected, got $machine"
exit 1
fi
- name: Download tarball artifact
uses: actions/download-artifact@v4
with:
name: containai-tarball-${{ env.TARBALL_ARCH }}
path: /tmp/tarball
- name: Extract and install from tarball
run: |
echo "============================================="
echo "Installing from tarball artifact"
echo "============================================="
cd /tmp/tarball
tar xzf containai-*.tar.gz
cd containai-*/
./install.sh --local --yes --no-setup
- name: Run cai setup (Lima + Sysbox)
env:
CAI_YES: "1"
run: |
set -euo pipefail
echo "============================================="
echo "Running cai setup"
echo "============================================="
export PATH="$HOME/.local/bin:$PATH"
setup_timeout_secs=1200
run_cai_setup_with_timeout() {
if command -v gtimeout >/dev/null 2>&1; then
gtimeout "$setup_timeout_secs" cai setup --verbose
else
# macOS runners always have perl; use alarm() as a timeout fallback.
perl -e 'alarm shift; exec @ARGV or die $!' "$setup_timeout_secs" cai setup --verbose
fi
}
for attempt in 1 2; do
echo "Attempt $attempt: cai setup --verbose (timeout ${setup_timeout_secs}s)"
if run_cai_setup_with_timeout; then
echo "cai setup succeeded"
break
fi
setup_rc=$?
if [[ $setup_rc -eq 124 || $setup_rc -eq 142 ]]; then
echo "::warning::cai setup timed out on attempt $attempt after ${setup_timeout_secs}s"
else
echo "::warning::cai setup failed on attempt $attempt (rc=$setup_rc)"
fi
if [[ -d "$HOME/.config/containai/logs" ]]; then
echo "Recent ContainAI logs:"
while IFS= read -r file; do
echo "--- $file (tail) ---"
tail -n 20 "$file" || true
done < <(find "$HOME/.config/containai/logs" -type f -name '*.log' 2>/dev/null | head -20 || true)
fi
limactl list || true
if [[ "$attempt" -lt 2 ]]; then
if command -v limactl >/dev/null 2>&1; then
limactl delete -f containai-docker || true
fi
rm -rf "$HOME/.lima/containai-docker" || true
echo "Retrying after Lima cleanup..."
continue
fi
echo "::error::cai setup failed after retries"
exit 1
done
- name: Ensure containai-docker context is ready
run: |
set -euo pipefail
CONTEXT_NAME="${{ env.CONTAINAI_DOCKER_CONTEXT }}"
if [[ -z "$CONTEXT_NAME" ]]; then
CONTEXT_NAME="containai-docker"
fi
echo "============================================="
echo "Context diagnostics + readiness checks"
echo "============================================="
docker context ls || true
if ! docker context inspect "$CONTEXT_NAME" >/dev/null 2>&1; then
echo "::error::Docker context '$CONTEXT_NAME' not found after cai setup"
docker context ls || true
exit 1
fi
context_host=$(docker context inspect "$CONTEXT_NAME" --format '{{.Endpoints.docker.Host}}' 2>/dev/null || true)
echo "Context '$CONTEXT_NAME' endpoint: ${context_host:-unknown}"
if [[ "$context_host" == unix://* ]]; then
socket_path="${context_host#unix://}"
echo "Waiting for context socket: $socket_path"
for i in $(seq 1 60); do
if [[ -S "$socket_path" ]]; then
echo "Socket ready"
break
fi
sleep 2
done
if [[ ! -S "$socket_path" ]]; then
echo "::error::Context socket not available: $socket_path"
ls -la "$(dirname "$socket_path")" || true
exit 1
fi
fi
echo "Waiting for Docker API via context '$CONTEXT_NAME'"
for i in $(seq 1 60); do
if docker --context "$CONTEXT_NAME" info >/tmp/containai-docker-info.log 2>&1; then
echo "Docker context '$CONTEXT_NAME' is ready"
break
fi
echo "Attempt $i/60: context not ready yet"
tail -n 5 /tmp/containai-docker-info.log 2>/dev/null || true
sleep 2
done
if ! docker --context "$CONTEXT_NAME" info >/dev/null 2>&1; then
echo "::error::Docker context '$CONTEXT_NAME' not reachable"
cat /tmp/containai-docker-info.log || true
docker --context "$CONTEXT_NAME" version || true
if command -v limactl >/dev/null 2>&1; then
limactl list || true
fi
exit 1
fi
- name: Log in to Container Registry (containai-docker)
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | docker --context containai-docker login ${{ env.REGISTRY }} -u "${{ github.actor }}" --password-stdin
- name: Pull images into Lima
run: |
docker --context containai-docker pull "${CONTAINAI_TEST_IMAGE}"
docker --context containai-docker images | grep -E '^(containai|ghcr.io|REPOSITORY)' || true
- name: Diagnose Lima iptables access
run: |
set +e
VM_NAME="containai-docker"
echo "============================================="
echo "Lima iptables diagnostics"
echo "============================================="
limactl list || true
limactl shell "$VM_NAME" -- sh -c 'id; uname -a' || true
limactl shell "$VM_NAME" -- sh -c 'command -v sudo || true; command -v iptables || true; command -v iptables-nft || true' || true
limactl shell "$VM_NAME" -- sh -c 'ls -l /usr/sbin/iptables /sbin/iptables /usr/bin/iptables /usr/sbin/iptables-nft /sbin/iptables-nft /usr/bin/iptables-nft 2>/dev/null || true' || true
limactl shell "$VM_NAME" -- sh -c 'sudo -n true && echo "sudo-nopasswd=yes" || echo "sudo-nopasswd=no"' || true
limactl shell "$VM_NAME" -- sh -c 'sudo -n iptables -S >/tmp/cai-iptables-s.out 2>/tmp/cai-iptables-s.err; echo "sudo iptables -S rc=$?"; cat /tmp/cai-iptables-s.err' || true
limactl shell "$VM_NAME" -- sh -c 'sudo -n iptables-nft -S >/tmp/cai-iptables-nft-s.out 2>/tmp/cai-iptables-nft-s.err; echo "sudo iptables-nft -S rc=$?"; cat /tmp/cai-iptables-nft-s.err' || true
limactl shell "$VM_NAME" -- sh -c 'sudo -n iptables -S DOCKER-USER >/tmp/cai-iptables-docker-user.out 2>/tmp/cai-iptables-docker-user.err; echo "sudo iptables -S DOCKER-USER rc=$?"; cat /tmp/cai-iptables-docker-user.err' || true
- name: Run Secure Engine E2E tests (macOS)
run: |
BASH_BIN="$(brew --prefix bash)/bin/bash"
"$BASH_BIN" -lc './tests/integration/test-secure-engine.sh'
"$BASH_BIN" -lc './tests/integration/test-dind.sh'
"$BASH_BIN" -lc './tests/integration/test-dind-runc133.sh'
"$BASH_BIN" -lc './tests/integration/test-network-blocking.sh'
- name: Collect Lima diagnostics
if: always()
run: |
set +e
VM_NAME="${{ env.CONTAINAI_DOCKER_CONTEXT }}"
if [[ -z "$VM_NAME" ]]; then
VM_NAME="containai-docker"
fi
echo "============================================="
echo "Lima diagnostics (failure)"
echo "============================================="
mkdir -p /tmp/lima-logs
limactl list > /tmp/lima-logs/limactl-list.log 2>&1 || true
limactl info "$VM_NAME" > /tmp/lima-logs/limactl-info.log 2>&1 || true
if [[ -d "$HOME/.lima/$VM_NAME" ]]; then
cp -a "$HOME/.lima/$VM_NAME"/*.log /tmp/lima-logs/ 2>/dev/null || true
cp -a "$HOME/.lima/$VM_NAME"/serial*.log /tmp/lima-logs/ 2>/dev/null || true
fi
if [[ -d "$HOME/Library/Logs/Lima" ]]; then
cp -a "$HOME/Library/Logs/Lima" /tmp/lima-logs/ 2>/dev/null || true
fi
limactl shell "$VM_NAME" -- systemctl status docker > /tmp/lima-logs/docker-status.log 2>&1 || true
limactl shell "$VM_NAME" -- journalctl -u docker --no-pager > /tmp/lima-logs/docker-journal.log 2>&1 || true
limactl shell "$VM_NAME" -- journalctl -u sysbox-mgr --no-pager > /tmp/lima-logs/sysbox-mgr-journal.log 2>&1 || true
limactl shell "$VM_NAME" -- journalctl -u sysbox-fs --no-pager > /tmp/lima-logs/sysbox-fs-journal.log 2>&1 || true
- name: Upload Lima logs
if: always()
uses: actions/upload-artifact@v4
with:
name: lima-logs-macos-15-intel-${{ github.run_id }}
path: /tmp/lima-logs/
retention-days: 7
if-no-files-found: ignore
- name: Collect ContainAI logs
if: always()
run: |
mkdir -p /tmp/containai-logs
cp -a ~/.config/containai/logs /tmp/containai-logs/ 2>/dev/null || true
cp -a ~/.containai-test-* /tmp/containai-logs/ 2>/dev/null || true
docker info > /tmp/containai-logs/docker-info.log 2>&1 || true
docker --context containai-docker info > /tmp/containai-logs/containai-docker-info.log 2>&1 || true
- name: Upload ContainAI logs
if: always()
uses: actions/upload-artifact@v4
with:
name: containai-logs-macos-macos-15-intel-${{ github.run_id }}
path: /tmp/containai-logs/
retention-days: 7
if-no-files-found: ignore
# ============================================================================
# E2E Tests (macOS + Lima) - ARM (disabled)
# ============================================================================
e2e-test-macos-arm:
if: ${{ false }}
needs: [lint, build-push, build-tarballs]
runs-on: macos-26
timeout-minutes: 60
permissions:
contents: read
packages: read
steps:
- name: Skip macOS ARM E2E
run: |
echo "macOS ARM E2E is disabled on GitHub-hosted runners."