306 lines
9.3 KiB
Bash
Executable File
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
|