865 lines
24 KiB
Bash
865 lines
24 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
usage() {
|
|
cat <<'USAGE'
|
|
Usage:
|
|
tools/release/push-release-tag.sh [options]
|
|
|
|
Bumps installable GovOPlaN package versions, commits pending changes in the
|
|
GovOPlaN release/module repositories, creates an annotated release tag in each
|
|
repo, and pushes branch+tag.
|
|
|
|
By default the script asks which version part to bump:
|
|
major X.Y.Z -> (X+1).0.0
|
|
minor X.Y.Z -> X.(Y+1).0
|
|
subversion X.Y.Z -> X.Y.(Z+1)
|
|
|
|
Options:
|
|
--bump <kind> Version bump: major, minor, subversion, or patch.
|
|
--version <x.y.z> Explicit target version instead of a bump. May equal
|
|
the current repo versions when they were bumped already.
|
|
-r, --remote <name> Remote to push to. Defaults to origin.
|
|
-b, --branch <name> Branch to update in every repo. Defaults to each current branch.
|
|
-m, --message <text> Commit message. Defaults to "Release v<x.y.z>".
|
|
--tag-message <text> Annotated tag message. Defaults to the commit message.
|
|
--skip-release-lock Do not regenerate core webui/package-lock.release.json.
|
|
--warn-migration-audit Run the migration release audit without baseline strictness.
|
|
--strict-migration-audit
|
|
Require current Alembic heads to be recorded in the
|
|
latest migration release baseline.
|
|
--skip-migration-audit Do not run the release migration audit.
|
|
--publish-web-catalog After core/modules are pushed, publish a signed release
|
|
catalog into addideas-govoplan-website.
|
|
--web-root <path> addideas-govoplan-website checkout for --publish-web-catalog.
|
|
Defaults to ../addideas-govoplan-website.
|
|
--catalog-signing-key <key-id=/path/private.pem>
|
|
Catalog signing key for --publish-web-catalog. May be
|
|
repeated during key rotation.
|
|
--catalog-channel <name>
|
|
Catalog channel. Defaults to stable.
|
|
--catalog-sequence <n> Monotonic catalog sequence. Defaults to UTC timestamp.
|
|
--catalog-expires-days <days>
|
|
Catalog expiry window. Defaults to 90.
|
|
--catalog-public-url <url>
|
|
Public website URL. Defaults to
|
|
https://govoplan.add-ideas.de.
|
|
--build-web-catalog Run npm run build in addideas-govoplan-website before committing the
|
|
catalog update.
|
|
--npm <path> npm executable for lockfile generation.
|
|
-y, --yes Do not prompt before committing, tagging, and pushing.
|
|
-n, --dry-run Print intended actions without changing files or git state.
|
|
-h, --help Show this help.
|
|
|
|
Repos:
|
|
Installable release packages:
|
|
govoplan-access
|
|
govoplan-admin
|
|
govoplan-tenancy
|
|
govoplan-organizations
|
|
govoplan-identity
|
|
govoplan-idm
|
|
govoplan-policy
|
|
govoplan-audit
|
|
govoplan-dashboard
|
|
govoplan-files
|
|
govoplan-mail
|
|
govoplan-campaign
|
|
govoplan-calendar
|
|
govoplan-ops
|
|
govoplan-core
|
|
|
|
Roadmap/scaffold module repositories are tag-only until they have package
|
|
metadata: addresses, appointments, cases, connectors, dms, erp,
|
|
fit-connect, forms, identity-trust, ledger, notifications, payments,
|
|
portal, reporting, scheduling, search, tasks, templates, workflow, xoev,
|
|
xrechnung, and xta-osci.
|
|
|
|
Examples:
|
|
tools/release/push-release-tag.sh
|
|
tools/release/push-release-tag.sh --bump subversion
|
|
tools/release/push-release-tag.sh --version 0.2.0 --message "Release v0.2.0"
|
|
USAGE
|
|
}
|
|
|
|
fail() {
|
|
echo "error: $*" >&2
|
|
exit 1
|
|
}
|
|
|
|
META_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
PARENT="$(dirname "$META_ROOT")"
|
|
ROOT="${GOVOPLAN_CORE_ROOT:-$PARENT/govoplan-core}"
|
|
ROOT="$(cd "$ROOT" && pwd)"
|
|
PACKAGE_MODULE_REPOS=(
|
|
"$PARENT/govoplan-access"
|
|
"$PARENT/govoplan-admin"
|
|
"$PARENT/govoplan-approvals"
|
|
"$PARENT/govoplan-assets"
|
|
"$PARENT/govoplan-audit"
|
|
"$PARENT/govoplan-booking"
|
|
"$PARENT/govoplan-calendar"
|
|
"$PARENT/govoplan-campaign"
|
|
"$PARENT/govoplan-certificates"
|
|
"$PARENT/govoplan-committee"
|
|
"$PARENT/govoplan-consultation"
|
|
"$PARENT/govoplan-contracts"
|
|
"$PARENT/govoplan-dashboard"
|
|
"$PARENT/govoplan-docs"
|
|
"$PARENT/govoplan-facilities"
|
|
"$PARENT/govoplan-files"
|
|
"$PARENT/govoplan-forms-runtime"
|
|
"$PARENT/govoplan-grants"
|
|
"$PARENT/govoplan-helpdesk"
|
|
"$PARENT/govoplan-identity"
|
|
"$PARENT/govoplan-idm"
|
|
"$PARENT/govoplan-inspections"
|
|
"$PARENT/govoplan-issue-reporting"
|
|
"$PARENT/govoplan-learning"
|
|
"$PARENT/govoplan-mail"
|
|
"$PARENT/govoplan-ops"
|
|
"$PARENT/govoplan-organizations"
|
|
"$PARENT/govoplan-permits"
|
|
"$PARENT/govoplan-policy"
|
|
"$PARENT/govoplan-procurement"
|
|
"$PARENT/govoplan-records"
|
|
"$PARENT/govoplan-resources"
|
|
"$PARENT/govoplan-risk-compliance"
|
|
"$PARENT/govoplan-tenancy"
|
|
"$PARENT/govoplan-transparency"
|
|
)
|
|
TAG_ONLY_MODULE_REPOS=(
|
|
"$PARENT/govoplan-addresses"
|
|
"$PARENT/govoplan-appointments"
|
|
"$PARENT/govoplan-cases"
|
|
"$PARENT/govoplan-connectors"
|
|
"$PARENT/govoplan-dms"
|
|
"$PARENT/govoplan-erp"
|
|
"$PARENT/govoplan-fit-connect"
|
|
"$PARENT/govoplan-forms"
|
|
"$PARENT/govoplan-identity-trust"
|
|
"$PARENT/govoplan-ledger"
|
|
"$PARENT/govoplan-notifications"
|
|
"$PARENT/govoplan-payments"
|
|
"$PARENT/govoplan-portal"
|
|
"$PARENT/govoplan-postbox"
|
|
"$PARENT/govoplan-reporting"
|
|
"$PARENT/govoplan-scheduling"
|
|
"$PARENT/govoplan-search"
|
|
"$PARENT/govoplan-tasks"
|
|
"$PARENT/govoplan-templates"
|
|
"$PARENT/govoplan-workflow"
|
|
"$PARENT/govoplan-xoev"
|
|
"$PARENT/govoplan-xrechnung"
|
|
"$PARENT/govoplan-xta-osci"
|
|
)
|
|
MODULE_REPOS=("${PACKAGE_MODULE_REPOS[@]}" "${TAG_ONLY_MODULE_REPOS[@]}")
|
|
PACKAGE_REPOS=("${PACKAGE_MODULE_REPOS[@]}" "$ROOT")
|
|
REPOS=(
|
|
"${MODULE_REPOS[@]}"
|
|
"$ROOT"
|
|
)
|
|
|
|
REMOTE="origin"
|
|
BRANCH=""
|
|
BUMP=""
|
|
TARGET_VERSION=""
|
|
COMMIT_MESSAGE=""
|
|
TAG_MESSAGE=""
|
|
DRY_RUN=0
|
|
YES=0
|
|
GENERATE_RELEASE_LOCK=1
|
|
MIGRATION_AUDIT_MODE="auto"
|
|
NPM_BIN="${NPM:-}"
|
|
PUBLISH_WEB_CATALOG=0
|
|
WEB_ROOT="$PARENT/addideas-govoplan-website"
|
|
CATALOG_CHANNEL="stable"
|
|
CATALOG_SEQUENCE=""
|
|
CATALOG_EXPIRES_DAYS="90"
|
|
CATALOG_PUBLIC_URL="https://govoplan.add-ideas.de"
|
|
BUILD_WEB_CATALOG=0
|
|
CATALOG_SIGNING_KEYS=()
|
|
|
|
if [[ -z "${PYTHON:-}" ]]; then
|
|
if [[ -x "$META_ROOT/.venv/bin/python" ]]; then
|
|
PYTHON="$META_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
|
|
|
|
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o BatchMode=yes -o ConnectTimeout=15}"
|
|
GIT_REMOTE_TIMEOUT="${GIT_REMOTE_TIMEOUT:-30}"
|
|
|
|
normalize_bump() {
|
|
local value="${1,,}"
|
|
case "$value" in
|
|
major|minor|subversion)
|
|
printf '%s\n' "$value"
|
|
;;
|
|
patch|sub|sub-version)
|
|
printf '%s\n' "subversion"
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
validate_version() {
|
|
[[ "$1" =~ ^[0-9]+[.][0-9]+[.][0-9]+$ ]]
|
|
}
|
|
|
|
version_gt() {
|
|
local new_major new_minor new_patch old_major old_minor old_patch
|
|
IFS=. read -r new_major new_minor new_patch <<< "$1"
|
|
IFS=. read -r old_major old_minor old_patch <<< "$2"
|
|
|
|
(( new_major > old_major )) && return 0
|
|
(( new_major < old_major )) && return 1
|
|
(( new_minor > old_minor )) && return 0
|
|
(( new_minor < old_minor )) && return 1
|
|
(( new_patch > old_patch )) && return 0
|
|
return 1
|
|
}
|
|
|
|
bump_version() {
|
|
local version="$1"
|
|
local bump="$2"
|
|
local major minor patch
|
|
IFS=. read -r major minor patch <<< "$version"
|
|
|
|
case "$bump" in
|
|
major)
|
|
printf '%s.0.0\n' "$((major + 1))"
|
|
;;
|
|
minor)
|
|
printf '%s.%s.0\n' "$major" "$((minor + 1))"
|
|
;;
|
|
subversion)
|
|
printf '%s.%s.%s\n' "$major" "$minor" "$((patch + 1))"
|
|
;;
|
|
*)
|
|
fail "unknown bump kind: $bump"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
get_project_name() {
|
|
"$PYTHON" -c 'import pathlib, sys, tomllib; print(tomllib.loads(pathlib.Path(sys.argv[1]).read_text())["project"]["name"])' "$1/pyproject.toml"
|
|
}
|
|
|
|
get_project_version() {
|
|
"$PYTHON" -c 'import pathlib, sys, tomllib; print(tomllib.loads(pathlib.Path(sys.argv[1]).read_text())["project"]["version"])' "$1/pyproject.toml"
|
|
}
|
|
|
|
update_pyproject() {
|
|
local repo="$1"
|
|
local version="$2"
|
|
|
|
"$PYTHON" - "$repo/pyproject.toml" "$version" <<'PYCODE'
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
import tomllib
|
|
|
|
path = pathlib.Path(sys.argv[1])
|
|
new_version = sys.argv[2]
|
|
text = path.read_text()
|
|
data = tomllib.loads(text)
|
|
project = data["project"]
|
|
name = project["name"]
|
|
old_version = project["version"]
|
|
|
|
text, version_count = re.subn(
|
|
r'(?m)^version\s*=\s*["\'][^"\']+["\']\s*$',
|
|
f'version = "{new_version}"',
|
|
text,
|
|
count=1,
|
|
)
|
|
if version_count != 1:
|
|
raise SystemExit(f"could not update project.version in {path}")
|
|
|
|
if name != "govoplan-core":
|
|
text, dependency_count = re.subn(
|
|
r'(govoplan-[A-Za-z0-9-]+>=)\d+\.\d+\.\d+',
|
|
rf'\g<1>{new_version}',
|
|
text,
|
|
)
|
|
if dependency_count < 1:
|
|
raise SystemExit(f"could not update govoplan-core dependency in {path}")
|
|
|
|
path.write_text(text)
|
|
print(f"{name}: {old_version} -> {new_version}")
|
|
PYCODE
|
|
}
|
|
|
|
update_manifest_version() {
|
|
local repo="$1"
|
|
local project_name="$2"
|
|
local version="$3"
|
|
local manifest_paths=()
|
|
|
|
if [[ "$project_name" == "govoplan-core" ]]; then
|
|
return 0
|
|
fi
|
|
|
|
while IFS= read -r -d '' manifest_path; do
|
|
manifest_paths+=("$manifest_path")
|
|
done < <(find "$repo/src" -path "*/backend/manifest.py" -print0 2>/dev/null)
|
|
|
|
[[ "${#manifest_paths[@]}" -gt 0 ]] || fail "missing module manifest under $repo/src"
|
|
|
|
for manifest_path in "${manifest_paths[@]}"; do
|
|
"$PYTHON" - "$manifest_path" "$version" <<'PYCODE'
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
path = pathlib.Path(sys.argv[1])
|
|
new_version = sys.argv[2]
|
|
text = path.read_text()
|
|
text, count = re.subn(
|
|
r'(?m)^(\s*version=)["\'][^"\']+["\'](,?\s*)$',
|
|
rf'\1"{new_version}"\2',
|
|
text,
|
|
count=1,
|
|
)
|
|
if count == 0:
|
|
text, count = re.subn(
|
|
r'(?m)^(MODULE_VERSION\s*=\s*)["\'][^"\']+["\'](\s*)$',
|
|
rf'\1"{new_version}"\2',
|
|
text,
|
|
count=1,
|
|
)
|
|
if count != 1:
|
|
raise SystemExit(f"could not update ModuleManifest.version in {path}")
|
|
path.write_text(text)
|
|
PYCODE
|
|
done
|
|
}
|
|
|
|
update_webui_package() {
|
|
local repo="$1"
|
|
local project_name="$2"
|
|
local version="$3"
|
|
local package_path=""
|
|
local release_package_path="$repo/webui/package.release.json"
|
|
|
|
for package_path in "$repo/package.json" "$repo/webui/package.json"; do
|
|
[[ -f "$package_path" ]] || continue
|
|
"$PYTHON" - "$package_path" "$project_name" "$version" <<'PYCODE'
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import pathlib
|
|
import sys
|
|
|
|
path = pathlib.Path(sys.argv[1])
|
|
project_name = sys.argv[2]
|
|
new_version = sys.argv[3]
|
|
|
|
data = json.loads(path.read_text())
|
|
data["version"] = new_version
|
|
if project_name != "govoplan-core":
|
|
peers = data.setdefault("peerDependencies", {})
|
|
if "@govoplan/core-webui" in peers:
|
|
peers["@govoplan/core-webui"] = f"^{new_version}"
|
|
path.write_text(json.dumps(data, indent=2) + "\n")
|
|
PYCODE
|
|
done
|
|
|
|
if [[ "$project_name" == "govoplan-core" && -f "$release_package_path" ]]; then
|
|
"$PYTHON" - "$release_package_path" "$version" <<'PYCODE'
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import pathlib
|
|
import re
|
|
import sys
|
|
|
|
path = pathlib.Path(sys.argv[1])
|
|
new_version = sys.argv[2]
|
|
|
|
data = json.loads(path.read_text())
|
|
data["version"] = new_version
|
|
dependencies = data.setdefault("dependencies", {})
|
|
for name, spec in list(dependencies.items()):
|
|
if name.startswith("@govoplan/") and isinstance(spec, str) and spec.startswith("git+"):
|
|
dependencies[name] = re.sub(r"#v\d+\.\d+\.\d+$", f"#v{new_version}", spec)
|
|
path.write_text(json.dumps(data, indent=2) + "\n")
|
|
PYCODE
|
|
fi
|
|
}
|
|
|
|
update_version_files() {
|
|
local repo="$1"
|
|
local version="$2"
|
|
local project_name="${PROJECT_NAMES[$repo]}"
|
|
|
|
update_pyproject "$repo" "$version"
|
|
update_manifest_version "$repo" "$project_name" "$version"
|
|
update_webui_package "$repo" "$project_name" "$version"
|
|
}
|
|
|
|
refresh_development_webui_lock() {
|
|
[[ -f "$ROOT/webui/package.json" ]] || return 0
|
|
[[ -f "$ROOT/webui/package-lock.json" ]] || return 0
|
|
|
|
run env PATH="$(dirname "$NPM_BIN"):$PATH" "$NPM_BIN" --prefix "$ROOT/webui" install --package-lock-only --ignore-scripts
|
|
}
|
|
|
|
generate_release_lock() {
|
|
if [[ "$GENERATE_RELEASE_LOCK" -eq 0 ]]; then
|
|
return
|
|
fi
|
|
|
|
[[ -x "$META_ROOT/tools/release/generate-release-lock.sh" ]] || fail "missing executable release lock generator: $META_ROOT/tools/release/generate-release-lock.sh"
|
|
run "$META_ROOT/tools/release/generate-release-lock.sh" --core-root "$ROOT" --npm "$NPM_BIN"
|
|
}
|
|
|
|
run_migration_release_audit() {
|
|
local audit_script="$META_ROOT/tools/release/release-migration-audit.py"
|
|
local command=("$PYTHON" "$audit_script" "--track" "release")
|
|
|
|
case "$MIGRATION_AUDIT_MODE" in
|
|
skip)
|
|
echo "Migration release audit: skipped"
|
|
return
|
|
;;
|
|
strict)
|
|
command+=("--strict")
|
|
;;
|
|
auto)
|
|
command+=("--strict-if-baseline")
|
|
;;
|
|
warn)
|
|
;;
|
|
*)
|
|
fail "unknown migration audit mode: $MIGRATION_AUDIT_MODE"
|
|
;;
|
|
esac
|
|
|
|
[[ -f "$audit_script" ]] || fail "missing migration audit helper: $audit_script"
|
|
run "${command[@]}"
|
|
}
|
|
|
|
print_command() {
|
|
printf '+'
|
|
printf ' %q' "$@"
|
|
printf '\n'
|
|
}
|
|
|
|
run() {
|
|
print_command "$@"
|
|
if [[ "$DRY_RUN" -eq 0 ]]; then
|
|
"$@"
|
|
fi
|
|
}
|
|
|
|
prompt_for_bump() {
|
|
local answer=""
|
|
|
|
if [[ -n "$BUMP" || -n "$TARGET_VERSION" ]]; then
|
|
return
|
|
fi
|
|
|
|
if [[ ! -t 0 ]]; then
|
|
fail "pass --bump major|minor|subversion or --version x.y.z for non-interactive use"
|
|
fi
|
|
|
|
echo "Select version increase:"
|
|
echo " 1) major X.Y.Z -> (X+1).0.0"
|
|
echo " 2) minor X.Y.Z -> X.(Y+1).0"
|
|
echo " 3) subversion X.Y.Z -> X.Y.(Z+1)"
|
|
printf 'Choice [subversion]: '
|
|
read -r answer
|
|
|
|
case "${answer,,}" in
|
|
""|3|subversion|patch|sub|sub-version)
|
|
BUMP="subversion"
|
|
;;
|
|
1|major)
|
|
BUMP="major"
|
|
;;
|
|
2|minor)
|
|
BUMP="minor"
|
|
;;
|
|
*)
|
|
fail "unknown version bump: $answer"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
confirm_release() {
|
|
local answer=""
|
|
|
|
if [[ "$DRY_RUN" -eq 1 || "$YES" -eq 1 ]]; then
|
|
return
|
|
fi
|
|
|
|
if [[ ! -t 0 ]]; then
|
|
fail "refusing to commit, tag, and push without confirmation; pass --yes for non-interactive use"
|
|
fi
|
|
|
|
printf 'Commit all changes, tag all repos, and push to %s? [y/N] ' "$REMOTE"
|
|
read -r answer
|
|
case "${answer,,}" in
|
|
y|yes)
|
|
;;
|
|
*)
|
|
echo "Aborted."
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--bump)
|
|
[[ $# -ge 2 ]] || fail "missing value for $1"
|
|
BUMP="$(normalize_bump "$2")" || fail "unknown bump kind: $2"
|
|
shift 2
|
|
;;
|
|
--version)
|
|
[[ $# -ge 2 ]] || fail "missing value for $1"
|
|
TARGET_VERSION="$2"
|
|
shift 2
|
|
;;
|
|
-r|--remote)
|
|
[[ $# -ge 2 ]] || fail "missing value for $1"
|
|
REMOTE="$2"
|
|
shift 2
|
|
;;
|
|
-b|--branch)
|
|
[[ $# -ge 2 ]] || fail "missing value for $1"
|
|
BRANCH="$2"
|
|
shift 2
|
|
;;
|
|
-m|--message)
|
|
[[ $# -ge 2 ]] || fail "missing value for $1"
|
|
COMMIT_MESSAGE="$2"
|
|
shift 2
|
|
;;
|
|
--tag-message)
|
|
[[ $# -ge 2 ]] || fail "missing value for $1"
|
|
TAG_MESSAGE="$2"
|
|
shift 2
|
|
;;
|
|
--skip-release-lock)
|
|
GENERATE_RELEASE_LOCK=0
|
|
shift
|
|
;;
|
|
--warn-migration-audit)
|
|
MIGRATION_AUDIT_MODE="warn"
|
|
shift
|
|
;;
|
|
--strict-migration-audit)
|
|
MIGRATION_AUDIT_MODE="strict"
|
|
shift
|
|
;;
|
|
--skip-migration-audit)
|
|
MIGRATION_AUDIT_MODE="skip"
|
|
shift
|
|
;;
|
|
--publish-web-catalog)
|
|
PUBLISH_WEB_CATALOG=1
|
|
shift
|
|
;;
|
|
--web-root)
|
|
[[ $# -ge 2 ]] || fail "missing value for $1"
|
|
WEB_ROOT="$2"
|
|
shift 2
|
|
;;
|
|
--catalog-signing-key)
|
|
[[ $# -ge 2 ]] || fail "missing value for $1"
|
|
CATALOG_SIGNING_KEYS+=("$2")
|
|
shift 2
|
|
;;
|
|
--catalog-channel)
|
|
[[ $# -ge 2 ]] || fail "missing value for $1"
|
|
CATALOG_CHANNEL="$2"
|
|
shift 2
|
|
;;
|
|
--catalog-sequence)
|
|
[[ $# -ge 2 ]] || fail "missing value for $1"
|
|
CATALOG_SEQUENCE="$2"
|
|
shift 2
|
|
;;
|
|
--catalog-expires-days)
|
|
[[ $# -ge 2 ]] || fail "missing value for $1"
|
|
CATALOG_EXPIRES_DAYS="$2"
|
|
shift 2
|
|
;;
|
|
--catalog-public-url)
|
|
[[ $# -ge 2 ]] || fail "missing value for $1"
|
|
CATALOG_PUBLIC_URL="$2"
|
|
shift 2
|
|
;;
|
|
--build-web-catalog)
|
|
BUILD_WEB_CATALOG=1
|
|
shift
|
|
;;
|
|
--npm)
|
|
[[ $# -ge 2 ]] || fail "missing value for $1"
|
|
NPM_BIN="$2"
|
|
shift 2
|
|
;;
|
|
-y|--yes)
|
|
YES=1
|
|
shift
|
|
;;
|
|
-n|--dry-run)
|
|
DRY_RUN=1
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown argument: $1" >&2
|
|
usage >&2
|
|
exit 2
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -n "$BUMP" && -n "$TARGET_VERSION" ]]; then
|
|
fail "use either --bump or --version, not both"
|
|
fi
|
|
if [[ "$PUBLISH_WEB_CATALOG" -eq 1 && ${#CATALOG_SIGNING_KEYS[@]} -eq 0 ]]; then
|
|
fail "--publish-web-catalog requires at least one --catalog-signing-key"
|
|
fi
|
|
|
|
prompt_for_bump
|
|
|
|
if ! command -v "$PYTHON" >/dev/null 2>&1; then
|
|
fail "Python interpreter not found: $PYTHON"
|
|
fi
|
|
if ! command -v "$NPM_BIN" >/dev/null 2>&1; then
|
|
fail "npm executable not found: $NPM_BIN"
|
|
fi
|
|
if [[ "$PUBLISH_WEB_CATALOG" -eq 1 ]]; then
|
|
[[ -d "$WEB_ROOT/.git" ]] || fail "addideas-govoplan-website repo not found: $WEB_ROOT"
|
|
[[ -x "$META_ROOT/tools/release/publish-release-catalog.sh" ]] || fail "missing executable catalog publisher: $META_ROOT/tools/release/publish-release-catalog.sh"
|
|
fi
|
|
|
|
declare -A PROJECT_NAMES
|
|
declare -A OLD_VERSIONS
|
|
declare -A BRANCHES
|
|
declare -A REPO_KINDS
|
|
|
|
for repo in "${REPOS[@]}"; do
|
|
[[ -d "$repo/.git" ]] || fail "not a git repo: $repo"
|
|
|
|
if [[ -f "$repo/pyproject.toml" ]]; then
|
|
REPO_KINDS["$repo"]="package"
|
|
PROJECT_NAMES["$repo"]="$(get_project_name "$repo")"
|
|
OLD_VERSIONS["$repo"]="$(get_project_version "$repo")"
|
|
|
|
validate_version "${OLD_VERSIONS[$repo]}" || fail "unsupported version ${OLD_VERSIONS[$repo]} in $repo"
|
|
else
|
|
REPO_KINDS["$repo"]="tag-only"
|
|
PROJECT_NAMES["$repo"]="$(basename "$repo")"
|
|
OLD_VERSIONS["$repo"]="tag-only"
|
|
fi
|
|
|
|
if [[ -n "$BRANCH" ]]; then
|
|
BRANCHES["$repo"]="$BRANCH"
|
|
else
|
|
BRANCHES["$repo"]="$(git -C "$repo" symbolic-ref --quiet --short HEAD || true)"
|
|
fi
|
|
|
|
[[ -n "${BRANCHES[$repo]}" ]] || fail "cannot infer branch for $repo; pass --branch <name>"
|
|
git -C "$repo" check-ref-format "refs/heads/${BRANCHES[$repo]}" || fail "invalid branch name ${BRANCHES[$repo]} for $repo"
|
|
git -C "$repo" remote get-url "$REMOTE" >/dev/null || fail "remote $REMOTE not found in $repo"
|
|
|
|
upstream="$(git -C "$repo" rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || true)"
|
|
if [[ -n "$upstream" && "$upstream" != "@{u}" ]]; then
|
|
behind_count="$(git -C "$repo" rev-list --count "HEAD..$upstream")"
|
|
[[ "$behind_count" == "0" ]] || fail "$repo is behind $upstream by $behind_count commit(s); pull/rebase first"
|
|
fi
|
|
done
|
|
|
|
if [[ -z "$TARGET_VERSION" ]]; then
|
|
BASE_VERSION="${OLD_VERSIONS[$ROOT]}"
|
|
for repo in "${PACKAGE_REPOS[@]}"; do
|
|
if [[ "${OLD_VERSIONS[$repo]}" != "$BASE_VERSION" ]]; then
|
|
fail "repo versions differ; pass --version x.y.z to choose an explicit target"
|
|
fi
|
|
done
|
|
TARGET_VERSION="$(bump_version "$BASE_VERSION" "$BUMP")"
|
|
fi
|
|
|
|
validate_version "$TARGET_VERSION" || fail "target version must be x.y.z: $TARGET_VERSION"
|
|
|
|
for repo in "${PACKAGE_REPOS[@]}"; do
|
|
if [[ "$TARGET_VERSION" == "${OLD_VERSIONS[$repo]}" ]]; then
|
|
continue
|
|
fi
|
|
if ! version_gt "$TARGET_VERSION" "${OLD_VERSIONS[$repo]}"; then
|
|
fail "target version $TARGET_VERSION must be greater than ${OLD_VERSIONS[$repo]} for ${PROJECT_NAMES[$repo]}"
|
|
fi
|
|
done
|
|
|
|
TAG="v$TARGET_VERSION"
|
|
COMMIT_MESSAGE="${COMMIT_MESSAGE:-Release $TAG}"
|
|
TAG_MESSAGE="${TAG_MESSAGE:-$COMMIT_MESSAGE}"
|
|
|
|
git -C "$ROOT" check-ref-format "refs/tags/$TAG" || fail "invalid tag name: $TAG"
|
|
|
|
for repo in "${REPOS[@]}"; do
|
|
if git -C "$repo" rev-parse --quiet --verify "refs/tags/$TAG" >/dev/null; then
|
|
fail "local tag already exists in ${PROJECT_NAMES[$repo]}: $TAG"
|
|
fi
|
|
|
|
set +e
|
|
timeout "$GIT_REMOTE_TIMEOUT" git -C "$repo" ls-remote --exit-code --tags "$REMOTE" "refs/tags/$TAG" >/dev/null
|
|
ls_remote_status=$?
|
|
set -e
|
|
|
|
if [[ "$ls_remote_status" -eq 0 ]]; then
|
|
fail "remote tag already exists for ${PROJECT_NAMES[$repo]} on $REMOTE: $TAG"
|
|
elif [[ "$ls_remote_status" -ne 2 ]]; then
|
|
fail "could not check remote tags for ${PROJECT_NAMES[$repo]} on $REMOTE"
|
|
fi
|
|
done
|
|
|
|
echo "Release plan:"
|
|
echo " version: $TARGET_VERSION"
|
|
echo " tag: $TAG"
|
|
echo " remote: $REMOTE"
|
|
echo " commit message: $COMMIT_MESSAGE"
|
|
echo " package repos: ${#PACKAGE_REPOS[@]} versioned"
|
|
echo " tag-only module repos: ${#TAG_ONLY_MODULE_REPOS[@]} committed/tagged/pushed without package version files"
|
|
if [[ "$GENERATE_RELEASE_LOCK" -eq 1 ]]; then
|
|
echo " release lock: regenerate core webui/package-lock.release.json after module tags are pushed"
|
|
else
|
|
echo " release lock: skipped"
|
|
fi
|
|
echo " migration audit: $MIGRATION_AUDIT_MODE"
|
|
if [[ "$PUBLISH_WEB_CATALOG" -eq 1 ]]; then
|
|
echo " web catalog: publish $CATALOG_CHANNEL catalog to $WEB_ROOT after core tag is pushed"
|
|
else
|
|
echo " web catalog: skipped"
|
|
fi
|
|
echo
|
|
|
|
for repo in "${REPOS[@]}"; do
|
|
if [[ "${REPO_KINDS[$repo]}" == "package" ]]; then
|
|
echo "${PROJECT_NAMES[$repo]}: ${OLD_VERSIONS[$repo]} -> $TARGET_VERSION on ${BRANCHES[$repo]}"
|
|
else
|
|
echo "${PROJECT_NAMES[$repo]}: tag-only $TAG on ${BRANCHES[$repo]}"
|
|
fi
|
|
git -C "$repo" status --short
|
|
echo
|
|
done
|
|
|
|
run_migration_release_audit
|
|
|
|
confirm_release
|
|
|
|
for repo in "${PACKAGE_REPOS[@]}"; do
|
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
echo "Would update version files in $repo to $TARGET_VERSION"
|
|
else
|
|
update_version_files "$repo" "$TARGET_VERSION"
|
|
fi
|
|
done
|
|
|
|
refresh_development_webui_lock
|
|
|
|
for repo in "${MODULE_REPOS[@]}"; do
|
|
run git -C "$repo" add -A
|
|
|
|
if [[ "$DRY_RUN" -eq 0 ]]; then
|
|
if git -C "$repo" diff --cached --quiet; then
|
|
if [[ "${REPO_KINDS[$repo]}" == "package" ]]; then
|
|
if [[ "$TARGET_VERSION" == "${OLD_VERSIONS[$repo]}" ]]; then
|
|
echo "No staged changes for ${PROJECT_NAMES[$repo]}; version is already $TARGET_VERSION, tagging current HEAD."
|
|
else
|
|
fail "no staged changes for ${PROJECT_NAMES[$repo]} after version bump"
|
|
fi
|
|
else
|
|
echo "No staged changes for ${PROJECT_NAMES[$repo]}; tagging current HEAD."
|
|
fi
|
|
else
|
|
run git -C "$repo" commit -m "$COMMIT_MESSAGE"
|
|
fi
|
|
else
|
|
run git -C "$repo" commit -m "$COMMIT_MESSAGE"
|
|
fi
|
|
|
|
run git -C "$repo" tag -a "$TAG" -m "$TAG_MESSAGE"
|
|
done
|
|
|
|
for repo in "${MODULE_REPOS[@]}"; do
|
|
run git -C "$repo" push --atomic "$REMOTE" "HEAD:refs/heads/${BRANCHES[$repo]}" "refs/tags/$TAG"
|
|
done
|
|
|
|
generate_release_lock
|
|
|
|
run git -C "$ROOT" add -A
|
|
|
|
if [[ "$DRY_RUN" -eq 0 ]]; then
|
|
if git -C "$ROOT" diff --cached --quiet; then
|
|
fail "no staged changes for ${PROJECT_NAMES[$ROOT]} after version bump"
|
|
fi
|
|
fi
|
|
|
|
run git -C "$ROOT" commit -m "$COMMIT_MESSAGE"
|
|
run git -C "$ROOT" tag -a "$TAG" -m "$TAG_MESSAGE"
|
|
run git -C "$ROOT" push --atomic "$REMOTE" "HEAD:refs/heads/${BRANCHES[$ROOT]}" "refs/tags/$TAG"
|
|
|
|
if [[ "$PUBLISH_WEB_CATALOG" -eq 1 ]]; then
|
|
CATALOG_ARGS=(
|
|
"$META_ROOT/tools/release/publish-release-catalog.sh"
|
|
--version "$TARGET_VERSION"
|
|
--channel "$CATALOG_CHANNEL"
|
|
--expires-days "$CATALOG_EXPIRES_DAYS"
|
|
--core-root "$ROOT"
|
|
--web-root "$WEB_ROOT"
|
|
--public-base-url "$CATALOG_PUBLIC_URL"
|
|
--npm "$NPM_BIN"
|
|
--commit
|
|
--tag
|
|
--push
|
|
--remote "$REMOTE"
|
|
)
|
|
if [[ -n "$BRANCH" ]]; then
|
|
CATALOG_ARGS+=(--branch "$BRANCH")
|
|
fi
|
|
if [[ -n "$CATALOG_SEQUENCE" ]]; then
|
|
CATALOG_ARGS+=(--sequence "$CATALOG_SEQUENCE")
|
|
fi
|
|
if [[ "$BUILD_WEB_CATALOG" -eq 1 ]]; then
|
|
CATALOG_ARGS+=(--build-web)
|
|
fi
|
|
for signing_key in "${CATALOG_SIGNING_KEYS[@]}"; do
|
|
CATALOG_ARGS+=(--catalog-signing-key "$signing_key")
|
|
done
|
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
CATALOG_ARGS+=(--dry-run)
|
|
fi
|
|
run "${CATALOG_ARGS[@]}"
|
|
fi
|
|
|
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
echo "Dry run complete for $TAG across all GovOPlaN repos."
|
|
else
|
|
echo "Released $TAG across all GovOPlaN repos."
|
|
fi
|