Skip to content

Build and Push containerd Images #112

Build and Push containerd Images

Build and Push containerd Images #112

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