#!/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 `, 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 ] [--publish] --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