#!/usr/bin/env bash set -euo pipefail usage() { cat <<'USAGE' Usage: scripts/publish-release-catalog.sh --version --catalog-signing-key [options] Generates a signed module package catalog for a GovOPlaN release, writes it into govoplan-web/public/catalogs/v1, validates it, optionally builds the website, and optionally commits/tags/pushes govoplan-web. Options: --version Release version without leading v. --channel Catalog channel. Defaults to stable. --sequence Monotonic channel sequence. Defaults to UTC timestamp. --expires-days Catalog expiry window. Defaults to 90. --catalog-signing-key Ed25519 private key. May be repeated for rotation. --core-root govoplan-core checkout. Defaults to this script parent. --web-root govoplan-web checkout. Defaults to ../govoplan-web. --public-base-url Public website URL. Defaults to https://govoplan.add-ideas.de. --npm npm executable used for optional web build. --build-web Run npm run build in govoplan-web after generating files. --commit Commit generated catalog files in govoplan-web. --tag Tag govoplan-web with catalog-v. Implies --commit. --push Push govoplan-web branch and tag. Implies --commit. --remote Git remote for push. Defaults to origin. --branch Branch to push. Defaults to current govoplan-web branch. -n, --dry-run Print commands and validate inputs without writing git changes. -h, --help Show this help. USAGE } fail() { echo "error: $*" >&2 exit 1 } ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PARENT="$(dirname "$ROOT")" CORE_ROOT="$ROOT" WEB_ROOT="$PARENT/govoplan-web" VERSION="" CHANNEL="stable" SEQUENCE="" EXPIRES_DAYS="90" PUBLIC_BASE_URL="https://govoplan.add-ideas.de" REMOTE="origin" BRANCH="" BUILD_WEB=0 COMMIT=0 TAG_WEB=0 PUSH=0 DRY_RUN=0 SIGNING_KEYS=() NPM_BIN="${NPM:-}" if [[ -z "${PYTHON:-}" ]]; then if [[ -x "$ROOT/.venv/bin/python" ]]; then PYTHON="$ROOT/.venv/bin/python" else PYTHON="python3" fi fi if [[ -z "$NPM_BIN" ]]; then if [[ -x "/home/zemion/.nvm/versions/node/v22.22.3/bin/npm" ]]; then NPM_BIN="/home/zemion/.nvm/versions/node/v22.22.3/bin/npm" else NPM_BIN="npm" fi fi while [[ $# -gt 0 ]]; do case "$1" in --version) [[ $# -ge 2 ]] || fail "missing value for $1" VERSION="${2#v}" shift 2 ;; --channel) [[ $# -ge 2 ]] || fail "missing value for $1" CHANNEL="$2" shift 2 ;; --sequence) [[ $# -ge 2 ]] || fail "missing value for $1" SEQUENCE="$2" shift 2 ;; --expires-days) [[ $# -ge 2 ]] || fail "missing value for $1" EXPIRES_DAYS="$2" shift 2 ;; --catalog-signing-key) [[ $# -ge 2 ]] || fail "missing value for $1" SIGNING_KEYS+=("$2") shift 2 ;; --core-root) [[ $# -ge 2 ]] || fail "missing value for $1" CORE_ROOT="$(cd "$2" && pwd)" shift 2 ;; --web-root) [[ $# -ge 2 ]] || fail "missing value for $1" WEB_ROOT="$(cd "$2" && pwd)" shift 2 ;; --public-base-url) [[ $# -ge 2 ]] || fail "missing value for $1" PUBLIC_BASE_URL="$2" shift 2 ;; --npm) [[ $# -ge 2 ]] || fail "missing value for $1" NPM_BIN="$2" shift 2 ;; --build-web) BUILD_WEB=1 shift ;; --commit) COMMIT=1 shift ;; --tag) TAG_WEB=1 COMMIT=1 shift ;; --push) PUSH=1 COMMIT=1 shift ;; --remote) [[ $# -ge 2 ]] || fail "missing value for $1" REMOTE="$2" shift 2 ;; --branch) [[ $# -ge 2 ]] || fail "missing value for $1" BRANCH="$2" shift 2 ;; -n|--dry-run) DRY_RUN=1 shift ;; -h|--help) usage exit 0 ;; *) echo "Unknown argument: $1" >&2 usage >&2 exit 2 ;; esac done [[ -n "$VERSION" ]] || fail "--version is required" [[ "$VERSION" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]] || fail "version must be x.y.z: $VERSION" [[ ${#SIGNING_KEYS[@]} -gt 0 ]] || fail "at least one --catalog-signing-key is required" [[ -d "$CORE_ROOT/.git" ]] || fail "not a govoplan-core git repo: $CORE_ROOT" [[ -d "$WEB_ROOT/.git" ]] || fail "not a govoplan-web git repo: $WEB_ROOT" command -v "$PYTHON" >/dev/null 2>&1 || fail "Python not found: $PYTHON" if [[ "$BUILD_WEB" -eq 1 ]]; then command -v "$NPM_BIN" >/dev/null 2>&1 || fail "npm not found: $NPM_BIN" fi if [[ -z "$BRANCH" ]]; then BRANCH="$(git -C "$WEB_ROOT" symbolic-ref --quiet --short HEAD || true)" fi [[ -n "$BRANCH" ]] || fail "cannot infer govoplan-web branch; pass --branch" CATALOG_PATH="$WEB_ROOT/public/catalogs/v1/channels/$CHANNEL.json" KEYRING_PATH="$WEB_ROOT/public/catalogs/v1/keyring.json" TAG_NAME="catalog-v$VERSION" run() { printf '+' printf ' %q' "$@" printf '\n' if [[ "$DRY_RUN" -eq 0 ]]; then "$@" fi } GEN_ARGS=( "$PYTHON" "$CORE_ROOT/scripts/generate-release-catalog.py" --version "$VERSION" --channel "$CHANNEL" --expires-days "$EXPIRES_DAYS" --catalog-output "$CATALOG_PATH" --keyring-output "$KEYRING_PATH" --public-base-url "$PUBLIC_BASE_URL" ) if [[ -n "$SEQUENCE" ]]; then GEN_ARGS+=(--sequence "$SEQUENCE") fi for signing_key in "${SIGNING_KEYS[@]}"; do GEN_ARGS+=(--catalog-signing-key "$signing_key") done run "${GEN_ARGS[@]}" if [[ "$DRY_RUN" -eq 0 ]]; then GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS_FILE="$KEYRING_PATH" \ "$PYTHON" -m govoplan_core.commands.module_installer \ --validate-package-catalog "$CATALOG_PATH" \ --require-signed-catalog \ --approved-catalog-channel "$CHANNEL" \ --format json >/dev/null else echo "+ GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS_FILE=$KEYRING_PATH $PYTHON -m govoplan_core.commands.module_installer --validate-package-catalog $CATALOG_PATH --require-signed-catalog --approved-catalog-channel $CHANNEL --format json" fi if [[ "$BUILD_WEB" -eq 1 ]]; then run env PATH="$(dirname "$NPM_BIN"):$PATH" "$NPM_BIN" --prefix "$WEB_ROOT" run build fi if [[ "$COMMIT" -eq 1 ]]; then run git -C "$WEB_ROOT" add "$CATALOG_PATH" "$KEYRING_PATH" if [[ "$DRY_RUN" -eq 0 ]]; then if git -C "$WEB_ROOT" diff --cached --quiet; then echo "No govoplan-web catalog changes to commit." else run git -C "$WEB_ROOT" commit -m "Publish GovOPlaN v$VERSION $CHANNEL catalog" fi else run git -C "$WEB_ROOT" commit -m "Publish GovOPlaN v$VERSION $CHANNEL catalog" fi fi if [[ "$TAG_WEB" -eq 1 ]]; then if git -C "$WEB_ROOT" rev-parse --quiet --verify "refs/tags/$TAG_NAME" >/dev/null; then fail "govoplan-web tag already exists: $TAG_NAME" fi run git -C "$WEB_ROOT" tag -a "$TAG_NAME" -m "Publish GovOPlaN v$VERSION $CHANNEL catalog" fi if [[ "$PUSH" -eq 1 ]]; then if [[ "$TAG_WEB" -eq 1 ]]; then run git -C "$WEB_ROOT" push --atomic "$REMOTE" "HEAD:refs/heads/$BRANCH" "refs/tags/$TAG_NAME" else run git -C "$WEB_ROOT" push "$REMOTE" "HEAD:refs/heads/$BRANCH" fi fi echo "Catalog ready:" echo " $CATALOG_PATH" echo " $KEYRING_PATH" echo " URL: $PUBLIC_BASE_URL/catalogs/v1/channels/$CHANNEL.json"