ci(#192): thin .woodpecker via #196 escape-hatch (emmett#44)
ci/woodpecker/push/container Pipeline was successful
ci/woodpecker/push/container Pipeline was successful
cms-plugins is an Astro/emdash web app whose image is built by npm/astro against an upstream package-lock.json (better-sqlite3 native build) and cannot be expressed as Nix on emmett/amd64, so it stays on docker buildx. Apply the cluster #196 OCI escape-hatch: move all build/tag/registry truth into ci/local.sh, parameterized by BUILDKIT_ADDR (local docker-container default, dry-run; CI overrides to the in-cluster native arm64 remote + PUBLISH=1). CI now runs the same script a developer runs, so CI and local can't drift. The three-tag Flux ImagePolicy contract (0.1.<pipeline>, <branch>, <branch>-latest) and the arm64/kotkan targeting are preserved verbatim; the Dockerfile is unchanged.
This commit is contained in:
+13
-36
@@ -5,6 +5,14 @@
|
||||
# repo → pod rolls), and <branch>-latest (same image; chart image.tag
|
||||
# fallback). Only staging/production have an ImagePolicy, so only those
|
||||
# move pods.
|
||||
#
|
||||
# Thin wrapper (cluster #196, emmett#44): this is an Astro/emdash WEB app
|
||||
# whose image is built by npm/astro against an upstream lockfile and can NOT
|
||||
# be expressed as Nix on emmett/amd64, so it stays on `docker buildx`. CI
|
||||
# runs the SAME ci/local.sh a developer runs — the only CI-specific bits are
|
||||
# env overrides (BUILDKIT_ADDR -> in-cluster native-arch remote, PUBLISH=1).
|
||||
# All the tag/registry semantics live in ci/local.sh so CI and local can't
|
||||
# drift.
|
||||
|
||||
labels:
|
||||
# kotkan (the deploy target) is an arm64 host, so we build natively on
|
||||
@@ -32,16 +40,11 @@ steps:
|
||||
environment:
|
||||
REGISTRY_TOKEN:
|
||||
from_secret: registry_token
|
||||
# In-cluster native arm64 buildkit (kotkan's arch). ci/local.sh treats
|
||||
# this as a native remote builder and skips qemu emulation.
|
||||
BUILDKIT_ADDR: "tcp://buildkit-rootless-arm64.infra.svc.cluster.local:1234"
|
||||
PUBLISH: "1"
|
||||
commands:
|
||||
- BRANCH="$CI_COMMIT_BRANCH"
|
||||
- SHA=$(echo "$CI_COMMIT_SHA" | cut -c1-12)
|
||||
# Semver-shaped immutable tag, one per build. First two components
|
||||
# stay 0.1 (no real semver discipline yet); patch is the pipeline
|
||||
# number, monotonic across the whole repo.
|
||||
- VERSION="0.1.$CI_PIPELINE_NUMBER"
|
||||
- IMAGE="git.oleks.space/oleks/cms-plugins"
|
||||
- 'echo "Building $IMAGE:$VERSION (branch=$BRANCH sha=$SHA linux/arm64)"'
|
||||
|
||||
# Wait for the in-cluster buildkit to be reachable (it can be cold).
|
||||
- |
|
||||
BUILDER_HOST="buildkit-rootless-arm64.infra.svc.cluster.local"
|
||||
@@ -54,33 +57,7 @@ steps:
|
||||
[ "$i" -eq 30 ] && echo "Builder not available" && exit 1
|
||||
sleep 10
|
||||
done
|
||||
|
||||
- echo "$REGISTRY_TOKEN" | docker login git.oleks.space -u oleks --password-stdin
|
||||
- docker buildx create --name cms-plugins-builder --driver remote "tcp://$BUILDER_HOST:$BUILDER_PORT"
|
||||
|
||||
# Tagging scheme — every build pushes three refs:
|
||||
# $VERSION — semver-shaped, one per build, immutable (audit).
|
||||
# $BRANCH — floating channel pointer. THIS is what Flux's
|
||||
# ImagePolicy tracks (filterTags `^staging$` /
|
||||
# `^production$`, digestReflectionPolicy: Always);
|
||||
# retagging it onto the new image is what makes
|
||||
# ImageUpdateAutomation rewrite the pinned digest
|
||||
# in the workloads repo and roll the pod.
|
||||
# $BRANCH-latest — same image, kept only so the chart's cosmetic
|
||||
# `image.tag` fallback (used when image.digest is
|
||||
# unset) resolves to a real ref.
|
||||
# All branches publish all three; only staging/production have an
|
||||
# ImagePolicy, so only those actually move pods.
|
||||
- |
|
||||
TAGS="-t $IMAGE:$VERSION -t $IMAGE:$BRANCH -t $IMAGE:$BRANCH-latest"
|
||||
docker buildx build \
|
||||
--builder cms-plugins-builder \
|
||||
--platform linux/arm64 \
|
||||
$TAGS \
|
||||
--push \
|
||||
.
|
||||
- 'echo "Pushed $IMAGE:$VERSION + floated $IMAGE:$BRANCH and $IMAGE:$BRANCH-latest"'
|
||||
|
||||
- ci/local.sh --arch arm64
|
||||
backend_options:
|
||||
kubernetes:
|
||||
nodeSelector:
|
||||
|
||||
Executable
+305
@@ -0,0 +1,305 @@
|
||||
#!/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-rootless-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
|
||||
Reference in New Issue
Block a user