Files
cms-plugins/ci/local.sh
T
2026-06-04 19:10:28 +03:00

306 lines
9.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# shellcheck shell=bash
#
# oci-image escape hatch (cluster #196) for a keep-Dockerfile repo.
#
# cms-plugins is an Astro/emdash WEB app: its production image is built by
# `npm ci` + `astro build` against an upstream package-lock.json with an
# apt-installed C toolchain (better-sqlite3 native build). It does NOT
# nixify and cannot be built on emmett/amd64 as Nix, so it stays on
# `docker buildx` instead of the parity-lib nix archetypes.
#
# This script is the single source of build truth: a developer runs it
# locally (default builder, dry-run) and CI runs the EXACT same script with
# a couple of env overrides, so CI and local can't drift (emmett#44).
#
# Builder is parameterized:
# BUILDKIT_ADDR default docker-container://local (dev: local buildkitd)
# CI overrides to the in-cluster native-arch remote, e.g.
# tcp://buildkit-arm64.infra.svc.cluster.local:1234
#
# kotkan (the deploy target) is arm64, so the default target arch is arm64.
# Foreign-arch (an arch != the host) is emulated via qemu binfmt and is
# SLOW — an explicit notice is printed and, for a remote NATIVE-arch
# builder, emulation is skipped. If a foreign arch cannot be built locally
# (no binfmt handler, no remote builder) the blocker is named + exit != 0.
#
# DRY-RUN by default. Pass --publish (or PUBLISH=1) to actually --push.
#
# Tagging contract (PRESERVED verbatim from the old .woodpecker pipeline):
# every PUBLISH pushes three refs onto the one image:
# $VERSION — semver-shaped, one per build, immutable (audit).
# Default 0.1.$CI_PIPELINE_NUMBER (override via VERSION).
# $BRANCH — floating channel pointer Flux's ImagePolicy tracks.
# Retagging it is what rolls staging/production pods.
# $BRANCH-latest — same image; chart image.tag cosmetic fallback.
# Only staging/production have an ImagePolicy, so only those move pods.
#
# Token: $REGISTRY_TOKEN, else `pass <PASS_ENTRY>`, else a named hard fail.
# The token is NEVER echoed and `set -x` is never enabled.
set -euo pipefail
# ---- configuration (override via env) ---------------------------------------
REGISTRY_HOST="${REGISTRY_HOST:-git.oleks.space}"
REGISTRY_OWNER="${REGISTRY_OWNER:-oleks}"
REGISTRY_USER="${REGISTRY_USER:-oleks}"
IMAGE_REPO="${IMAGE_REPO:-cms-plugins}"
PASS_ENTRY="${PASS_ENTRY:-infra/gitea/personal_access_token_packages_rw}"
# Local buildkitd by default; CI overrides to the cluster native-arch remote.
BUILDKIT_ADDR="${BUILDKIT_ADDR:-docker-container://local}"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# ---- argument parsing -------------------------------------------------------
PUBLISH="${PUBLISH:-0}"
TARGET_ARCH=""
usage() {
cat <<'EOF'
Usage: ci/local.sh [--arch <arch>] [--publish]
--arch <arch> Target arch: amd64 | arm64 | s390x (default: arm64,
the deploy target kotkan's arch)
--publish Actually push to the registry (default: DRY-RUN)
Environment:
BUILDKIT_ADDR buildx builder endpoint
(default docker-container://local; CI -> cluster remote)
PUBLISH=1 same as --publish
REGISTRY_TOKEN registry token (else `pass PASS_ENTRY`)
VERSION immutable semver-shaped tag (default 0.1.$CI_PIPELINE_NUMBER)
CI_COMMIT_BRANCH branch -> floating channel tag (default current git branch)
Examples:
ci/local.sh # dry-run, arm64, both via local buildkit
ci/local.sh --arch amd64 # dry-run, foreign arch via qemu (slow)
PUBLISH=1 ci/local.sh # real push of the arm64 image (3 tags)
EOF
}
while [ "$#" -gt 0 ]; do
case "$1" in
--arch)
TARGET_ARCH="${2:-}"
shift 2
;;
--publish)
PUBLISH=1
shift
;;
-h | --help)
usage
exit 0
;;
*)
echo "ERROR: unknown argument: $1" >&2
usage >&2
exit 2
;;
esac
done
# ---- arch resolution --------------------------------------------------------
host_arch() {
case "$(uname -m)" in
x86_64) echo amd64 ;;
aarch64 | arm64) echo arm64 ;;
s390x) echo s390x ;;
*) echo "unknown" ;;
esac
}
HOST_ARCH="$(host_arch)"
# kotkan is arm64; the image is only ever deployed there.
[ -n "$TARGET_ARCH" ] || TARGET_ARCH="arm64"
case "$TARGET_ARCH" in
amd64 | arm64 | s390x) ;;
*)
echo "ERROR: unsupported --arch '$TARGET_ARCH' (want amd64|arm64|s390x)" >&2
exit 2
;;
esac
# ---- version + branch derivation --------------------------------------------
# $VERSION -> $VERSION env -> 0.1.$CI_PIPELINE_NUMBER -> 0.1.0-dev.
derive_version() {
if [ -n "${VERSION:-}" ]; then
printf '%s' "$VERSION"
return
fi
if [ -n "${CI_PIPELINE_NUMBER:-}" ]; then
printf '0.1.%s' "$CI_PIPELINE_NUMBER"
return
fi
printf '0.1.0-dev'
}
# $CI_COMMIT_BRANCH -> current git branch -> "dev".
derive_branch() {
if [ -n "${CI_COMMIT_BRANCH:-}" ]; then
printf '%s' "$CI_COMMIT_BRANCH"
return
fi
local b
if b="$(git -C "$REPO_ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null)" &&
[ -n "$b" ] && [ "$b" != "HEAD" ]; then
printf '%s' "$b"
return
fi
printf 'dev'
}
IMAGE_VERSION="$(derive_version)"
BRANCH="$(derive_branch)"
# ---- token resolution (never printed) ---------------------------------------
resolve_token() {
if [ -n "${REGISTRY_TOKEN:-}" ]; then
printf '%s' "$REGISTRY_TOKEN"
return 0
fi
if command -v pass >/dev/null 2>&1; then
local t
if t="$(pass "$PASS_ENTRY" 2>/dev/null)" && [ -n "$t" ]; then
printf '%s' "$t"
return 0
fi
fi
echo "BLOCKER: no registry token." >&2
echo " Set \$REGISTRY_TOKEN or store it at: pass $PASS_ENTRY" >&2
return 1
}
# ---- foreign-arch (qemu binfmt) gate ----------------------------------------
binfmt_handler_present() {
local q="$1"
[ -e "/proc/sys/fs/binfmt_misc/qemu-${q}" ]
}
qemu_name_for() {
case "$1" in
amd64) echo "x86_64" ;;
arm64) echo "aarch64" ;;
s390x) echo "s390x" ;;
esac
}
# Is BUILDKIT_ADDR the matching per-arch cluster builder (native, no qemu)?
builder_is_native_remote() {
case "$BUILDKIT_ADDR" in
tcp://*"buildkit-"*"${TARGET_ARCH}"*) return 0 ;;
*) return 1 ;;
esac
}
emulation_gate() {
if [ "$TARGET_ARCH" = "$HOST_ARCH" ]; then
return 0
fi
if builder_is_native_remote; then
echo "NOTE: target $TARGET_ARCH served NATIVELY by remote builder ($BUILDKIT_ADDR)."
return 0
fi
local q
q="$(qemu_name_for "$TARGET_ARCH")"
echo "================================================================"
echo " SLOW / ACCELERATOR NOTICE"
echo " Target arch '$TARGET_ARCH' != host '$HOST_ARCH'."
echo " This build runs under qemu-${q} binfmt emulation and will be"
echo " MUCH slower than a native build. For speed, point BUILDKIT_ADDR"
echo " at a native remote builder, e.g.:"
echo " BUILDKIT_ADDR=tcp://buildkit-rootless-${TARGET_ARCH}.infra.svc.cluster.local:1234"
echo "================================================================"
if ! binfmt_handler_present "$q"; then
echo "BLOCKER: cannot build '$TARGET_ARCH' locally — no qemu-${q} binfmt" >&2
echo " handler is registered with this kernel." >&2
echo " Fix one of:" >&2
echo " * register emulation: docker run --privileged --rm tonistiigi/binfmt --install ${TARGET_ARCH}" >&2
echo " * or use a native remote builder via BUILDKIT_ADDR (see notice above)." >&2
return 1
fi
return 0
}
# ---- buildx builder bootstrap ----------------------------------------------
BUILDER_NAME="cc-local-${TARGET_ARCH}"
ensure_builder() {
if docker buildx inspect "$BUILDER_NAME" >/dev/null 2>&1; then
return 0
fi
case "$BUILDKIT_ADDR" in
docker-container://*)
docker buildx create --name "$BUILDER_NAME" \
--driver docker-container >/dev/null
;;
tcp://*)
local endpoint="${BUILDKIT_ADDR#tcp://}"
docker buildx create --name "$BUILDER_NAME" \
--driver remote "tcp://${endpoint}" >/dev/null
;;
*)
echo "ERROR: unrecognized BUILDKIT_ADDR scheme: $BUILDKIT_ADDR" >&2
echo " want docker-container://... or tcp://host:port" >&2
exit 2
;;
esac
}
# ---- build / publish --------------------------------------------------------
# Builds the single repo-root Dockerfile and (on --publish) pushes the three
# contract tags onto it in one buildx invocation.
build_image() {
local image="${REGISTRY_HOST}/${REGISTRY_OWNER}/${IMAGE_REPO}"
local -a args=(
docker buildx build
--builder "$BUILDER_NAME"
--platform "linux/${TARGET_ARCH}"
-f "${REPO_ROOT}/Dockerfile"
-t "${image}:${IMAGE_VERSION}"
-t "${image}:${BRANCH}"
-t "${image}:${BRANCH}-latest"
)
if [ "$PUBLISH" = "1" ]; then
args+=(--push)
echo ">> PUBLISH $image tags: ${IMAGE_VERSION}, ${BRANCH}, ${BRANCH}-latest"
else
echo ">> DRY-RUN $image tags: ${IMAGE_VERSION}, ${BRANCH}, ${BRANCH}-latest (build-only; pass --publish to push)"
fi
args+=("$REPO_ROOT")
"${args[@]}"
}
# ---- main -------------------------------------------------------------------
main() {
echo "repo : ${REGISTRY_HOST}/${REGISTRY_OWNER}/${IMAGE_REPO}"
echo "host arch : ${HOST_ARCH}"
echo "target arch : ${TARGET_ARCH}"
echo "branch : ${BRANCH}"
echo "version tag : ${IMAGE_VERSION}"
echo "builder : ${BUILDKIT_ADDR}"
echo "mode : $([ "$PUBLISH" = "1" ] && echo PUBLISH || echo DRY-RUN)"
emulation_gate
if [ "$PUBLISH" = "1" ]; then
local token
token="$(resolve_token)"
echo "$token" | docker login "$REGISTRY_HOST" \
-u "$REGISTRY_USER" --password-stdin >/dev/null
unset token
fi
ensure_builder
build_image
echo "done."
}
main