44d6ba9c8d
render_thin_quartet.py's kind=matrix rendering derived image_name from the
manifest's repo field, which silently diverges from the actually-published
image for xonsh-image (repo "xonsh-image" vs. live image "oleks/xonsh",
confirmed via the package registry). image_name is now a required, explicit
manifest field with no inference; xonsh-image and csi-s3 manifests updated
with their verified values.
entrypoint.sh's buildx-build/buildx-manifest flows always used the raw
CI_COMMIT_TAG, with no way to reproduce ci/local.sh's leading-"v" /
trailing-"-N" tag stripping. Added resolve_version() with two additive,
default-preserving settings: `version` (explicit override, wins over
CI_COMMIT_TAG when set — a no-op today since nothing sets it) and
`tag_strip_v` (opt-in normalization). Default behavior is unchanged: raw
CI_COMMIT_TAG, matching this plugin's own v1.0.x self-build tags and every
other current consumer.
xonsh-image's manifest now opts into tag_strip_v to prove the fix (verified:
resolve_version("v0.22.7") -> "0.22.7", rendered tag "0.22.7-amd64" matches
the live-consumed scheme) but is NOT wired into its actual .woodpecker/ dir
yet — migration is a separate step once both fixes are released.
oleks/ci-scripts#20
252 lines
9.1 KiB
Bash
Executable File
252 lines
9.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# shellcheck disable=SC2250,SC2292,SC2310,SC2312
|
|
# (style-only: brace-optional var refs, [[ ]] preference, set -e-in-||
|
|
# advisories — none affect correctness here; the explicit run_status/
|
|
# attic_status capture pattern is intentional, not an oversight.)
|
|
# 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.
|
|
#
|
|
# Version resolution for buildx-build/buildx-manifest (oleks/ci-scripts#21,
|
|
# see resolve_version() below): defaults to the raw CI_COMMIT_TAG (or an
|
|
# explicit `version:` setting, or "latest") — unchanged from before. A repo
|
|
# whose own build script normalizes tags (strips a leading "v" and a
|
|
# trailing "-<build-number>") can opt in with `tag_strip_v: "true"` to
|
|
# reproduce that exactly; default is off.
|
|
#
|
|
# 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"
|
|
}
|
|
|
|
resolve_version() {
|
|
# resolve_version — the string substituted for {version} in tag_scheme,
|
|
# used by both buildx_build_flow and buildx_manifest_flow so the two
|
|
# stay in lockstep (oleks/ci-scripts#21).
|
|
#
|
|
# Precedence: an explicit `version:` setting (PLUGIN_VERSION) wins over
|
|
# the tag the pipeline triggered on (CI_COMMIT_TAG), else "latest".
|
|
# Nothing currently sets PLUGIN_VERSION (audited across every repo using
|
|
# this plugin), so this is a no-op precedence change for every existing
|
|
# consumer today.
|
|
#
|
|
# `tag_strip_v: "true"` (PLUGIN_TAG_STRIP_V) additionally strips a
|
|
# leading "v" and a trailing "-<build-number>" suffix, e.g. "v0.22.7-4"
|
|
# -> "0.22.7" — the normalization xonsh-image's (and csi-s3's) ci/local.sh
|
|
# already does before tagging. Default is "false": preserves the raw
|
|
# tag verbatim, which is what ci-scripts' own self-build (ci/local.sh,
|
|
# this plugin's own v1.0.x tags) and every other current consumer of
|
|
# this plugin already expect. Opt in per-repo via `tag_strip_v: "true"`
|
|
# in `settings:` — do not flip the default.
|
|
local version="${PLUGIN_VERSION:-${CI_COMMIT_TAG:-latest}}"
|
|
if [ "${PLUGIN_TAG_STRIP_V:-false}" = "true" ]; then
|
|
version="${version#v}"
|
|
version="${version%-[0-9]*}"
|
|
fi
|
|
printf '%s' "$version"
|
|
}
|
|
|
|
# --- 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
|
|
version=$(resolve_version)
|
|
local tag
|
|
tag=$(render_tag "$tag_scheme" "$version" "$arch")
|
|
local context="${PLUGIN_CONTEXT:-.}"
|
|
echo "▸ building ${PLUGIN_IMAGE_NAME}:${tag} for linux/${arch} (context ${context})"
|
|
# --provenance=false --sbom=false: BuildKit >=0.11 attaches attestations
|
|
# by default, which forces even a single-platform build to push as an
|
|
# OCI manifest LIST (index) rather than a plain image manifest. That
|
|
# breaks `docker manifest create` downstream (buildx-manifest flow),
|
|
# which refuses to add a manifest-list as a member. Disable both so the
|
|
# per-arch tag is a plain image manifest.
|
|
docker buildx build --push --provenance=false --sbom=false --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
|
|
version=$(resolve_version)
|
|
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"
|