Build and Push containerd Images #111
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 containerd Images | |
| on: | |
| schedule: | |
| - cron: "0 17 * * *" | |
| workflow_dispatch: | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 120 | |
| concurrency: | |
| group: containerd-build-${{ github.workflow }}-${{ matrix.system }}-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| packages: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - system: ubuntu | |
| dockerfile: Dockerfile_ubuntu | |
| tag_prefix: ubuntu | |
| - system: debian | |
| dockerfile: Dockerfile_debian | |
| tag_prefix: debian | |
| - system: alpine | |
| dockerfile: Dockerfile_alpine | |
| tag_prefix: alpine | |
| - system: almalinux | |
| dockerfile: Dockerfile_almalinux | |
| tag_prefix: almalinux | |
| - system: rockylinux | |
| dockerfile: Dockerfile_rockylinux | |
| tag_prefix: rockylinux | |
| - system: openeuler | |
| dockerfile: Dockerfile_openeuler | |
| tag_prefix: openeuler | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up QEMU (multi-arch) | |
| uses: docker/setup-qemu-action@v3 | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Login to GitHub Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and export amd64 tar | |
| run: | | |
| docker buildx build \ | |
| --platform linux/amd64 \ | |
| --file dockerfiles/${{ matrix.dockerfile }} \ | |
| --tag spiritlhl/${{ matrix.tag_prefix }}:latest \ | |
| --output type=docker \ | |
| dockerfiles/ | |
| docker save spiritlhl/${{ matrix.tag_prefix }}:latest > /tmp/raw_${{ matrix.tag_prefix }}_amd64.tar | |
| python3 - "${{ matrix.tag_prefix }}" amd64 /tmp/raw_${{ matrix.tag_prefix }}_amd64.tar <<'PYFIX' | |
| import json, tarfile, io, sys, os | |
| system, arch, tar_path = sys.argv[1], sys.argv[2], sys.argv[3] | |
| platform = {"architecture": arch, "os": "linux"} | |
| with open(tar_path, "rb") as f: | |
| raw = f.read() | |
| bio = io.BytesIO(raw) | |
| with tarfile.open(fileobj=bio) as tf: | |
| names = tf.getnames() | |
| if "index.json" in names: | |
| # OCI layout: patch index.json with platform | |
| with tarfile.open(fileobj=io.BytesIO(raw)) as tf: | |
| idx_data = json.loads(tf.extractfile("index.json").read()) | |
| for m in idx_data.get("manifests", []): | |
| if "platform" not in m: | |
| m["platform"] = platform | |
| fixed_idx = json.dumps(idx_data, separators=(",", ":")).encode() + b"\n" | |
| out_bio = io.BytesIO() | |
| with tarfile.open(fileobj=out_bio, mode="w") as out: | |
| with tarfile.open(fileobj=io.BytesIO(raw)) as tf: | |
| for member in tf.getmembers(): | |
| if member.name == "index.json": | |
| member.size = len(fixed_idx) | |
| out.addfile(member, io.BytesIO(fixed_idx)) | |
| else: | |
| out.addfile(member, tf.extractfile(member)) | |
| out_bio.seek(0) | |
| with open(tar_path, "wb") as f: | |
| f.write(out_bio.read()) | |
| print(f"[OK] OCI index.json patched with platform: {platform}") | |
| elif "manifest.json" in names: | |
| # Docker format: inject platform into manifest entries | |
| with tarfile.open(fileobj=io.BytesIO(raw)) as tf: | |
| mf_data = json.loads(tf.extractfile("manifest.json").read()) | |
| for entry in mf_data: | |
| if "platform" not in entry: | |
| entry["platform"] = {"os": "linux", "architecture": arch} | |
| # also ensure Config is present (docker save with single-platform buildx may omit it) | |
| if "Config" not in entry or not entry["Config"]: | |
| # look for a config blob | |
| for name in names: | |
| if name.endswith(".json") and name not in ("manifest.json",): | |
| entry["Config"] = name + ".json" if not name.endswith(".json") else name | |
| break | |
| fixed_mf = json.dumps(mf_data, separators=(",", ":")).encode() + b"\n" | |
| out_bio = io.BytesIO() | |
| with tarfile.open(fileobj=out_bio, mode="w") as out: | |
| with tarfile.open(fileobj=io.BytesIO(raw)) as tf: | |
| for member in tf.getmembers(): | |
| if member.name == "manifest.json": | |
| member.size = len(fixed_mf) | |
| out.addfile(member, io.BytesIO(fixed_mf)) | |
| else: | |
| out.addfile(member, tf.extractfile(member)) | |
| out_bio.seek(0) | |
| with open(tar_path, "wb") as f: | |
| f.write(out_bio.read()) | |
| print(f"[OK] Docker manifest.json patched with platform: {platform}") | |
| else: | |
| print(f"[WARN] No index.json or manifest.json found; leaving tar unchanged") | |
| PYFIX | |
| gzip < /tmp/raw_${{ matrix.tag_prefix }}_amd64.tar > spiritlhl_${{ matrix.tag_prefix }}_amd64.tar.gz | |
| rm -f /tmp/raw_${{ matrix.tag_prefix }}_amd64.tar | |
| echo "amd64 tar size: $(du -sh spiritlhl_${{ matrix.tag_prefix }}_amd64.tar.gz)" | |
| - name: Build and export arm64 tar | |
| run: | | |
| docker buildx build \ | |
| --platform linux/arm64 \ | |
| --file dockerfiles/${{ matrix.dockerfile }} \ | |
| --tag spiritlhl/${{ matrix.tag_prefix }}:latest \ | |
| --output type=docker \ | |
| dockerfiles/ | |
| docker save spiritlhl/${{ matrix.tag_prefix }}:latest > /tmp/raw_${{ matrix.tag_prefix }}_arm64.tar | |
| python3 - "${{ matrix.tag_prefix }}" arm64 /tmp/raw_${{ matrix.tag_prefix }}_arm64.tar <<'PYFIX' | |
| import json, tarfile, io, sys, os | |
| system, arch, tar_path = sys.argv[1], sys.argv[2], sys.argv[3] | |
| platform = {"architecture": arch, "os": "linux"} | |
| with open(tar_path, "rb") as f: | |
| raw = f.read() | |
| bio = io.BytesIO(raw) | |
| with tarfile.open(fileobj=bio) as tf: | |
| names = tf.getnames() | |
| if "index.json" in names: | |
| with tarfile.open(fileobj=io.BytesIO(raw)) as tf: | |
| idx_data = json.loads(tf.extractfile("index.json").read()) | |
| for m in idx_data.get("manifests", []): | |
| if "platform" not in m: | |
| m["platform"] = platform | |
| fixed_idx = json.dumps(idx_data, separators=(",", ":")).encode() + b"\n" | |
| out_bio = io.BytesIO() | |
| with tarfile.open(fileobj=out_bio, mode="w") as out: | |
| with tarfile.open(fileobj=io.BytesIO(raw)) as tf: | |
| for member in tf.getmembers(): | |
| if member.name == "index.json": | |
| member.size = len(fixed_idx) | |
| out.addfile(member, io.BytesIO(fixed_idx)) | |
| else: | |
| out.addfile(member, tf.extractfile(member)) | |
| out_bio.seek(0) | |
| with open(tar_path, "wb") as f: | |
| f.write(out_bio.read()) | |
| print(f"[OK] OCI index.json patched with platform: {platform}") | |
| elif "manifest.json" in names: | |
| with tarfile.open(fileobj=io.BytesIO(raw)) as tf: | |
| mf_data = json.loads(tf.extractfile("manifest.json").read()) | |
| for entry in mf_data: | |
| if "platform" not in entry: | |
| entry["platform"] = {"os": "linux", "architecture": arch} | |
| if "Config" not in entry or not entry["Config"]: | |
| for name in names: | |
| if name.endswith(".json") and name not in ("manifest.json",): | |
| entry["Config"] = name + ".json" if not name.endswith(".json") else name | |
| break | |
| fixed_mf = json.dumps(mf_data, separators=(",", ":")).encode() + b"\n" | |
| out_bio = io.BytesIO() | |
| with tarfile.open(fileobj=out_bio, mode="w") as out: | |
| with tarfile.open(fileobj=io.BytesIO(raw)) as tf: | |
| for member in tf.getmembers(): | |
| if member.name == "manifest.json": | |
| member.size = len(fixed_mf) | |
| out.addfile(member, io.BytesIO(fixed_mf)) | |
| else: | |
| out.addfile(member, tf.extractfile(member)) | |
| out_bio.seek(0) | |
| with open(tar_path, "wb") as f: | |
| f.write(out_bio.read()) | |
| print(f"[OK] Docker manifest.json patched with platform: {platform}") | |
| else: | |
| print(f"[WARN] No index.json or manifest.json found; leaving tar unchanged") | |
| PYFIX | |
| gzip < /tmp/raw_${{ matrix.tag_prefix }}_arm64.tar > spiritlhl_${{ matrix.tag_prefix }}_arm64.tar.gz | |
| rm -f /tmp/raw_${{ matrix.tag_prefix }}_arm64.tar | |
| echo "arm64 tar size: $(du -sh spiritlhl_${{ matrix.tag_prefix }}_arm64.tar.gz)" | |
| - name: Upload tars to GitHub Release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh release view "${{ matrix.tag_prefix }}" >/dev/null 2>&1 || \ | |
| gh release create "${{ matrix.tag_prefix }}" \ | |
| --title "${{ matrix.tag_prefix }}" \ | |
| --notes "containerd image for ${{ matrix.tag_prefix }} (amd64 + arm64)" | |
| gh release upload "${{ matrix.tag_prefix }}" \ | |
| spiritlhl_${{ matrix.tag_prefix }}_amd64.tar.gz \ | |
| spiritlhl_${{ matrix.tag_prefix }}_arm64.tar.gz \ | |
| --clobber | |
| - name: Push multi-arch image to GHCR | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: dockerfiles/ | |
| file: dockerfiles/${{ matrix.dockerfile }} | |
| platforms: linux/amd64,linux/arm64 | |
| push: true | |
| tags: | | |
| ghcr.io/${{ github.repository_owner }}/containerd:${{ matrix.tag_prefix }} | |
| ghcr.io/${{ github.repository_owner }}/${{ matrix.tag_prefix }}:latest | |
| script-smoke: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| concurrency: | |
| group: containerd-script-smoke-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Non-interactive script smoke tests | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| images=(ubuntu:22.04 debian:12 alpine:3.20) | |
| for image in "${images[@]}"; do | |
| docker run --rm -v "$PWD:/repo:ro" -w /repo "$image" sh -c ' | |
| set -e | |
| if command -v apk >/dev/null 2>&1; then | |
| apk add --no-cache bash curl coreutils >/dev/null | |
| else | |
| export DEBIAN_FRONTEND=noninteractive | |
| apt-get update >/dev/null | |
| apt-get install -y --no-install-recommends bash curl ca-certificates >/dev/null | |
| fi | |
| tmp_repo=$(mktemp -d) | |
| cp -a /repo/. "$tmp_repo/" | |
| cd "$tmp_repo" | |
| export noninteractive=true | |
| bash -n containerdinstall.sh | |
| bash -n containerduninstall.sh | |
| bash -n scripts/create_containerd.sh | |
| bash -n scripts/onecontainerd.sh | |
| bash -n scripts/containerd_manage.sh | |
| bash -n scripts/ssh_bash.sh | |
| sh -n scripts/ssh_sh.sh | |
| bash scripts/create_containerd.sh --help >/tmp/create-help | |
| bash scripts/containerd_manage.sh --help >/tmp/manage-help | |
| tmpbin=$(mktemp -d) | |
| cat > "$tmpbin/nerdctl" <<'"'"'NERDCTL'"'"' | |
| #!/bin/sh | |
| case "${1:-}" in | |
| image) | |
| [ "${2:-}" = "inspect" ] && exit 1 | |
| ;; | |
| pull|tag|cp|exec|commit|export|stats) | |
| exit 0 | |
| ;; | |
| run) | |
| printf "%s\n" "fake-container-id" | |
| exit 0 | |
| ;; | |
| ps|images) | |
| exit 0 | |
| ;; | |
| esac | |
| exit 0 | |
| NERDCTL | |
| chmod +x "$tmpbin/nerdctl" | |
| cat > "$tmpbin/curl" <<'"'"'FAKECURL'"'"' | |
| #!/bin/sh | |
| printf "%s\n" "127.0.0.1" | |
| exit 0 | |
| FAKECURL | |
| chmod +x "$tmpbin/curl" | |
| cat > "$tmpbin/noop" <<'"'"'NOOP'"'"' | |
| #!/bin/sh | |
| exit 0 | |
| NOOP | |
| chmod +x "$tmpbin/noop" | |
| mkdir -p /usr/local/bin | |
| ln -sf "$tmpbin/nerdctl" /usr/local/bin/nerdctl | |
| ln -sf "$tmpbin/curl" /usr/local/bin/curl | |
| for cmd in iptables ip6tables iptables-save ip6tables-save nft systemctl service rc-update rc-service netfilter-persistent; do | |
| ln -s "$tmpbin/noop" "$tmpbin/$cmd" | |
| ln -sf "$tmpbin/noop" "/usr/local/bin/$cmd" | |
| done | |
| PATH="$tmpbin:$PATH" WITHOUTCDN=TRUE bash scripts/onecontainerd.sh ctSmoke 0.5 64 "pass'\''word" 25001 35001 35002 n debian 0 | |
| rm -f ctSmoke | |
| PATH="$tmpbin:$PATH" WITHOUTCDN=TRUE bash scripts/create_containerd.sh --noninteractive --count 1 --memory 64 --cpu 0.5 --disk 0 --system alpine --ipv6 n | |
| rm -f /root/ct[0-9]* /root/ctlog | |
| PATH="$tmpbin:$PATH" WITHOUTCDN=TRUE bash scripts/containerd_manage.sh version-check debian >/tmp/version-check | |
| ' | |
| done |