Add ci-fleet-publish plugin image + multi-arch quartet flow (oleks/ci-scripts#14, #15)
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
# GENERATED by oleks/ci-scripts — do not hand-edit.
|
||||||
|
# Source manifest: manifests/ci-fleet-publish.yaml
|
||||||
|
# Regenerate: python3 generator/render.py manifests/ci-fleet-publish.yaml --out <repo>/.woodpecker
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
ARCH:
|
||||||
|
- amd64
|
||||||
|
- arm64
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
ref: "refs/tags/v*"
|
||||||
|
|
||||||
|
labels:
|
||||||
|
arch: ${ARCH}
|
||||||
|
|
||||||
|
# Thin wrapper (cluster #196, emmett#44): CI runs the SAME ci/local.sh a
|
||||||
|
# developer runs, so CI and local can't drift. CI-specific bits are env
|
||||||
|
# overrides only: BUILDKIT_ADDR -> the in-cluster NATIVE per-arch buildkit
|
||||||
|
# (no QEMU), PUBLISH=1 -> actually push. Output is unchanged: a per-arch
|
||||||
|
# :<version>-<arch> image, later merged into :<version>+:latest by manifest.yaml.
|
||||||
|
steps:
|
||||||
|
- name: build-and-push
|
||||||
|
image: git.oleks.space/oleks/nix-ci:latest
|
||||||
|
environment:
|
||||||
|
REGISTRY_TOKEN:
|
||||||
|
from_secret: registry_token
|
||||||
|
BUILDKIT_ADDR: "tcp://buildkit-${ARCH}.infra.svc.cluster.local:1234"
|
||||||
|
PUBLISH: "1"
|
||||||
|
commands:
|
||||||
|
- echo "▸ arch=$(uname -m)"
|
||||||
|
- ci/local.sh --arch "${ARCH}"
|
||||||
|
backend_options:
|
||||||
|
kubernetes:
|
||||||
|
labels:
|
||||||
|
commit-tag: "${CI_COMMIT_TAG}"
|
||||||
|
pipeline-number: "${CI_PIPELINE_NUMBER}"
|
||||||
|
build-arch: "${ARCH}"
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# GENERATED by oleks/ci-scripts — do not hand-edit.
|
||||||
|
# Source manifest: manifests/ci-fleet-publish.yaml
|
||||||
|
# Regenerate: python3 generator/render.py manifests/ci-fleet-publish.yaml --out <repo>/.woodpecker
|
||||||
|
|
||||||
|
when:
|
||||||
|
- event: tag
|
||||||
|
ref: "refs/tags/v*"
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
|
||||||
|
# oleks/ci-scripts#9: run even if a per-arch matrix leg failed. Without
|
||||||
|
# `runs_on`, Woodpecker skips a dependent workflow outright once any leg of
|
||||||
|
# `build` fails, so a lone arm64 success would silently produce no image at
|
||||||
|
# all. `ci/local.sh --manifest` itself must probe the registry for which
|
||||||
|
# per-arch tags actually landed (not assume a fixed arch list, and not rely
|
||||||
|
# on step/pipeline success status) and build the manifest from whatever
|
||||||
|
# exists — a partial manifest beats none.
|
||||||
|
runs_on:
|
||||||
|
- success
|
||||||
|
- failure
|
||||||
|
|
||||||
|
# Thin wrapper (cluster #196, emmett#44): the SAME ci/local.sh composes the
|
||||||
|
# per-arch tags into the multi-arch :<version> + :latest manifest list.
|
||||||
|
# PUBLISH=1 is the only CI-specific override (local default is dry-run).
|
||||||
|
steps:
|
||||||
|
- name: manifest
|
||||||
|
image: git.oleks.space/oleks/nix-ci:latest
|
||||||
|
environment:
|
||||||
|
REGISTRY_TOKEN:
|
||||||
|
from_secret: registry_token
|
||||||
|
PUBLISH: "1"
|
||||||
|
commands:
|
||||||
|
- echo "▸ arch=$(uname -m)"
|
||||||
|
- ci/local.sh --manifest
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
# ci-fleet-publish plugin image (oleks/ci-scripts#14).
|
||||||
|
#
|
||||||
|
# Base: the existing nixos-ci toolchain image (nix, docker-buildx,
|
||||||
|
# attic-client, nixos-ci-entrypoint already baked in — see
|
||||||
|
# ~/projects/nix-customs/nixos-ci). This image adds only the plugin
|
||||||
|
# entrypoint on top.
|
||||||
|
FROM git.oleks.space/oleks/nix-ci:latest
|
||||||
|
|
||||||
|
COPY entrypoint.sh /usr/local/bin/ci-fleet-publish-entrypoint
|
||||||
|
RUN chmod +x /usr/local/bin/ci-fleet-publish-entrypoint
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/ci-fleet-publish-entrypoint"]
|
||||||
Executable
+101
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Dev-parity build script for the ci-fleet-publish plugin image itself
|
||||||
|
# (oleks/ci-scripts#14, #15). CI runs this SAME script (cluster #196,
|
||||||
|
# emmett#44 convention) so CI and local can't drift; the only CI-specific
|
||||||
|
# overrides are BUILDKIT_ADDR (native per-arch remote buildkit) and
|
||||||
|
# PUBLISH=1 (local default is dry-run / --load).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
IMAGE="git.oleks.space/oleks/ci-fleet-publish"
|
||||||
|
PUBLISH="${PUBLISH:-0}"
|
||||||
|
VERSION="${CI_COMMIT_TAG:-dev}"
|
||||||
|
ARCHES=(amd64 arm64)
|
||||||
|
|
||||||
|
echo "▸ arch=$(uname -m)"
|
||||||
|
|
||||||
|
registry_login() {
|
||||||
|
if [ -n "${REGISTRY_TOKEN:-}" ]; then
|
||||||
|
echo "${REGISTRY_TOKEN}" | docker login git.oleks.space -u oleks --password-stdin
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_builder() {
|
||||||
|
if [ -n "${BUILDKIT_ADDR:-}" ]; then
|
||||||
|
if ! docker buildx inspect ci-fleet-publish >/dev/null 2>&1; then
|
||||||
|
docker buildx create --name ci-fleet-publish --driver remote "${BUILDKIT_ADDR}"
|
||||||
|
fi
|
||||||
|
docker buildx use ci-fleet-publish
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
echo "usage: $0 --arch ARCH | --manifest" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mode=""
|
||||||
|
arch=""
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--arch)
|
||||||
|
arch="$2"
|
||||||
|
mode="build"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--manifest)
|
||||||
|
mode="manifest"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
case "${mode}" in
|
||||||
|
build)
|
||||||
|
: "${arch:?--arch required}"
|
||||||
|
registry_login
|
||||||
|
ensure_builder
|
||||||
|
tag="${VERSION}-${arch}"
|
||||||
|
push_flag="--load"
|
||||||
|
if [ "${PUBLISH}" = "1" ]; then
|
||||||
|
push_flag="--push"
|
||||||
|
fi
|
||||||
|
echo "▸ building ${IMAGE}:${tag} for linux/${arch}"
|
||||||
|
docker buildx build --platform "linux/${arch}" "${push_flag}" -t "${IMAGE}:${tag}" .
|
||||||
|
;;
|
||||||
|
manifest)
|
||||||
|
registry_login
|
||||||
|
major="${VERSION%%.*}"
|
||||||
|
dest_tags=("${VERSION}" "${major}" "latest")
|
||||||
|
found=()
|
||||||
|
for a in "${ARCHES[@]}"; do
|
||||||
|
ref="${IMAGE}:${VERSION}-${a}"
|
||||||
|
echo "▸ probing ${ref}"
|
||||||
|
if DOCKER_CLI_EXPERIMENTAL=enabled docker manifest inspect "${ref}" >/dev/null 2>&1; then
|
||||||
|
echo "▸ found ${ref}"
|
||||||
|
found+=("${ref}")
|
||||||
|
else
|
||||||
|
echo "▸ missing ${ref} (oleks/ci-scripts#9: skip this arch, not the whole manifest)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "${#found[@]}" -eq 0 ]; then
|
||||||
|
echo "no per-arch tags landed for ${IMAGE}:${VERSION}; failing manifest step" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "${PUBLISH}" != "1" ]; then
|
||||||
|
echo "▸ dry-run: would create manifest ${dest_tags[*]} from ${found[*]}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
for dest in "${dest_tags[@]}"; do
|
||||||
|
dest_ref="${IMAGE}:${dest}"
|
||||||
|
echo "▸ creating manifest ${dest_ref} from ${#found[@]}/${#ARCHES[@]} arches"
|
||||||
|
docker manifest create "${dest_ref}" "${found[@]}"
|
||||||
|
docker manifest push "${dest_ref}"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
esac
|
||||||
Executable
+205
@@ -0,0 +1,205 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# ci-fleet-publish entrypoint (oleks/ci-scripts#14, #15).
|
||||||
|
#
|
||||||
|
# Woodpecker plugin entrypoint: reads PLUGIN_* settings (Woodpecker maps a
|
||||||
|
# step's `settings:` map onto PLUGIN_<UPPER_KEY> env vars) and PLUGIN_FLOW
|
||||||
|
# selects the behavior:
|
||||||
|
#
|
||||||
|
# publish - (default) run PLUGIN_RUN after the shared setup
|
||||||
|
# (arch banner, resolv.conf/nix.conf shims, attic
|
||||||
|
# cache) — the single-step shape from the wiki's thin
|
||||||
|
# pipeline contract.
|
||||||
|
# buildx-build - buildx build+push a single per-arch tag for the
|
||||||
|
# current node's arch (one Woodpecker matrix leg per
|
||||||
|
# arch) — half of the "buildx-multiarch" quartet shape.
|
||||||
|
# buildx-manifest - probe the registry for which per-arch tags actually
|
||||||
|
# landed and create a manifest from whatever exists
|
||||||
|
# (oleks/ci-scripts#9: fail only if NONE landed, never
|
||||||
|
# skip on partial success) — the other half.
|
||||||
|
#
|
||||||
|
# Deviation from Spec/Fleet Publish Plugin System (oleks/ci-scripts#15):
|
||||||
|
# the spec names one flow value `buildx-multiarch` for the whole quartet
|
||||||
|
# shape, but a single flow value can't distinguish "build my arch's tag"
|
||||||
|
# from "consolidate the manifest" — those are two different Woodpecker
|
||||||
|
# steps (build matrix + a `depends_on: build, runs_on: [success, failure]`
|
||||||
|
# manifest step) using the SAME plugin image. Split into `buildx-build` /
|
||||||
|
# `buildx-manifest` instead; wiki updated to match.
|
||||||
|
#
|
||||||
|
# See wiki Spec/Modern-CI-Target and Spec/Fleet Publish Plugin System.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "▸ arch=$(uname -m)"
|
||||||
|
|
||||||
|
: "${PLUGIN_FLOW:=publish}"
|
||||||
|
|
||||||
|
# --- resolv.conf shim (s390x cross-build DNS fix, oleks/ci-scripts#4) ---
|
||||||
|
if [ -n "${PLUGIN_NAMESERVER:-}" ]; then
|
||||||
|
echo "nameserver ${PLUGIN_NAMESERVER}" >/etc/resolv.conf
|
||||||
|
echo "options ndots:1" >>/etc/resolv.conf
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- nix.conf shims ---
|
||||||
|
if [ "${PLUGIN_MAX_JOBS:-}" = "true" ]; then
|
||||||
|
echo "max-jobs = 1" >>/etc/nix/nix.conf
|
||||||
|
fi
|
||||||
|
if [ -n "${PLUGIN_CORES:-}" ]; then
|
||||||
|
echo "cores = ${PLUGIN_CORES}" >>/etc/nix/nix.conf
|
||||||
|
fi
|
||||||
|
echo "sandbox = false" >>/etc/nix/nix.conf
|
||||||
|
|
||||||
|
normalize_arch() {
|
||||||
|
case "$1" in
|
||||||
|
x86_64) echo "amd64" ;;
|
||||||
|
aarch64) echo "arm64" ;;
|
||||||
|
*) echo "$1" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
render_tag() {
|
||||||
|
# render_tag SCHEME VERSION ARCH
|
||||||
|
local scheme="$1" version="$2" arch="$3"
|
||||||
|
scheme="${scheme//\{version\}/$version}"
|
||||||
|
scheme="${scheme//\{arch\}/$arch}"
|
||||||
|
echo "$scheme"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- attic watch-store lifecycle: explicit non-masking shutdown, never `|| true` ---
|
||||||
|
ATTIC_PID=""
|
||||||
|
|
||||||
|
attic_start() {
|
||||||
|
if [ "${PLUGIN_CACHE:-true}" != "true" ] || [ -z "${ATTIC_TOKEN:-}" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local server="${PLUGIN_ATTIC_SERVER:-https://nix-cache-upload.oleks.space}"
|
||||||
|
local cache="${PLUGIN_ATTIC_CACHE:-attic-infra-cache-k3s-1}"
|
||||||
|
attic login ci "$server" "$ATTIC_TOKEN"
|
||||||
|
attic watch-store "$cache" &
|
||||||
|
ATTIC_PID=$!
|
||||||
|
sleep 2
|
||||||
|
echo "▸ attic watch-store started for ${cache} (pid ${ATTIC_PID})"
|
||||||
|
}
|
||||||
|
|
||||||
|
attic_stop() {
|
||||||
|
# attic_stop RUN_STATUS — folds a non-zero shutdown into the exit code
|
||||||
|
# only if the run itself succeeded (a failing run's status always wins).
|
||||||
|
local run_status="$1"
|
||||||
|
if [ -z "$ATTIC_PID" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if ! kill "$ATTIC_PID" 2>/dev/null; then
|
||||||
|
echo "▸ attic watch-store already exited before shutdown" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if wait "$ATTIC_PID"; then
|
||||||
|
echo "▸ attic watch-store exited cleanly after shutdown signal"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local status=$?
|
||||||
|
if [ "$status" -eq 143 ] || [ "$status" -eq 137 ]; then
|
||||||
|
echo "▸ attic watch-store terminated by shutdown signal (exit ${status}, expected)"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "▸ attic watch-store exited non-zero (${status}) after shutdown signal" >&2
|
||||||
|
if [ "$run_status" -eq 0 ]; then
|
||||||
|
return "$status"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
publish_flow() {
|
||||||
|
: "${PLUGIN_RUN:?PLUGIN_RUN is required when PLUGIN_FLOW=publish}"
|
||||||
|
# Delegate netrc/redis setup to nixos-ci-entrypoint, but WITHOUT
|
||||||
|
# ATTIC_TOKEN: this script already owns the attic lifecycle above, so
|
||||||
|
# nixos-ci-entrypoint must not start a second watch-store.
|
||||||
|
env -u ATTIC_TOKEN nixos-ci-entrypoint bash -lc "$PLUGIN_RUN"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildx_build_flow() {
|
||||||
|
: "${PLUGIN_IMAGE_NAME:?PLUGIN_IMAGE_NAME is required for buildx-build flow}"
|
||||||
|
local arch
|
||||||
|
arch=$(normalize_arch "$(uname -m)")
|
||||||
|
# Note: default assigned in a separate statement, not
|
||||||
|
# ${PLUGIN_TAG_SCHEME:-{version}-{arch}} — bash's brace-matching for the
|
||||||
|
# default word gets confused by literal `{...}` braces in the fallback
|
||||||
|
# and silently corrupts the rendered tag.
|
||||||
|
local tag_scheme="{version}-{arch}"
|
||||||
|
if [ -n "${PLUGIN_TAG_SCHEME:-}" ]; then
|
||||||
|
tag_scheme="${PLUGIN_TAG_SCHEME}"
|
||||||
|
fi
|
||||||
|
local version="${CI_COMMIT_TAG:-${PLUGIN_VERSION:-latest}}"
|
||||||
|
local tag
|
||||||
|
tag=$(render_tag "$tag_scheme" "$version" "$arch")
|
||||||
|
local context="${PLUGIN_CONTEXT:-.}"
|
||||||
|
echo "▸ building ${PLUGIN_IMAGE_NAME}:${tag} for linux/${arch} (context ${context})"
|
||||||
|
docker buildx build --push --platform "linux/${arch}" -t "${PLUGIN_IMAGE_NAME}:${tag}" "${context}"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildx_manifest_flow() {
|
||||||
|
: "${PLUGIN_IMAGE_NAME:?PLUGIN_IMAGE_NAME is required for buildx-manifest flow}"
|
||||||
|
: "${PLUGIN_PLATFORMS:?PLUGIN_PLATFORMS is required for buildx-manifest flow (comma-separated arches to probe)}"
|
||||||
|
local tag_scheme="{version}-{arch}"
|
||||||
|
if [ -n "${PLUGIN_TAG_SCHEME:-}" ]; then
|
||||||
|
tag_scheme="${PLUGIN_TAG_SCHEME}"
|
||||||
|
fi
|
||||||
|
local version="${CI_COMMIT_TAG:-${PLUGIN_VERSION:-latest}}"
|
||||||
|
local dest_tags="${PLUGIN_DEST_TAGS:-${version},latest}"
|
||||||
|
|
||||||
|
IFS=',' read -ra platforms <<<"$PLUGIN_PLATFORMS"
|
||||||
|
local found=()
|
||||||
|
local arch tag ref
|
||||||
|
for arch in "${platforms[@]}"; do
|
||||||
|
arch="$(echo "$arch" | xargs)"
|
||||||
|
tag=$(render_tag "$tag_scheme" "$version" "$arch")
|
||||||
|
ref="${PLUGIN_IMAGE_NAME}:${tag}"
|
||||||
|
echo "▸ probing ${ref}"
|
||||||
|
if DOCKER_CLI_EXPERIMENTAL=enabled docker manifest inspect "$ref" >/dev/null 2>&1; then
|
||||||
|
echo "▸ found ${ref}"
|
||||||
|
found+=("$ref")
|
||||||
|
else
|
||||||
|
echo "▸ missing ${ref} (oleks/ci-scripts#9: skipping this arch, not failing the manifest)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "${#found[@]}" -eq 0 ]; then
|
||||||
|
echo "no per-arch tags landed for ${PLUGIN_IMAGE_NAME}:${version}; failing manifest step" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local dests=()
|
||||||
|
IFS=',' read -ra dests <<<"$dest_tags"
|
||||||
|
local dest dest_ref
|
||||||
|
for dest in "${dests[@]}"; do
|
||||||
|
dest="$(echo "$dest" | xargs)"
|
||||||
|
dest_ref="${PLUGIN_IMAGE_NAME}:${dest}"
|
||||||
|
echo "▸ creating manifest ${dest_ref} from ${#found[@]}/${#platforms[@]} arches"
|
||||||
|
docker manifest create "$dest_ref" "${found[@]}"
|
||||||
|
docker manifest push "$dest_ref"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
attic_start
|
||||||
|
|
||||||
|
run_status=0
|
||||||
|
case "$PLUGIN_FLOW" in
|
||||||
|
publish)
|
||||||
|
publish_flow || run_status=$?
|
||||||
|
;;
|
||||||
|
buildx-build)
|
||||||
|
buildx_build_flow || run_status=$?
|
||||||
|
;;
|
||||||
|
buildx-manifest)
|
||||||
|
buildx_manifest_flow || run_status=$?
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "unknown PLUGIN_FLOW: ${PLUGIN_FLOW} (expected publish|buildx-build|buildx-manifest)" >&2
|
||||||
|
run_status=1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
attic_status=0
|
||||||
|
attic_stop "$run_status" || attic_status=$?
|
||||||
|
if [ "$attic_status" -ne 0 ] && [ "$run_status" -eq 0 ]; then
|
||||||
|
run_status="$attic_status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit "$run_status"
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
repo: ci-fleet-publish
|
||||||
|
kind: matrix
|
||||||
|
image: git.oleks.space/oleks/nix-ci
|
||||||
|
script: ci/local.sh
|
||||||
|
arches: [amd64, arm64]
|
||||||
|
buildkit_prefix: buildkit
|
||||||
Reference in New Issue
Block a user