Podman: fix compose-devcontainer health + multi-service checkpoint/restore #196
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: ci | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| permissions: | |
| contents: read | |
| # Force JavaScript-based actions onto Node.js 24 ahead of the | |
| # 2026-06-02 default flip; silences the Node 20 deprecation banner. | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| jobs: | |
| lint: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: "1.25" | |
| cache: true | |
| - uses: golangci/golangci-lint-action@v9 | |
| with: | |
| # Pinned exact (not v2.5). Keep in sync with Makefile | |
| # GOLANGCI_LINT_VERSION. | |
| version: v2.5.0 | |
| test-linux: | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| go: ["1.25", "1.26"] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: ${{ matrix.go }} | |
| cache: true | |
| - run: go vet ./... | |
| - run: go test -race -count=1 ./... | |
| # Apple-container backend lives in runtime/applecontainer and is | |
| # darwin/arm64-only (see build tags). This job builds the Swift | |
| # bridge and runs the Go test suite on macOS so we get coverage of | |
| # cgo compilation, go:embed of libACBridge.dylib, and the | |
| # daemon-free unit tests. Daemon-dependent tests skip cleanly via | |
| # runtimeOrSkip when Apple's `container` apiserver isn't running. | |
| test-darwin: | |
| runs-on: macos-26 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| go: ["1.25", "1.26"] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: ${{ matrix.go }} | |
| cache: true | |
| - name: Select Xcode with Swift 6.2 | |
| # apple/container 0.12.3 declares swift-tools-version 6.2; | |
| # the macos-15 image ships an older Xcode by default. Pick | |
| # the newest installed so SwiftPM can resolve the package. | |
| run: sudo xcode-select -s /Applications/Xcode_latest.app || sudo xcode-select -s "$(ls -d /Applications/Xcode_*.app | sort -V | tail -1)" | |
| - name: Cache SwiftPM artifacts | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| applecontainer-bridge/.build | |
| ~/Library/Caches/org.swift.swiftpm | |
| # Key on Package.resolved so the cache busts when dependency | |
| # versions move. Bump the `v1` prefix to force a full miss | |
| # after a toolchain change that breaks artifact compat. | |
| key: swiftpm-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('applecontainer-bridge/Package.resolved') }} | |
| restore-keys: | | |
| swiftpm-v1-${{ runner.os }}-${{ runner.arch }}- | |
| - name: Build Swift bridge | |
| run: make bridge | |
| - run: go vet ./... | |
| - run: go test -race -count=1 ./... | |
| test-integration-linux: | |
| runs-on: ubuntu-latest | |
| needs: [lint, test-linux] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| go: ["1.25", "1.26"] | |
| shard: [1, 2, 3] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: ${{ matrix.go }} | |
| cache: true | |
| - name: Verify Docker available | |
| run: | | |
| docker version | |
| docker compose version | |
| - name: Run integration tests (shard ${{ matrix.shard }}/3) | |
| env: | |
| SHARD_INDEX: ${{ matrix.shard }} | |
| SHARD_TOTAL: 3 | |
| run: | | |
| # Enumerate tests under the integration tag, partition them | |
| # deterministically by sorted-index modulo SHARD_TOTAL, and | |
| # run only this shard's subset. Apple-container tests are | |
| # build-tagged darwin/arm64, so they don't appear here. | |
| set -euo pipefail | |
| tests=$(go test -tags=integration -list '.*' ./test/integration/... \ | |
| | grep -E '^Test' | sort -u) | |
| if [ -z "$tests" ]; then | |
| echo "no integration tests discovered" >&2 | |
| exit 1 | |
| fi | |
| selected=$(echo "$tests" | awk -v s="$SHARD_INDEX" -v t="$SHARD_TOTAL" \ | |
| '{ if ((NR - 1) % t == (s - 1)) print }') | |
| echo "Shard ${SHARD_INDEX}/${SHARD_TOTAL} will run:" | |
| echo "$selected" | |
| pattern="^($(echo "$selected" | paste -sd '|' -))$" | |
| go test -race -count=1 -tags=integration -timeout=15m \ | |
| -run "$pattern" ./test/integration/... | |
| # Real Podman + CRIU checkpoint/restore. The runtime/podman backend and | |
| # the Engine checkpoint/restore + project-orchestrator paths only execute | |
| # against a live Podman socket with CRIU (PODMAN_SOCKET-gated). The | |
| # cross-node test (TestPodmanXNode_*) needs two hosts, so it skips here | |
| # (no DCCKPT_XNODE_DIR) — run it on two machines by hand. | |
| # | |
| # Why a container: the hosted runner's apt podman is unusable (24.04 has | |
| # no criu; 22.04's podman 3.4.4 can't checkpoint and predates the libpod | |
| # v5 API). So we build the gated tests on the runner (compile coverage, | |
| # static CGO_ENABLED=0 so they run on Fedora), then run them INSIDE a | |
| # modern-podman container (podman 5.x + crun + criu) that's privileged + | |
| # --cgroupns=host so CRIU can drive the runner's kernel. The script | |
| # smoke-tests an actual checkpoint first and skips green (with the real | |
| # reason) if this runner can't — e.g. nested cgroup-freezer perms — so a | |
| # capable runner runs for real while an incapable one stays green. | |
| test-integration-podman: | |
| runs-on: ubuntu-latest | |
| needs: [lint, test-linux] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: "1.25" | |
| cache: true | |
| - name: Build gated test binaries (static; compile coverage + run in container) | |
| env: | |
| CGO_ENABLED: "0" | |
| run: | | |
| go test -tags=integration -c ./test/integration -o ./int.test | |
| go test -c ./runtime/podman -o ./podman.test | |
| - name: Checkpoint/restore in a modern-podman privileged container | |
| run: | | |
| docker run --rm --privileged --cgroupns=host \ | |
| --security-opt seccomp=unconfined \ | |
| --security-opt apparmor=unconfined \ | |
| --security-opt label=disable \ | |
| -v "$PWD":/w -w /w \ | |
| quay.io/podman/stable bash /w/.github/scripts/podman-cr.sh | |
| # Integration tests against a live Apple `container` daemon. | |
| # | |
| # Verified-on-CI status: | |
| # - pkg install : OK on macos-15 and macos-26 | |
| # - system start : OK | |
| # - kernel set : OK | |
| # - builder start : FAILS on both macos-15 and macos-26 with | |
| # "VZErrorDomain Code=2 Virtualization is not available on | |
| # this hardware." | |
| # | |
| # i.e. GitHub-hosted macOS runners do not expose | |
| # Virtualization.framework for Linux guests, regardless of image | |
| # version. Apple's `container` runtime is hardcoded to VZ (no QEMU | |
| # fallback like Colima/Lima), so there is no workaround at the | |
| # workflow level. The only paths to a passing job today are: | |
| # 1. Self-hosted macOS runner with virtualization entitlements | |
| # 2. GH exposing VZ on hosted runners (no announced timeline) | |
| # | |
| # continue-on-error keeps the failure visible without blocking | |
| # merges. Drop it once a real green run is available. | |
| test-integration-darwin: | |
| runs-on: macos-26 | |
| needs: [test-darwin] | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| go: ["1.25", "1.26"] | |
| shard: [1, 2, 3] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-go@v6 | |
| with: | |
| go-version: ${{ matrix.go }} | |
| cache: true | |
| - name: Install apple/container | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # `gh release download` authenticates with GH_TOKEN, dodging | |
| # the strict anonymous rate limit on api.github.com that | |
| # bit us when curling the unauth'd releases endpoint. | |
| gh release download --repo apple/container \ | |
| --pattern '*installer-signed.pkg' \ | |
| --output /tmp/container.pkg | |
| sudo installer -pkg /tmp/container.pkg -target / | |
| container --version | |
| - name: Select Xcode with Swift 6.2 | |
| # apple/container 0.12.3 declares swift-tools-version 6.2; | |
| # the macos-15 image ships an older Xcode by default. Pick | |
| # the newest installed so SwiftPM can resolve the package. | |
| run: sudo xcode-select -s /Applications/Xcode_latest.app || sudo xcode-select -s "$(ls -d /Applications/Xcode_*.app | sort -V | tail -1)" | |
| - name: Cache SwiftPM artifacts | |
| uses: actions/cache@v5 | |
| with: | |
| path: | | |
| applecontainer-bridge/.build | |
| ~/Library/Caches/org.swift.swiftpm | |
| key: swiftpm-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('applecontainer-bridge/Package.resolved') }} | |
| restore-keys: | | |
| swiftpm-v1-${{ runner.os }}-${{ runner.arch }}- | |
| - name: Build Swift bridge | |
| run: make bridge | |
| - name: Start container apiserver | |
| run: | | |
| # --disable-kernel-install skips the interactive kernel prompt | |
| # that would otherwise hang in CI; we install the recommended | |
| # kernel explicitly in the next step. | |
| container system start --disable-kernel-install | |
| container system status | |
| - name: Install default kernel | |
| # The builder (and any container) needs a configured kernel. | |
| # `--recommended` pulls Apple's recommended binary | |
| # non-interactively, which `--disable-kernel-install` skipped. | |
| run: container system kernel set --recommended | |
| - name: Start builder | |
| id: builder | |
| # On GH-hosted macOS runners VZ is unavailable, so this step | |
| # always fails. Mark it continue-on-error so the JOB stays | |
| # green (continue-on-error at job level wouldn't — that only | |
| # affects workflow status, not the per-job check). The next | |
| # step gates on this step's outcome so we don't run the | |
| # integration tests against a missing daemon. | |
| continue-on-error: true | |
| run: container builder start | |
| - name: Run apple-container integration tests (shard ${{ matrix.shard }}/3) | |
| if: steps.builder.outcome == 'success' | |
| env: | |
| SHARD_INDEX: ${{ matrix.shard }} | |
| SHARD_TOTAL: 3 | |
| run: | | |
| # Filter to TestAppleContainer_* — the docker-backed tests | |
| # share the `integration` build tag and would fail without a | |
| # Docker daemon on this runner. Then partition that subset | |
| # across shards. | |
| set -euo pipefail | |
| tests=$(go test -tags=integration -list '^TestAppleContainer_' ./test/integration/... \ | |
| | grep -E '^TestAppleContainer_' | sort -u) | |
| if [ -z "$tests" ]; then | |
| echo "no apple-container integration tests discovered" >&2 | |
| exit 1 | |
| fi | |
| selected=$(echo "$tests" | awk -v s="$SHARD_INDEX" -v t="$SHARD_TOTAL" \ | |
| '{ if ((NR - 1) % t == (s - 1)) print }') | |
| echo "Shard ${SHARD_INDEX}/${SHARD_TOTAL} will run:" | |
| echo "$selected" | |
| if [ -z "$selected" ]; then | |
| echo "shard is empty; nothing to run" | |
| exit 0 | |
| fi | |
| pattern="^($(echo "$selected" | paste -sd '|' -))$" | |
| go test -race -count=1 -tags=integration -timeout=15m \ | |
| -run "$pattern" ./test/integration/... | |
| - name: Stop container services | |
| if: always() | |
| run: | | |
| container builder stop || true | |
| container system stop || true |