diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b44c493..9d10c6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,9 @@ permissions: contents: read concurrency: - group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + # Supersede PR checks, but keep a build for every push to main. + group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: sdk-unit-tests: @@ -112,8 +113,19 @@ jobs: standalone-e2e: name: Standalone E2E + needs: + - sdk-unit-tests + - sdk-quality + - sdk-distributions + - deployment-script-syntax runs-on: ubuntu-22.04 - timeout-minutes: 45 + timeout-minutes: 60 + # Keep testing and publishing the same local image. Serialize main jobs + # through the latest push, including reruns, without replacing queued jobs. + concurrency: + group: ci-standalone-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: false + queue: max steps: - name: Check out repository @@ -146,12 +158,15 @@ jobs: sudo sysctl -w net.bridge.bridge-nf-call-iptables=1 - name: Build all-in-one image + env: + AKERNEL_ENABLE_KATA: "false" + AKERNEL_ENABLE_FIRECRACKER: "false" + AKERNEL_ENABLE_RUNC: "false" run: | - AKERNEL_ENABLE_KATA=false \ - make build \ - IMAGE_REPOSITORY=akernel-ci/all-in-one \ - IMAGE_TAG="${GITHUB_SHA}" \ - RUNTIME_PROFILE=rrt + make build \ + IMAGE_REPOSITORY=akernel-ci/all-in-one \ + IMAGE_TAG="${GITHUB_SHA}" \ + RUNTIME_PROFILE=rrt - name: Verify systemd container identity in the image run: | @@ -159,6 +174,21 @@ jobs: --format '{{json .Config.Env}}' | \ python -c 'import json, sys; assert "container=oci" in json.load(sys.stdin)' + - name: Verify gVisor-only runtime payload + run: | + docker run --rm --entrypoint /bin/bash \ + "akernel-ci/all-in-one:${GITHUB_SHA}" -euc ' + test -x /usr/local/bin/runsc + test ! -e /usr/local/bin/containerd-shim-kata-v2 + test ! -e /opt/kata/runtime-rs/bin/containerd-shim-kata-v2 + test ! -e /usr/local/bin/firecracker + test ! -e /usr/local/bin/virtiofsd + test ! -e /opt/firecracker/vmlinux + test ! -e /opt/firecracker/initrd.img + test ! -e /usr/local/bin/runc + test ! -e /usr/local/bin/runc-shim + ' + - name: Start standalone AKernel run: | IMAGE="akernel-ci/all-in-one:${GITHUB_SHA}" \ @@ -198,7 +228,13 @@ jobs: ) for example in "${examples[@]}"; do echo "=== Running ${example} ===" - timeout 120s python "sdk/python/examples/${example}" + example_timeout=120 + if [[ "${example}" == "dockerfile_launch.py" ]]; then + # This example creates fresh sandboxes across nine sections. + example_timeout=600 + fi + timeout --kill-after=30s "${example_timeout}s" \ + python -u "sdk/python/examples/${example}" done - name: Run runsc checkpoint reload E2E @@ -253,3 +289,36 @@ jobs: else docker rm -f akernel-traefik akernel-node >/dev/null 2>&1 || true fi + + - name: Log in to Docker Hub + if: >- + github.repository == 'inclusionAI/AKernel' && + github.event_name == 'push' && github.ref == 'refs/heads/main' + uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Publish tested all-in-one image + if: >- + github.repository == 'inclusionAI/AKernel' && + github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + GH_TOKEN: ${{ github.token }} + IMAGE_REPOSITORY: akerneldev/all-in-one + run: | + # Jobs can queue out of commit order, and old runs can be rerun. + # Check main before any push while holding the job lock. Publish + # only latest; superseded commits must not create registry tags. + main_sha="$(gh api "repos/${GITHUB_REPOSITORY}/commits/main" --jq .sha)" + if [[ "${main_sha}" != "${GITHUB_SHA}" ]]; then + echo "Skipped latest: main has advanced to ${main_sha}." \ + >> "${GITHUB_STEP_SUMMARY}" + exit 0 + fi + + source_image="akernel-ci/all-in-one:${GITHUB_SHA}" + docker tag "${source_image}" "${IMAGE_REPOSITORY}:latest" + docker push "${IMAGE_REPOSITORY}:latest" + echo "Published ${IMAGE_REPOSITORY}:latest from ${GITHUB_SHA}" \ + >> "${GITHUB_STEP_SUMMARY}" diff --git a/AGENTS.md b/AGENTS.md index d24d22f..7ba109c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -520,6 +520,35 @@ are not part of the default test suite. ## Release +The CI workflow builds the public Linux/amd64 all-in-one image with only +gVisor runsc and the `rrt` runtime profile. It explicitly sets +`AKERNEL_ENABLE_KATA=false`, `AKERNEL_ENABLE_FIRECRACKER=false`, and +`AKERNEL_ENABLE_RUNC=false`, excluding VM payloads and virtiofsd. Source-build +defaults still include Kata and Firecracker for operators who need them. +After SDK checks, distribution validation, deployment syntax checks, and +standalone runsc E2E pass, pushes to `main` in `inclusionAI/AKernel` publish +that tested image only as `akerneldev/all-in-one:latest`. PRs and forks never +publish. Check that the commit is still the current `main` head before any +push; superseded commits and reruns of older commits skip publication +entirely. Do not publish per-commit SHA tags or other historical image tags. +The job checks the image contains runsc and excludes Kata, Firecracker, +virtiofsd, and runc before starting standalone E2E. + +CI runs examples with unbuffered Python output. Ordinary examples have a +120-second limit; `dockerfile_launch.py` gets 600 seconds for its nine +sections. Its core startup script uses the Ubuntu base image's shell and +does not install packages. Keep its RUN, context-transfer, and startup checks +independent of external package mirrors. + +Configure the repository Actions variable `DOCKERHUB_USERNAME` and secret +`DOCKERHUB_TOKEN` with Docker Hub credentials that can push to +`akerneldev/all-in-one`. Keep credentials out of source and logs. Main CI runs +are not canceled by subsequent pushes. The standalone job serializes builds, +tests, and publication using `queue: max` (up to 100 pending jobs), so a slow +or rerun job cannot overwrite a newer published `latest`. Do not remove the +main-head check or the publication lock. A failed check or push leaves the +run failed and can be retried using GitHub Actions' rerun controls. + Python SDK releases use stable `vX.Y.Z` tags or release-candidate `vX.Y.ZrcN` tags. The tag version must match the version in `sdk/python/pyproject.toml`, and the tagged commit must be part of `main`. diff --git a/sdk/python/docs/launch-from-dockerfile.md b/sdk/python/docs/launch-from-dockerfile.md index e0a7dc8..d0a0815 100644 --- a/sdk/python/docs/launch-from-dockerfile.md +++ b/sdk/python/docs/launch-from-dockerfile.md @@ -43,6 +43,8 @@ with Sandbox(dockerfile=DockerfileLaunch(context, run_timeout=300)) as sandbox: See the maintained end-to-end [`examples/dockerfile_launch.py`](../examples/dockerfile_launch.py). +It exercises context transfer and startup with the base image's shell, so +running the example does not require downloading additional packages. ## Precheck and DockerfileLaunch configuration diff --git a/sdk/python/examples/dockerfile_launch.py b/sdk/python/examples/dockerfile_launch.py index 0f559e1..8d5b2f9 100644 --- a/sdk/python/examples/dockerfile_launch.py +++ b/sdk/python/examples/dockerfile_launch.py @@ -19,6 +19,9 @@ sets runtime state. RUN/COPY/ADD execute for every launch, without a snapshot or build cache. +The core startup script uses the base image's shell, so exercising Dockerfile +semantics does not require installing packages from an external apt mirror. + Sections: 1. Core path, ignore filtering, wildcard COPY, modes, and empty directories 2. Root cwd plus ENTRYPOINT + CMD exec-form combination @@ -35,7 +38,12 @@ import tempfile from pathlib import Path -from akernel_sdk import DockerfileLaunch, LocalDockerContext, Sandbox, check_direct_launch +from akernel_sdk import ( + DockerfileLaunch, + LocalDockerContext, + Sandbox, + check_direct_launch, +) def _precheck(context: LocalDockerContext) -> None: @@ -60,11 +68,9 @@ def section_core_path() -> None: docs.mkdir() (docs / "README.md").write_text("visible\n", encoding="utf-8") (docs / "private.txt").write_text("hidden\n", encoding="utf-8") - (context_dir / "app.py").write_text( - "import os\n" - "open('/tmp/app.started', 'w').write(\n" - " f\"whoami={os.environ['WHOAMI']} cwd={os.getcwd()}\"\n" - ")\n", + (context_dir / "app.sh").write_text( + "#!/bin/sh\n" + "printf 'whoami=%s cwd=%s' \"$WHOAMI\" \"$(pwd)\" > /tmp/app.started\n", encoding="utf-8", ) executable = context_dir / "entrypoint.sh" @@ -82,7 +88,6 @@ def section_core_path() -> None: dockerfile = build / "custom.Dockerfile" dockerfile.write_text( """FROM ubuntu:22.04 -RUN apt-get update && apt-get install -y --no-install-recommends python3 RUN useradd -m app ENV WHOAMI=app WORKDIR /srv @@ -96,7 +101,7 @@ def section_core_path() -> None: COPY wild/* /srv/wild/ COPY docs /srv/reincluded-literal/ COPY doc* /srv/reincluded-wildcard/ -CMD ["python3", "/srv/core/app.py"] +CMD ["/bin/sh", "/srv/core/app.sh"] """, encoding="utf-8", ) @@ -131,7 +136,9 @@ def section_core_path() -> None: "/srv/core/tree/nested-empty /srv/core/literal-empty" ) assert modes.exit_code == 0, modes.stderr - assert modes.stdout.splitlines() == ["755", "711", "750", "755"], modes.stdout + assert modes.stdout.splitlines() == ["755", "711", "750", "755"], ( + modes.stdout + ) wildcard = sandbox.commands.run( "test -f /srv/wild/dir2/foo && test ! -e /srv/wild/dir1" ) @@ -264,7 +271,9 @@ def section_auto_start_disabled() -> None: ) _precheck(context) - with Sandbox(dockerfile=DockerfileLaunch(context, auto_start_cmd=False)) as sandbox: + with Sandbox( + dockerfile=DockerfileLaunch(context, auto_start_cmd=False) + ) as sandbox: assert sandbox.startup_command is None absent = sandbox.commands.run("test ! -e /tmp/disabled-start.out") assert absent.exit_code == 0, absent.stderr