Files
opensquilla--opensquilla/.github/workflows/docker-image.yml
T
2026-07-13 13:12:33 +08:00

235 lines
8.4 KiB
YAML

name: Container Images
# Builds the multi-arch (linux/amd64 + linux/arm64) gateway image and
# publishes it to GHCR so home-server/NAS users can `docker pull` a release
# instead of building from a source checkout. See docs/docker.md.
#
# Publishing model:
# * Pushing a release tag (v0.5.0rc3, v0.6.0, ...) first publishes the
# immutable ghcr.io/<owner>/<repo>:<tag> image. The workflow verifies its
# amd64/arm64 manifest and container HEALTHCHECK before moving :latest.
# :latest tracks the most recently pushed release tag — including
# prereleases and backports; if a backport repoints it, re-run this
# workflow from the newest tag.
# * workflow_dispatch validates the full multi-arch build without
# publishing; set publish=true to push the result as :edge.
#
# The GITHUB_TOKEN with job-level `packages: write` is the only credential.
# First-time note for maintainers: a new GHCR package is created private —
# flip ghcr.io/<owner>/<repo> to public in the package settings once.
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
publish:
description: Push the built image to GHCR as the edge tag (leave off to only validate the build)
type: boolean
required: false
default: false
permissions:
contents: read
concurrency:
group: container-images-${{ github.ref }}
cancel-in-progress: false
jobs:
build-and-publish:
name: Build multi-arch gateway image
runs-on: ubuntu-latest
# The arm64 leg runs under QEMU emulation. All compiled dependencies
# install from prebuilt aarch64 wheels, so this is emulated pip time,
# not source builds — slow but bounded.
timeout-minutes: 120
permissions:
contents: read
packages: write
steps:
- name: Validate release tag
if: github.event_name == 'push'
run: |
if [[ ! "${GITHUB_REF_NAME}" =~ ^v[0-9]+[.][0-9]+[.][0-9]+.*$ ]]; then
echo "Release tag must look like v0.2.0rc1 or v0.2.0: ${GITHUB_REF_NAME}" >&2
exit 1
fi
- name: Checkout
uses: actions/checkout@v4
with:
lfs: true
persist-credentials: false
- name: Check tag version
if: github.event_name == 'push'
run: |
version="$(python3 - <<'PY'
import tomllib
from pathlib import Path
project = tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))
print(project["project"]["version"])
PY
)"
tag_version="${GITHUB_REF_NAME#v}"
if [[ "${tag_version}" != "${version}" ]]; then
echo "Tag ${GITHUB_REF_NAME} does not match project version ${version}" >&2
exit 1
fi
- name: Hydrate router assets
run: git lfs pull --include="src/opensquilla/squilla_router/models/**"
# The Dockerfile hard-fails on LFS pointer files; failing here first
# gives a clearer error than a mid-build SystemExit.
- name: Verify router assets are hydrated
run: |
python3 - <<'PY'
from pathlib import Path
root = Path("src/opensquilla/squilla_router/models/v4.2_phase3_inference")
required = [
root / "PROVENANCE.md",
root / "artifact_manifest.json",
root / "bge_onnx" / "model.onnx",
root / "features" / "tfidf.pkl",
root / "lgbm_main.bin",
root / "mlp" / "model.onnx",
root / "router.runtime.yaml",
]
for path in required:
assert path.is_file(), f"missing router asset: {path}"
prefix = path.read_bytes()[:80]
assert not prefix.startswith(b"version https://git-lfs.github.com/spec/v1"), (
f"Git LFS pointer file is not hydrated: {path}"
)
PY
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
if: ${{ github.event_name == 'push' || inputs.publish == true }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Release tags are PEP 440 (v0.5.0rc3), not strict semver, so the git
# tag is mapped through as-is instead of via type=semver.
- name: Compute image metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=tag,enable=${{ github.event_name == 'push' }}
type=raw,value=edge,enable=${{ github.event_name == 'workflow_dispatch' }}
flavor: |
latest=false
- name: Build multi-arch image
uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name == 'push' || inputs.publish == true }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Keep the pushed artifact a plain two-entry manifest list; the
# default provenance attestation adds an unknown/unknown entry that
# confuses registry UIs and `docker pull` inspection.
provenance: false
- name: Select pushed image
if: ${{ github.event_name == 'push' || inputs.publish == true }}
id: pushed_image
shell: bash
run: |
if [[ "${GITHUB_EVENT_NAME}" == "push" ]]; then
image_tag="${GITHUB_REF_NAME}"
else
image_tag="edge"
fi
echo "ref=ghcr.io/${GITHUB_REPOSITORY}:${image_tag}" >> "${GITHUB_OUTPUT}"
- name: Verify pushed manifest platforms
if: ${{ github.event_name == 'push' || inputs.publish == true }}
env:
IMAGE_REF: ${{ steps.pushed_image.outputs.ref }}
run: |
python3 - <<'PY'
import json
import os
import subprocess
image_ref = os.environ["IMAGE_REF"]
manifest = json.loads(
subprocess.check_output(
["docker", "buildx", "imagetools", "inspect", image_ref, "--raw"],
text=True,
)
)
platforms = {
f'{item["platform"]["os"]}/{item["platform"]["architecture"]}'
for item in manifest.get("manifests", [])
}
expected = {"linux/amd64", "linux/arm64"}
if platforms != expected:
raise SystemExit(
f"{image_ref} has platforms {sorted(platforms)}; "
f"expected {sorted(expected)}"
)
print(f"Verified {image_ref}: {sorted(platforms)}")
PY
- name: Smoke pushed image HEALTHCHECK
if: ${{ github.event_name == 'push' || inputs.publish == true }}
env:
IMAGE_REF: ${{ steps.pushed_image.outputs.ref }}
shell: bash
run: |
set -euo pipefail
container_id="$(docker run --detach --pull=always "${IMAGE_REF}")"
trap 'docker rm --force "${container_id}" >/dev/null 2>&1 || true' EXIT
deadline=$((SECONDS + 180))
while (( SECONDS < deadline )); do
running="$(docker inspect --format '{{.State.Running}}' "${container_id}")"
health="$(docker inspect \
--format '{{if .State.Health}}{{.State.Health.Status}}{{else}}missing{{end}}' \
"${container_id}")"
echo "Container health: ${health}"
if [[ "${health}" == "healthy" ]]; then
exit 0
fi
if [[ "${running}" != "true" || "${health}" == "unhealthy" || "${health}" == "missing" ]]; then
docker logs "${container_id}"
exit 1
fi
sleep 5
done
echo "Container did not become healthy within 180 seconds" >&2
docker logs "${container_id}"
exit 1
# Promotion happens only after the immutable release image passes both
# remote-manifest verification and its image-defined HEALTHCHECK.
- name: Promote verified release image to latest
if: github.event_name == 'push'
env:
IMAGE_REF: ${{ steps.pushed_image.outputs.ref }}
LATEST_REF: ghcr.io/${{ github.repository }}:latest
run: |
docker buildx imagetools create \
--tag "${LATEST_REF}" \
"${IMAGE_REF}"