Release v0.1.4

This commit is contained in:
2026-07-02 15:01:18 +02:00
parent 6d391d13bd
commit a2053518d1
13 changed files with 788 additions and 67 deletions

136
scripts/launch-dev.sh Normal file
View File

@@ -0,0 +1,136 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="${GOVOPLAN_CORE_ROOT:-/mnt/DATA/git/govoplan-core}"
WEBUI_ROOT="$ROOT/webui"
PYTHON="${PYTHON:-$ROOT/.venv/bin/python}"
NODE_BIN="${NODE_BIN:-/home/zemion/.nvm/versions/node/v22.22.3/bin}"
NPM="${NPM:-$NODE_BIN/npm}"
BACKEND_HOST="${GOVOPLAN_BACKEND_HOST:-127.0.0.1}"
BACKEND_PORT="${GOVOPLAN_BACKEND_PORT:-8000}"
FRONTEND_HOST="${GOVOPLAN_FRONTEND_HOST:-127.0.0.1}"
FRONTEND_PORT="${GOVOPLAN_FRONTEND_PORT:-5173}"
OPEN_BROWSER="${OPEN_BROWSER:-1}"
LOG_DIR="$ROOT/runtime/dev-launcher"
BACKEND_LOG="$LOG_DIR/backend.log"
FRONTEND_LOG="$LOG_DIR/frontend.log"
BACKEND_URL="http://$BACKEND_HOST:$BACKEND_PORT"
FRONTEND_URL="http://$FRONTEND_HOST:$FRONTEND_PORT"
backend_pid=""
frontend_pid=""
fail() {
printf 'launch-dev: %s\n' "$*" >&2
exit 1
}
port_is_free() {
"$PYTHON" - "$1" "$2" <<'PY'
import socket
import sys
host = sys.argv[1]
port = int(sys.argv[2])
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
sock.bind((host, port))
except OSError:
raise SystemExit(1)
PY
}
wait_for_url() {
"$PYTHON" - "$1" <<'PY'
import sys
import time
import urllib.request
url = sys.argv[1]
deadline = time.monotonic() + 60
last_error = None
while time.monotonic() < deadline:
try:
with urllib.request.urlopen(url, timeout=2) as response:
if 200 <= response.status < 500:
raise SystemExit(0)
except Exception as exc: # noqa: BLE001 - printed only on timeout.
last_error = exc
time.sleep(1)
print(f"Timed out waiting for {url}: {last_error}", file=sys.stderr)
raise SystemExit(1)
PY
}
cleanup() {
if [ -n "${frontend_pid:-}" ] && kill -0 "$frontend_pid" 2>/dev/null; then
kill "$frontend_pid" 2>/dev/null || true
fi
if [ -n "${backend_pid:-}" ] && kill -0 "$backend_pid" 2>/dev/null; then
kill "$backend_pid" 2>/dev/null || true
fi
}
trap cleanup EXIT INT TERM
[ -x "$PYTHON" ] || fail "Python virtualenv not found at $PYTHON. Run: cd $ROOT && ./.venv/bin/python -m pip install -r requirements-dev.txt"
[ -x "$NPM" ] || fail "npm not found at $NPM. Set NODE_BIN or NPM to your Node installation."
[ -f "$WEBUI_ROOT/package.json" ] || fail "WebUI package.json not found at $WEBUI_ROOT/package.json"
[ -d "$WEBUI_ROOT/node_modules" ] || fail "WebUI node_modules missing. Run: cd $WEBUI_ROOT && PATH=$NODE_BIN:\$PATH $NPM install"
mkdir -p "$LOG_DIR"
: > "$BACKEND_LOG"
: > "$FRONTEND_LOG"
port_is_free "$BACKEND_HOST" "$BACKEND_PORT" || fail "$BACKEND_URL is already in use"
port_is_free "$FRONTEND_HOST" "$FRONTEND_PORT" || fail "$FRONTEND_URL is already in use"
printf 'Starting GovOPlaN backend at %s\n' "$BACKEND_URL"
(
cd "$ROOT"
"$PYTHON" -m govoplan_core.devserver --host "$BACKEND_HOST" --port "$BACKEND_PORT"
) >"$BACKEND_LOG" 2>&1 &
backend_pid="$!"
printf 'Waiting for %s/health\n' "$BACKEND_URL"
wait_for_url "$BACKEND_URL/health" || {
tail -n 80 "$BACKEND_LOG" >&2 || true
fail "backend did not become healthy"
}
printf 'Starting GovOPlaN WebUI at %s\n' "$FRONTEND_URL"
(
cd "$WEBUI_ROOT"
PATH="$WEBUI_ROOT/node_modules/.bin:$NODE_BIN:$PATH" \
VITE_API_PROXY_TARGET="$BACKEND_URL" \
"$NPM" run dev
) >"$FRONTEND_LOG" 2>&1 &
frontend_pid="$!"
printf 'Waiting for %s\n' "$FRONTEND_URL"
wait_for_url "$FRONTEND_URL" || {
tail -n 80 "$FRONTEND_LOG" >&2 || true
fail "frontend did not become reachable"
}
if [ "$OPEN_BROWSER" = "1" ] && command -v xdg-open >/dev/null 2>&1; then
xdg-open "$FRONTEND_URL" >/dev/null 2>&1 || true
fi
cat <<EOF
GovOPlaN is running.
Web UI: $FRONTEND_URL
API: $BACKEND_URL/api/v1
Health: $BACKEND_URL/health
Logs:
Backend: $BACKEND_LOG
WebUI: $FRONTEND_LOG
Press Ctrl+C to stop both processes.
EOF
wait

