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:
Oleks
2026-06-02 21:11:49 +03:00
parent af64a8ea4c
commit 79f9a2dd62
3 changed files with 163 additions and 15 deletions
+87 -15
View File
@@ -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 36 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."