diff --git a/ci/local.sh b/ci/local.sh index 0a3bb23..bc03a47 100755 --- a/ci/local.sh +++ b/ci/local.sh @@ -1,4 +1,8 @@ #!/usr/bin/env bash +# shellcheck disable=SC2250,SC2292,SC2312 +# (style-only advisories: brace-optional var refs, [[ ]] preference, +# masked-return-value on `uname -m` in the banner echo — none affect +# correctness here.) # 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 @@ -14,88 +18,88 @@ 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 + 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 + 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 + 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 + 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 - ;; +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 diff --git a/entrypoint.sh b/entrypoint.sh index ead21d4..f2db88e 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,4 +1,8 @@ #!/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 @@ -34,172 +38,172 @@ echo "▸ arch=$(uname -m)" # --- 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 + 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 + echo "max-jobs = 1" >>/etc/nix/nix.conf fi if [ -n "${PLUGIN_CORES:-}" ]; then - echo "cores = ${PLUGIN_CORES}" >>/etc/nix/nix.conf + 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 + 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" + # 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})" + 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 + # 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" + : "${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}" + : "${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}" + : "${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 + 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 + 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 + 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 - ;; +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" + run_status="$attic_status" fi exit "$run_status"