View File

@@ -16,11 +16,14 @@ By default the script asks which version part to bump:
Options:
--bump <kind> Version bump: major, minor, subversion, or patch.
--version <x.y.z> Explicit target version instead of a bump.
--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.
--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.
@@ -60,6 +63,8 @@ COMMIT_MESSAGE=""
TAG_MESSAGE=""
DRY_RUN=0
YES=0
GENERATE_RELEASE_LOCK=1
NPM_BIN="${NPM:-}"
if [[ -z "${PYTHON:-}" ]]; then
if [[ -x "$ROOT/.venv/bin/python" ]]; then
@@ -69,6 +74,14 @@ if [[ -z "${PYTHON:-}" ]]; then
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
normalize_bump() {
local value="${1,,}"
case "$value" in
@@ -174,6 +187,133 @@ print(f"{name}: {old_version} -> {new_version}")
PYCODE
}
update_manifest_version() {
local repo="$1"
local project_name="$2"
local version="$3"
local manifest_path=""
case "$project_name" in
govoplan-core)
manifest_path="$repo/src/govoplan_core/access/manifest.py"
;;
govoplan-files)
manifest_path="$repo/src/govoplan_files/backend/manifest.py"
;;
govoplan-mail)
manifest_path="$repo/src/govoplan_mail/backend/manifest.py"
;;
govoplan-campaign)
manifest_path="$repo/src/govoplan_campaign/backend/manifest.py"
;;
*)
fail "unknown project for manifest version update: $project_name"
;;
esac
[[ -f "$manifest_path" ]] || fail "missing module manifest: $manifest_path"
"$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 != 1:
raise SystemExit(f"could not update ModuleManifest.version in {path}")
path.write_text(text)
PYCODE
}
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 in {"@govoplan/files-webui", "@govoplan/mail-webui", "@govoplan/campaign-webui"}:
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 "$ROOT/scripts/generate-release-lock.sh" ]] || fail "missing executable release lock generator: $ROOT/scripts/generate-release-lock.sh"
run "$ROOT/scripts/generate-release-lock.sh" --npm "$NPM_BIN"
}
print_command() {
printf '+'
printf ' %q' "$@"
@@ -276,6 +416,15 @@ while [[ $# -gt 0 ]]; do
TAG_MESSAGE="$2"
shift 2
;;
--skip-release-lock)
GENERATE_RELEASE_LOCK=0
shift
;;
--npm)
[[ $# -ge 2 ]] || fail "missing value for $1"
NPM_BIN="$2"
shift 2
;;
-y|--yes)
YES=1
shift
@@ -305,6 +454,9 @@ 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
declare -A PROJECT_NAMES
declare -A OLD_VERSIONS
@@ -349,6 +501,9 @@ fi
validate_version "$TARGET_VERSION" || fail "target version must be x.y.z: $TARGET_VERSION"
for repo in "${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
@@ -382,6 +537,11 @@ echo " version: $TARGET_VERSION"
echo " tag: $TAG"
echo " remote: $REMOTE"
echo " commit message: $COMMIT_MESSAGE"
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
for repo in "${REPOS[@]}"; do
@@ -394,13 +554,15 @@ confirm_release
for repo in "${REPOS[@]}"; do
if [[ "$DRY_RUN" -eq 1 ]]; then
echo "Would update $repo/pyproject.toml to $TARGET_VERSION"
echo "Would update version files in $repo to $TARGET_VERSION"
else
update_pyproject "$repo" "$TARGET_VERSION"
update_version_files "$repo" "$TARGET_VERSION"
fi
done
for repo in "${REPOS[@]}"; do
refresh_development_webui_lock
for repo in "${REPOS[@]:0:3}"; do
run git -C "$repo" add -A
if [[ "$DRY_RUN" -eq 0 ]]; then
@@ -413,10 +575,24 @@ for repo in "${REPOS[@]}"; do
run git -C "$repo" tag -a "$TAG" -m "$TAG_MESSAGE"
done
for repo in "${REPOS[@]}"; do
for repo in "${REPOS[@]:0:3}"; 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 [[ "$DRY_RUN" -eq 1 ]]; then
echo "Dry run complete for $TAG across all GovOPlaN repos."
else