#!/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_ 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})" # --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="${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"