Build and Push Docker Image #260
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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." |