Files
parity-lib/ci/pipeline-doctor.sh
T
Oleks db0bf3b9ab feat(parity): mkAtticClosurePublish builder + pipeline-doctor non-flake mode (#198, #193)
Adds the attic-closure archetype builder (build closure + attic push, no
registry artifact) so caddy/overlay-xonsh/flake-hub/woodpecker-peek share one
implementation. Adds non-flake mode to pipeline-doctor so ci-script repos
(gitea-mcp, helms) pass the gate. Self-check 9/9; gitea-mcp now passes.
2026-06-02 23:30:59 +03:00

277 lines
12 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 [--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
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"
# Non-flake parity form (cluster #193, issue #193). A repo with no root flake.nix
# is still a VALID parity repo if it ships the ci-script form: a ci/local.sh (or a
# ci/build.sh + ci/publish.sh pair) that is the single local==CI entrypoint, with
# a thin .woodpecker.yaml that calls those scripts. In that case the "consumes
# parity / single entrypoint" contract is satisfied via the CI-SCRIPT path instead
# of a parity-lib flake input, and the token-leak + #191 no-set-x scans below still
# apply to its ci/*.sh.
CI_SCRIPT_FORM=0
if [ -f "$REPO/ci/local.sh" ] || { [ -f "$REPO/ci/build.sh" ] && [ -f "$REPO/ci/publish.sh" ]; }; then
CI_SCRIPT_FORM=1
fi
fail=0
warn_n=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() {
warn_n=$((warn_n + 1))
note "WARN" "$1"
}
echo "pipeline-doctor: $REPO"
if [ ! -f "$FLAKE" ]; then
if [ "$CI_SCRIPT_FORM" -eq 1 ]; then
# Valid non-flake parity repo: assert via the ci-script form below.
flake_txt=""
else
echo "FAIL: no flake.nix and no ci/local.sh (or ci/build.sh + ci/publish.sh) at $REPO" >&2
exit 1
fi
else
flake_txt="$(cat "$FLAKE")"
fi
# 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
# Text of the ci/*.sh + .woodpecker.yaml — the archetype marker and the single
# local==CI entrypoint of a non-flake (ci-script form) repo live here, not in a
# flake.nix (cluster #193, issue #193).
ci_txt=""
if [ -d "$REPO/ci" ]; then
ci_txt="$(cat "$REPO"/ci/*.sh 2>/dev/null || true)"
fi
for wp in "$REPO/.woodpecker.yaml" "$REPO/.woodpecker.yml"; do
[ -f "$wp" ] && ci_txt="$ci_txt
$(cat "$wp")"
done
# 1. archetype declared (a publish/stage app whose name encodes the arch, or an
# explicit archetype marker comment) — in the flake OR, for a non-flake
# ci-script repo, in its ci/*.sh / .woodpecker.yaml.
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 [ "$CI_SCRIPT_FORM" -eq 1 ] && printf '%s' "$ci_txt" | grep -Eq 'archetype|ci/build\.sh|ci/publish\.sh|ci/local\.sh'; then
ok "archetype declared (non-flake ci-script form: ci/*.sh entrypoints, cluster #193)"
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 (or a ci-script form)"
fi
# 2. consumes parity-lib (the shared module) OR is a valid non-flake ci-script
# repo whose ci/local.sh (or ci/build.sh + ci/publish.sh) is the single
# local==CI entrypoint, OR a documented ci/local.sh escape-hatch.
CONSUMES_PARITY=0
if printf '%s' "$flake_txt" | grep -Eq 'parity-lib|parity\.lib|mk(PyPiWheel|S390xNpm|GenericBinary|Nix2Container|GoBinary|Helm|AtticClosure)Publish'; then
CONSUMES_PARITY=1
fi
if [ "$CONSUMES_PARITY" -eq 1 ]; then
ok "consumes parity-lib (input or one of the mk*Publish builders)"
elif [ "$CI_SCRIPT_FORM" -eq 1 ]; then
ok "single local==CI entrypoint via ci-script form (ci/local.sh or ci/build.sh + ci/publish.sh, cluster #193)"
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-script form)"
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"
if [ -d "$REPO/ci" ]; then
token_src="$token_src
$(cat "$REPO"/ci/*.sh 2>/dev/null || true)"
fi
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
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
# 3b. (security sweep, cluster #191) no `set -x` in a TOKEN-BEARING ci/*.sh.
# Scoped per-file: a script that references a registry token (REGISTRY_TOKEN /
# CI_REGISTRY_TOKEN / an 'Authorization: token' header) must NOT enable xtrace,
# which would echo the token to the build log. A `set -x` in a token-free helper
# (e.g. a pure version parser) is not flagged here.
xtrace_hits=""
if [ -d "$REPO/ci" ]; then
for s in "$REPO"/ci/*.sh; do
[ -f "$s" ] || continue
if grep -Eq '(REGISTRY_TOKEN|Authorization: token)' "$s" &&
grep -Eq '^[[:space:]]*set -[a-z]*x' "$s"; then
xtrace_hits="$xtrace_hits ${s#"$REPO"/}"
fi
done
fi
if [ -z "$xtrace_hits" ]; then
ok "no set -x in token-bearing ci/*.sh scripts"
else
bad "set -x in token-bearing ci/*.sh:$xtrace_hits (xtrace would echo the token)"
fi
# 4. dev-tag guard present. For a non-flake ci-script repo the equivalent guard
# is the dry-run-DEFAULT publish + the .woodpecker.yaml `refs/tags/v*` gate:
# nothing mutates the registry unless a v* tag run flips PUBLISH/DRY_RUN.
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"
elif [ "$CI_SCRIPT_FORM" -eq 1 ] &&
printf '%s' "$ci_txt" | grep -Eq 'DRY_RUN|dry-run|PUBLISH' &&
printf '%s' "$ci_txt" | grep -Eq 'refs/tags/v\*|event:[[:space:]]*tag|tag:'; then
ok "dev-tag guard equivalent (ci-script dry-run default + .woodpecker refs/tags/v* gate, cluster #193)"
else
bad "no dev-tag guard: a default-version --publish could clobber the registry"
fi
# 5. a --dry-run default exists.
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. N/A for a non-flake ci-script repo (it exposes
# shell-script entrypoints, not flake apps with meta); its self-documenting
# ci/*.sh header comments are the equivalent.
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"
elif [ "$CI_SCRIPT_FORM" -eq 1 ]; then
ok "meta.description N/A (non-flake ci-script form; ci/*.sh are the documented entrypoints)"
else
bad "apps missing meta.description"
fi
# 7. enumerable publish-* naming. For a non-flake ci-script repo the equivalent
# is the ci/build.sh + ci/publish.sh (or ci/local.sh) entrypoint set.
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/^/ /'
elif [ "$CI_SCRIPT_FORM" -eq 1 ]; then
ok "enumerable ci-script entrypoints (non-flake parity form, cluster #193):"
for s in ci/local.sh ci/build.sh ci/publish.sh; do
[ -f "$REPO/$s" ] && printf ' %s\n' "$s"
done
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 " nix run .#verify-digest # OCI only: print local manifest digests (parity assertion)"
echo ""
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."