feat(parity): gate-ready pipeline-doctor + OCI verify-digest + stage/push-staged audit
pipeline-doctor (#191/#193): add --strict (fail on WARN) so a .woodpecker.yaml step or pre-receive hook can gate on exit code; add documented ci/local.sh escape-hatch (#196); fix false-negative — token/dev-tag/dry-run/meta contracts are guaranteed by parity-lib for a consumer, so consumers PASS by delegation instead of being penalized for not re-implementing them inline. Self-check and numpy-s390x both pass 9/9. mkNix2ContainerPublish (#195): add verify-digest app that builds each local arch image and prints its OCI manifest digest (no registry contact), formalizing the content-addressed manifest digest as the parity contract. reproducible=false is kept deliberately (non-reproducible layer deps); digest-as-contract is the low-risk path. Generalized from claude-plugin-registry 55f2d0b. stage/push-staged audit (#194): verified all 8 builders expose stage-<arch> + push-staged; all already complete, no gaps.
This commit is contained in:
+87
-15
@@ -5,15 +5,40 @@
|
||||
# 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.
|
||||
# Usage: pipeline-doctor [--strict] [<repo-path>] (default repo: .)
|
||||
# --strict also exit non-zero if any WARN was emitted (gate the soft checks).
|
||||
# Exit: 0 = all required checks pass (and, in --strict, no warnings);
|
||||
# 1 = one or more required checks failed (or a warning under --strict).
|
||||
#
|
||||
# This is a GATE-READY check (cluster #191/#193): a .woodpecker.yaml step or a
|
||||
# server pre-receive hook can call `pipeline-doctor --strict <repo>` and rely on
|
||||
# the exit code. It is read-only: it never touches the registry, never needs a
|
||||
# token, and never prints one.
|
||||
set -euo pipefail
|
||||
|
||||
REPO="${1:-.}"
|
||||
STRICT=0
|
||||
REPO="."
|
||||
for a in "$@"; do
|
||||
case "$a" in
|
||||
--strict) STRICT=1 ;;
|
||||
-h | --help)
|
||||
echo "Usage: pipeline-doctor [--strict] [<repo-path>]"
|
||||
echo " Assert the parity contract for <repo-path> (default: .)."
|
||||
echo " --strict exit non-zero on any WARN as well as any FAIL."
|
||||
exit 0
|
||||
;;
|
||||
-*)
|
||||
echo "error: unknown option '$a' (try --help)" >&2
|
||||
exit 2
|
||||
;;
|
||||
*) REPO="$a" ;;
|
||||
esac
|
||||
done
|
||||
REPO="$(cd "$REPO" && pwd)"
|
||||
FLAKE="$REPO/flake.nix"
|
||||
|
||||
fail=0
|
||||
warn_n=0
|
||||
pass_n=0
|
||||
note() { printf ' %s %s\n' "$1" "$2"; }
|
||||
ok() {
|
||||
@@ -24,7 +49,10 @@ bad() {
|
||||
fail=$((fail + 1))
|
||||
note "FAIL" "$1"
|
||||
}
|
||||
warn() { note "WARN" "$1"; }
|
||||
warn() {
|
||||
warn_n=$((warn_n + 1))
|
||||
note "WARN" "$1"
|
||||
}
|
||||
|
||||
echo "pipeline-doctor: $REPO"
|
||||
|
||||
@@ -35,20 +63,47 @@ fi
|
||||
|
||||
flake_txt="$(cat "$FLAKE")"
|
||||
|
||||
# Documented escape-hatch (cluster #196): a repo that must keep a hand-written
|
||||
# Dockerfile/OCI pipeline (BuildKit, etc.) instead of a parity-lib builder is
|
||||
# allowed to opt out PROVIDED it ships a `ci/local.sh` that gives a dev the same
|
||||
# local==CI entrypoint. When present, the archetype + parity-lib asserts are
|
||||
# DOWNGRADED to warnings (the escape-hatch is the contract); the token / dev-tag
|
||||
# / dry-run / set-x asserts below still apply in full to ci/local.sh.
|
||||
ESCAPE_HATCH=0
|
||||
if [ -f "$REPO/ci/local.sh" ]; then
|
||||
ESCAPE_HATCH=1
|
||||
fi
|
||||
|
||||
# 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)"
|
||||
elif [ "$ESCAPE_HATCH" -eq 1 ]; then
|
||||
warn "no parity-lib archetype, but ci/local.sh escape-hatch present (cluster #196)"
|
||||
else
|
||||
bad "no archetype: expected a stage-<arch>/publish-<arch> app or 'archetype' marker"
|
||||
bad "no archetype: expected a stage-<arch>/publish-<arch> app or 'archetype' marker (or a ci/local.sh escape-hatch)"
|
||||
fi
|
||||
|
||||
# 2. consumes parity-lib (the shared module).
|
||||
# 2. consumes parity-lib (the shared module) OR a documented ci/local.sh hatch.
|
||||
CONSUMES_PARITY=0
|
||||
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"
|
||||
CONSUMES_PARITY=1
|
||||
fi
|
||||
if [ "$CONSUMES_PARITY" -eq 1 ]; then
|
||||
ok "consumes parity-lib (input or one of the mk*Publish builders)"
|
||||
elif [ "$ESCAPE_HATCH" -eq 1 ]; then
|
||||
warn "does not consume parity-lib, but ci/local.sh escape-hatch present (cluster #196)"
|
||||
else
|
||||
bad "does not consume parity-lib: inline the input and use a mk*Publish builder (or ship a ci/local.sh escape-hatch)"
|
||||
fi
|
||||
|
||||
# Checks 3–6 below are GUARANTEED by parity-lib for a consumer: the token
|
||||
# resolution, dev-tag guard, dry-run gate and meta.description all live inside
|
||||
# the mk*Publish-generated apps (in the parity-lib store path), not in the
|
||||
# consumer's flake.nix text. So when the repo consumes parity-lib, those four
|
||||
# contracts are satisfied BY DELEGATION and we don't re-grep the consumer's
|
||||
# source for them. They are only grepped for a non-consumer (escape-hatch or
|
||||
# bespoke) repo, where the consumer's own ci/*.sh must carry them.
|
||||
|
||||
# 3. token = $REGISTRY_TOKEN with pass fallback, and never printed.
|
||||
token_src="$flake_txt"
|
||||
@@ -56,7 +111,9 @@ 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' &&
|
||||
if [ "$CONSUMES_PARITY" -eq 1 ]; then
|
||||
ok "token = \$REGISTRY_TOKEN with pass fallback (delegated to parity-lib)"
|
||||
elif 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
|
||||
@@ -111,21 +168,27 @@ else
|
||||
fi
|
||||
|
||||
# 4. dev-tag guard present.
|
||||
if printf '%s' "$token_src" | grep -Eq 'parity_devtag_guard|refusing to publish without an explicit'; then
|
||||
if [ "$CONSUMES_PARITY" -eq 1 ]; then
|
||||
ok "dev-tag guard present (delegated to parity-lib parity_devtag_guard)"
|
||||
elif 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
|
||||
if [ "$CONSUMES_PARITY" -eq 1 ]; then
|
||||
ok "dry-run default exists (delegated to parity-lib parity_parse_args)"
|
||||
elif 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
|
||||
if [ "$CONSUMES_PARITY" -eq 1 ]; then
|
||||
ok "apps carry meta.description (delegated to parity-lib mkApp)"
|
||||
elif printf '%s' "$flake_txt" | grep -Eq 'meta\.description|meta = \{'; then
|
||||
ok "apps carry meta.description"
|
||||
else
|
||||
bad "apps missing meta.description"
|
||||
@@ -147,6 +210,15 @@ echo " nix run .#publish-<arch> # stage + push that arch (DRY-RUN; add
|
||||
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 " nix run .#verify-digest # OCI only: print local manifest digests (parity assertion)"
|
||||
echo ""
|
||||
echo "Summary: $pass_n passed, $fail failed."
|
||||
[ "$fail" -eq 0 ]
|
||||
echo "Summary: $pass_n passed, $fail failed, $warn_n warned."
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
echo "RESULT: parity contract VIOLATED ($fail failing check(s))." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "$STRICT" -eq 1 ] && [ "$warn_n" -ne 0 ]; then
|
||||
echo "RESULT: --strict and $warn_n warning(s) present — failing the gate." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "RESULT: parity contract satisfied."
|
||||
|
||||
Reference in New Issue
Block a user