af64a8ea4c
Mirrors mkPyPiWheelPublishMulti for npm: publishes a fixed {version,file,
distTag?} list, each staged into its own dir and npm-published with its
dist-tag (idempotent). file may be a .node or a plain binary; packageJson
declares main-vs-bin. Unblocks nextjs-swc (next15 dist-tag) + sentry-cli.
Shared parity_npm_publish_dir helper added.
218 lines
9.3 KiB
Bash
218 lines
9.3 KiB
Bash
# 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}"
|
|
|
|
# Snapshot whether the caller set $VERSION EXPLICITLY, captured at source time —
|
|
# BEFORE any app body overwrites VERSION with the derived default version
|
|
# (VERSION="$(parity_derive_version <default>)"). parity_devtag_guard reads this
|
|
# snapshot, not the live $VERSION, so an accidental local `--publish` with no
|
|
# explicit version and no v* tag is still blocked instead of silently shipping
|
|
# the flake's default version. Set-once: re-sourcing won't clobber the capture.
|
|
: "${PARITY_VERSION_EXPLICIT=${VERSION:-}}"
|
|
export PARITY_VERSION_EXPLICIT
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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() {
|
|
# Read the source-time snapshot, NOT the live $VERSION (which the app body
|
|
# has already set to the derived default by the time this runs).
|
|
if [ -n "${PARITY_VERSION_EXPLICIT:-}" ]; 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
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# POST one wheel to the Gitea PyPI registry. Idempotent: 409 == already present.
|
|
# Args: <pname> <version> <wheel-path> <token>. Never echoes the token. Used by
|
|
# both the single- and multi-version PyPI publish apps so the upload contract
|
|
# lives in one place. Returns non-zero (without aborting a caller loop) on a
|
|
# real upload failure so a multi-version run can attempt every version.
|
|
# ---------------------------------------------------------------------------
|
|
parity_pypi_post() {
|
|
local pname="$1" ver="$2" whl="$3" tok="$4" sha http
|
|
sha="$(sha256sum "$whl" | cut -d' ' -f1)"
|
|
http="$(curl -s -o /dev/null -w '%{http_code}' -X POST \
|
|
-H "Authorization: token $tok" \
|
|
-F ':action=file_upload' -F 'protocol_version=1' \
|
|
-F "sha256_digest=$sha" -F "name=$pname" -F "version=$ver" \
|
|
-F 'filetype=bdist_wheel' -F "content=@$whl" \
|
|
"https://$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/pypi")"
|
|
case "$http" in
|
|
200 | 201 | 409) echo "Published $pname $ver (HTTP $http)" ;;
|
|
*)
|
|
echo "BLOCKER(upload-failed): $pname $ver POST returned HTTP $http" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Derive a wheel's version from its filename. PEP 427: the second dash-separated
|
|
# field of "<dist>-<version>-<pytag>-<abitag>-<plat>.whl" is the version.
|
|
# ---------------------------------------------------------------------------
|
|
parity_wheel_version() {
|
|
basename "$1" | cut -d- -f2
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# `npm publish` a ready-to-publish package directory to the Gitea npm registry.
|
|
# Idempotent: an "already exists"/409 is treated as success. Never echoes the
|
|
# token (the output is sed-redacted). Args: <dir> <token> [<dist-tag>]. Used by
|
|
# both the single- and multi-version npm publish apps.
|
|
# ---------------------------------------------------------------------------
|
|
parity_npm_publish_dir() {
|
|
local dir="$1" tok="$2" dt="${3:-latest}" reg out rc
|
|
reg="https://$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/npm/"
|
|
out="$(cd "$dir" && npm publish --registry="$reg" \
|
|
"--//$PARITY_REGISTRY_HOST/api/packages/$PARITY_REGISTRY_OWNER/npm/:_authToken=$tok" \
|
|
--tag "$dt" 2>&1)"
|
|
rc=$?
|
|
printf '%s\n' "$out" | sed "s/$tok/***REDACTED***/g"
|
|
if [ "$rc" -ne 0 ]; then
|
|
if printf '%s' "$out" | grep -qiE 'already exists|409|conflict'; then
|
|
echo "npm: $(basename "$dir") already published — idempotent success."
|
|
return 0
|
|
fi
|
|
return "$rc"
|
|
fi
|
|
echo "Published $(basename "$dir") (dist-tag $dt)"
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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"
|
|
}
|