feat: shared per-archetype parity publish-app builders (v0.1.0)
Implements the shared parity flake-module library so the ~51 parity repos
consume one source of truth instead of hand-inlined publish shells.
- lib.mk{PyPiWheel,S390xNpm,GenericBinary,Nix2Container,GoBinary,Helm}Publish
builders returning stage-<arch>/publish-<arch>/publish-index/publish/
push-staged apps per the corrected emmett#44 standard (build-parity stages to
./.parity-stage with no registry contact; publish dry-runs by default;
publish-index is build-free + fail-closed; :latest is the last digest copy).
- Shared ci/parity-lib.sh: token resolution ($REGISTRY_TOKEN + pass fallback,
never printed), dev-tag guard, version derivation, dry-run gate, preflight.
- pipeline-doctor package/app asserting the parity contract (cluster #193).
Refs cluster #192, #193, #194, emmett#44.
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
# shellcheck shell=bash
|
||||
# shellcheck disable=SC2034 # vars/functions here are consumed by sourcing apps
|
||||
#
|
||||
# parity-lib shared shell helpers (cluster #192, emmett#44).
|
||||
#
|
||||
# This file is the single source of truth for the cross-cutting concerns every
|
||||
# parity publish app shares: token resolution, version derivation, the dev-tag
|
||||
# guard, argument parsing (the dry-run gate), registry preflight and the
|
||||
# on-disk stage directory convention. Each archetype builder in lib/builders.nix
|
||||
# generates a small writeShellApplication that sources THIS file and then does
|
||||
# only its archetype-specific build + push.
|
||||
#
|
||||
# TOKEN HYGIENE: nothing here ever echoes the token. Scripts that source this
|
||||
# MUST run under `set -euo pipefail` only and MUST NOT enable `set -x`.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Conventions (overridable by the sourcing app before calling the helpers).
|
||||
# ---------------------------------------------------------------------------
|
||||
PARITY_PASS_ENTRY="${PARITY_PASS_ENTRY:-infra/gitea/personal_access_token_packages_rw}"
|
||||
PARITY_REGISTRY_HOST="${PARITY_REGISTRY_HOST:-git.oleks.space}"
|
||||
PARITY_REGISTRY_OWNER="${PARITY_REGISTRY_OWNER:-oleks}"
|
||||
# Local on-disk stage. BUILD-parity artifacts land here; push-staged replays them.
|
||||
PARITY_STAGE_DIR="${PARITY_STAGE_DIR:-${PWD}/.parity-stage}"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Token resolution: $REGISTRY_TOKEN -> `pass` fallback -> named hard fail.
|
||||
# Prints the token on stdout so a caller can `tok="$(parity_resolve_token)"`.
|
||||
# The CALLER must capture it and never echo it. This function itself is silent.
|
||||
# ---------------------------------------------------------------------------
|
||||
parity_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 show "$PARITY_PASS_ENTRY" 2>/dev/null || true)" && [ -n "$t" ]; then
|
||||
printf '%s' "$t"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
echo "BLOCKER(empty-token): set \$REGISTRY_TOKEN (CI from_secret) or store '$PARITY_PASS_ENTRY' in pass; refusing to publish without credentials." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Version derivation, identical for CI and local.
|
||||
# Precedence: $VERSION -> strip-v + strip-trailing-increment of $CI_COMMIT_TAG
|
||||
# -> the default passed as $1 (the flake's pinned version).
|
||||
# Tag shape accepted: v<x.y.z> optionally with a -<N> build increment suffix.
|
||||
# Prints the derived version on stdout.
|
||||
# ---------------------------------------------------------------------------
|
||||
parity_derive_version() {
|
||||
local default_version="${1:-}"
|
||||
if [ -n "${VERSION:-}" ]; then
|
||||
printf '%s' "$VERSION"
|
||||
return 0
|
||||
fi
|
||||
if [ -n "${CI_COMMIT_TAG:-}" ]; then
|
||||
printf '%s' "$CI_COMMIT_TAG" | sed 's/^v//; s/-[0-9]*$//'
|
||||
return 0
|
||||
fi
|
||||
if [ -n "$default_version" ]; then
|
||||
printf '%s' "$default_version"
|
||||
return 0
|
||||
fi
|
||||
echo "BLOCKER(no-version): no \$VERSION, no \$CI_COMMIT_TAG and no default version." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Dev-tag guard. Refuse a real (registry-mutating, :latest/release) publish
|
||||
# unless an explicit $VERSION is set OR $CI_COMMIT_TAG looks like a v* tag.
|
||||
# This stops an accidental local `--publish` from clobbering the registry with
|
||||
# the flake's default development version.
|
||||
# ---------------------------------------------------------------------------
|
||||
parity_devtag_guard() {
|
||||
if [ -n "${VERSION:-}" ]; then
|
||||
return 0
|
||||
fi
|
||||
if printf '%s' "${CI_COMMIT_TAG:-}" | grep -Eq '^v[0-9]'; then
|
||||
return 0
|
||||
fi
|
||||
echo "BLOCKER(dev-tag): refusing to publish without an explicit version." >&2
|
||||
echo " Set VERSION=<x.y.z> or push a v* tag (CI_COMMIT_TAG) before publishing." >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Argument gate. Sets the global PARITY_PUBLISH (0 = dry-run default, 1 = push).
|
||||
# Honors $PUBLISH=1, --publish and --dry-run. --help prints usage and exits 0.
|
||||
# Usage: parity_parse_args "<app-description>" "$@"
|
||||
# ---------------------------------------------------------------------------
|
||||
parity_parse_args() {
|
||||
local desc="$1"
|
||||
shift
|
||||
PARITY_PUBLISH="${PUBLISH:-0}"
|
||||
local a
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--publish) PARITY_PUBLISH=1 ;;
|
||||
--dry-run) PARITY_PUBLISH=0 ;;
|
||||
-h | --help)
|
||||
echo "$desc"
|
||||
echo ""
|
||||
echo "Dry-run by default: builds/stages and prints what WOULD be pushed."
|
||||
echo " --publish | PUBLISH=1 actually mutate the registry"
|
||||
echo " --dry-run force dry-run"
|
||||
echo "Env: VERSION=<x.y.z> override version; REGISTRY_TOKEN registry token"
|
||||
echo " (fallback: pass $PARITY_PASS_ENTRY)."
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "error: unknown argument '$a' (try --help)" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Registry preflight: name the blocker if the registry is unreachable BEFORE
|
||||
# attempting any push (CI shares fate with the cluster the registry lives on).
|
||||
# ---------------------------------------------------------------------------
|
||||
parity_registry_preflight() {
|
||||
if ! curl -fsS -o /dev/null --max-time 10 "https://${PARITY_REGISTRY_HOST}/api/v1/version" 2>/dev/null; then
|
||||
echo "BLOCKER(registry-down): https://${PARITY_REGISTRY_HOST} unreachable — re-run when the registry is back; staged artifacts in ${PARITY_STAGE_DIR} are unchanged." >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage directory helpers. The convention: each staged artifact is written to
|
||||
# ${PARITY_STAGE_DIR}/<arch>/ alongside a one-line meta file recording the
|
||||
# version + intended registry coordinates, so push-staged can replay later.
|
||||
# ---------------------------------------------------------------------------
|
||||
parity_stage_path() {
|
||||
# $1 = arch
|
||||
printf '%s/%s' "$PARITY_STAGE_DIR" "$1"
|
||||
}
|
||||
|
||||
parity_stage_reset() {
|
||||
# $1 = arch ; wipes + recreates that arch's stage dir, prints the path.
|
||||
local d
|
||||
d="$(parity_stage_path "$1")"
|
||||
rm -rf "$d"
|
||||
mkdir -p "$d"
|
||||
printf '%s' "$d"
|
||||
}
|
||||
Executable
+131
@@ -0,0 +1,131 @@
|
||||
#!/usr/bin/env bash
|
||||
# pipeline-doctor (cluster #193): assert the parity contract for a repo.
|
||||
#
|
||||
# Given a repo path, it statically checks that the repo follows the corrected
|
||||
# emmett#44 parity standard, and prints the local-equivalent commands a dev can
|
||||
# run. It is read-only: it never touches the registry and never needs a token.
|
||||
#
|
||||
# Usage: pipeline-doctor [<repo-path>] (default: .)
|
||||
# Exit: 0 = all required checks pass; 1 = one or more required checks failed.
|
||||
set -euo pipefail
|
||||
|
||||
REPO="${1:-.}"
|
||||
REPO="$(cd "$REPO" && pwd)"
|
||||
FLAKE="$REPO/flake.nix"
|
||||
|
||||
fail=0
|
||||
pass_n=0
|
||||
note() { printf ' %s %s\n' "$1" "$2"; }
|
||||
ok() {
|
||||
pass_n=$((pass_n + 1))
|
||||
note "PASS" "$1"
|
||||
}
|
||||
bad() {
|
||||
fail=$((fail + 1))
|
||||
note "FAIL" "$1"
|
||||
}
|
||||
warn() { note "WARN" "$1"; }
|
||||
|
||||
echo "pipeline-doctor: $REPO"
|
||||
|
||||
if [ ! -f "$FLAKE" ]; then
|
||||
echo "FAIL: no flake.nix at $REPO" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
flake_txt="$(cat "$FLAKE")"
|
||||
|
||||
# 1. archetype declared (a publish/stage app whose name encodes the arch, or an
|
||||
# explicit archetype marker comment).
|
||||
if printf '%s' "$flake_txt" | grep -Eq 'archetype|stage-[a-z0-9_]+|publish-[a-z0-9_]+'; then
|
||||
ok "archetype declared (stage-*/publish-* app or archetype marker present)"
|
||||
else
|
||||
bad "no archetype: expected a stage-<arch>/publish-<arch> app or 'archetype' marker"
|
||||
fi
|
||||
|
||||
# 2. consumes parity-lib (the shared module).
|
||||
if printf '%s' "$flake_txt" | grep -Eq 'parity-lib|parity\.lib|mk(PyPiWheel|S390xNpm|GenericBinary|Nix2Container|GoBinary|Helm)Publish'; then
|
||||
ok "consumes parity-lib (input or one of the mk*Publish builders)"
|
||||
else
|
||||
bad "does not consume parity-lib: inline the input and use a mk*Publish builder"
|
||||
fi
|
||||
|
||||
# 3. token = $REGISTRY_TOKEN with pass fallback, and never printed.
|
||||
token_src="$flake_txt"
|
||||
if [ -d "$REPO/ci" ]; then
|
||||
token_src="$token_src
|
||||
$(cat "$REPO"/ci/*.sh 2>/dev/null || true)"
|
||||
fi
|
||||
if printf '%s' "$token_src" | grep -Eq 'REGISTRY_TOKEN' &&
|
||||
printf '%s' "$token_src" | grep -Eq 'parity_resolve_token|pass (show )?infra/gitea/personal_access_token_packages_rw'; then
|
||||
ok "token = \$REGISTRY_TOKEN with pass fallback"
|
||||
else
|
||||
bad "token contract missing: need \$REGISTRY_TOKEN + pass fallback (parity_resolve_token)"
|
||||
fi
|
||||
# token never printed: flag an obvious leak. A redacting pipe (sed s/$tok/.../)
|
||||
# is fine, so only flag a bare echo/printf of the token NOT piped to a redactor,
|
||||
# or an enabled `set -x` (which would trace the token). The leak pattern uses a
|
||||
# literal '$' built via a variable so this lib stays single-quote clean (SC2016).
|
||||
dollar='[$]'
|
||||
# Anchor on echo/printf used as a COMMAND (line-leading, after `;`, `|`, `&&`,
|
||||
# `(` or `then/do/else`) so the doctor doesn't flag the regex literals it
|
||||
# carries inside string assignments.
|
||||
leak_pat="(^|[;|&(]|then |do |else )[[:space:]]*(echo|printf)[^|]*${dollar}(REGISTRY_TOKEN|TOKEN|tok|token)([^A-Za-z0-9_]|$)"
|
||||
leak=0
|
||||
if printf '%s' "$token_src" | grep -Eq "^[[:space:]]*set -x([[:space:]]|$)"; then leak=1; fi
|
||||
# Exclude: redactions, stdin-feeds, the doctor's own $token_src var, an escaped
|
||||
# '\$' (a token NAME in a message, not its value), and the sanctioned
|
||||
# `printf '%s' "$TOKEN"` capture idiom (the resolver returns the token on stdout
|
||||
# for a caller to capture — that is the one blessed place a token is emitted).
|
||||
if printf '%s' "$token_src" |
|
||||
grep -E "$leak_pat" |
|
||||
grep -vE "REDACTED|sed |stdin|token_src" |
|
||||
grep -vqE 'printf .%s. "[$](REGISTRY_TOKEN|TOKEN|tok|token)"|\\[$]'; then
|
||||
leak=1
|
||||
fi
|
||||
if [ "$leak" -eq 0 ]; then
|
||||
ok "no obvious token leak (token never bare-echoed; no set -x)"
|
||||
else
|
||||
bad "possible token leak: a token var is echo/printf'd un-redacted, or set -x is enabled"
|
||||
fi
|
||||
|
||||
# 4. dev-tag guard present.
|
||||
if printf '%s' "$token_src" | grep -Eq 'parity_devtag_guard|refusing to publish without an explicit'; then
|
||||
ok "dev-tag guard present"
|
||||
else
|
||||
bad "no dev-tag guard: a default-version --publish could clobber the registry"
|
||||
fi
|
||||
|
||||
# 5. a --dry-run default exists.
|
||||
if printf '%s' "$token_src" | grep -Eq 'parity_parse_args|DRY-RUN|dry-run|PUBLISH:-0|PUBLISH=\$\{PUBLISH:-0\}'; then
|
||||
ok "dry-run default exists (publish mutates only on --publish/PUBLISH=1)"
|
||||
else
|
||||
bad "no dry-run default: publish must NOT mutate the registry by default"
|
||||
fi
|
||||
|
||||
# 6. apps carry meta.description.
|
||||
if printf '%s' "$flake_txt" | grep -Eq 'meta\.description|meta = \{'; then
|
||||
ok "apps carry meta.description"
|
||||
else
|
||||
bad "apps missing meta.description"
|
||||
fi
|
||||
|
||||
# 7. enumerable publish-* naming.
|
||||
pubs="$(printf '%s' "$flake_txt" | grep -oE '(stage|publish)(-[a-z0-9_]+)?' | sort -u || true)"
|
||||
if [ -n "$pubs" ]; then
|
||||
ok "enumerable publish-* / stage-* app naming:"
|
||||
printf '%s\n' "$pubs" | sed 's/^/ /'
|
||||
else
|
||||
bad "no enumerable publish-*/stage-* apps found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Local-equivalent commands (what CI runs is identical):"
|
||||
echo " nix run .#stage-<arch> # BUILD-parity: stage to .parity-stage, no registry"
|
||||
echo " nix run .#publish-<arch> # stage + push that arch (DRY-RUN; add --publish)"
|
||||
echo " nix run .#publish-index # build-free multi-arch assembly from pushed digests"
|
||||
echo " nix run .#publish # all local arches + index; :latest = digest copy of :TAG"
|
||||
echo " nix run .#push-staged # replay .parity-stage to the registry (cluster-was-down)"
|
||||
echo ""
|
||||
echo "Summary: $pass_n passed, $fail failed."
|
||||
[ "$fail" -eq 0 ]
|
||||
Reference in New Issue
Block a user