feat(release): enforce aligned trusted publications
This commit is contained in:
@@ -52,28 +52,10 @@ Options:
|
||||
-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.
|
||||
Core, every registered release/module repository listed below in this script,
|
||||
and the meta repository. Repositories with pyproject.toml are versioned and
|
||||
alignment-gated dynamically; remaining roadmap/scaffold repositories are
|
||||
committed and tagged without inventing package metadata.
|
||||
|
||||
Examples:
|
||||
tools/release/push-release-tag.sh
|
||||
@@ -121,12 +103,14 @@ PACKAGE_MODULE_REPOS=(
|
||||
"$PARENT/govoplan-organizations"
|
||||
"$PARENT/govoplan-permits"
|
||||
"$PARENT/govoplan-policy"
|
||||
"$PARENT/govoplan-poll"
|
||||
"$PARENT/govoplan-procurement"
|
||||
"$PARENT/govoplan-records"
|
||||
"$PARENT/govoplan-resources"
|
||||
"$PARENT/govoplan-risk-compliance"
|
||||
"$PARENT/govoplan-tenancy"
|
||||
"$PARENT/govoplan-transparency"
|
||||
"$PARENT/govoplan-evaluation"
|
||||
)
|
||||
TAG_ONLY_MODULE_REPOS=(
|
||||
"$PARENT/govoplan-addresses"
|
||||
@@ -134,6 +118,7 @@ TAG_ONLY_MODULE_REPOS=(
|
||||
"$PARENT/govoplan-cases"
|
||||
"$PARENT/govoplan-connectors"
|
||||
"$PARENT/govoplan-dms"
|
||||
"$PARENT/govoplan-dist-lists"
|
||||
"$PARENT/govoplan-erp"
|
||||
"$PARENT/govoplan-fit-connect"
|
||||
"$PARENT/govoplan-forms"
|
||||
@@ -144,8 +129,10 @@ TAG_ONLY_MODULE_REPOS=(
|
||||
"$PARENT/govoplan-portal"
|
||||
"$PARENT/govoplan-postbox"
|
||||
"$PARENT/govoplan-reporting"
|
||||
"$PARENT/govoplan-rest"
|
||||
"$PARENT/govoplan-scheduling"
|
||||
"$PARENT/govoplan-search"
|
||||
"$PARENT/govoplan-soap"
|
||||
"$PARENT/govoplan-tasks"
|
||||
"$PARENT/govoplan-templates"
|
||||
"$PARENT/govoplan-workflow"
|
||||
@@ -154,10 +141,12 @@ TAG_ONLY_MODULE_REPOS=(
|
||||
"$PARENT/govoplan-xta-osci"
|
||||
)
|
||||
MODULE_REPOS=("${PACKAGE_MODULE_REPOS[@]}" "${TAG_ONLY_MODULE_REPOS[@]}")
|
||||
PACKAGE_REPOS=("${PACKAGE_MODULE_REPOS[@]}" "$ROOT")
|
||||
SUPPORT_REPOS=("$META_ROOT")
|
||||
PACKAGE_REPOS=()
|
||||
REPOS=(
|
||||
"${MODULE_REPOS[@]}"
|
||||
"$ROOT"
|
||||
"${SUPPORT_REPOS[@]}"
|
||||
)
|
||||
|
||||
REMOTE="origin"
|
||||
@@ -379,6 +368,7 @@ if project_name != "govoplan-core":
|
||||
peers["@govoplan/core-webui"] = f"^{new_version}"
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
PYCODE
|
||||
synchronize_lockfile_root "$package_path" "${package_path%package.json}package-lock.json"
|
||||
done
|
||||
|
||||
if [[ "$project_name" == "govoplan-core" && -f "$release_package_path" ]]; then
|
||||
@@ -401,9 +391,59 @@ for name, spec in list(dependencies.items()):
|
||||
dependencies[name] = re.sub(r"#v\d+\.\d+\.\d+$", f"#v{new_version}", spec)
|
||||
path.write_text(json.dumps(data, indent=2) + "\n")
|
||||
PYCODE
|
||||
synchronize_lockfile_root "$release_package_path" "$repo/webui/package-lock.release.json"
|
||||
fi
|
||||
}
|
||||
|
||||
synchronize_lockfile_root() {
|
||||
local package_path="$1"
|
||||
local lock_path="$2"
|
||||
[[ -f "$package_path" && -f "$lock_path" ]] || return 0
|
||||
"$PYTHON" - "$package_path" "$lock_path" <<'PYCODE'
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
package_path = Path(sys.argv[1])
|
||||
lock_path = Path(sys.argv[2])
|
||||
package = json.loads(package_path.read_text())
|
||||
lock = json.loads(lock_path.read_text())
|
||||
version = package.get("version")
|
||||
if not isinstance(version, str) or not version:
|
||||
raise SystemExit(f"package version is missing from {package_path}")
|
||||
lock["version"] = version
|
||||
packages = lock.get("packages")
|
||||
if isinstance(packages, dict) and isinstance(packages.get(""), dict):
|
||||
packages[""]["version"] = version
|
||||
lock_path.write_text(json.dumps(lock, indent=2) + "\n")
|
||||
PYCODE
|
||||
}
|
||||
|
||||
update_release_requirements() {
|
||||
local version="$1"
|
||||
"$PYTHON" - "$META_ROOT/requirements-release.txt" "$version" <<'PYCODE'
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
version = sys.argv[2]
|
||||
text = path.read_text()
|
||||
updated, count = re.subn(
|
||||
r"(?m)^(govoplan-[a-z0-9-]+\s+@\s+git\+ssh://[^\s]+\.git)@v[^\s]+$",
|
||||
rf"\1@v{version}",
|
||||
text,
|
||||
)
|
||||
if count == 0:
|
||||
raise SystemExit(f"no tagged GovOPlaN requirements found in {path}")
|
||||
path.write_text(updated)
|
||||
PYCODE
|
||||
}
|
||||
|
||||
update_version_files() {
|
||||
local repo="$1"
|
||||
local version="$2"
|
||||
@@ -427,7 +467,30 @@ generate_release_lock() {
|
||||
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"
|
||||
local command=("$META_ROOT/tools/release/generate-release-lock.sh" --core-root "$ROOT" --npm "$NPM_BIN")
|
||||
local repo
|
||||
for repo in "${MODULE_REPOS[@]}"; do
|
||||
command+=(--local-git-repo "$repo")
|
||||
done
|
||||
run "${command[@]}"
|
||||
}
|
||||
|
||||
run_version_alignment_gate() {
|
||||
local mode="${1:-full}"
|
||||
local command=(
|
||||
"$PYTHON"
|
||||
"$META_ROOT/tools/checks/check-version-alignment.py"
|
||||
--workspace-root "$PARENT"
|
||||
--release-composition
|
||||
)
|
||||
if [[ "$mode" == "source" ]]; then
|
||||
command+=(--source-metadata-only)
|
||||
fi
|
||||
local repo
|
||||
for repo in "${PACKAGE_REPOS[@]}"; do
|
||||
command+=(--repo-version "$(basename "$repo")=$TARGET_VERSION")
|
||||
done
|
||||
run "${command[@]}"
|
||||
}
|
||||
|
||||
run_migration_release_audit() {
|
||||
@@ -694,6 +757,15 @@ for repo in "${REPOS[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
EFFECTIVE_TAG_ONLY_REPOS=()
|
||||
for repo in "${REPOS[@]}"; do
|
||||
if [[ "${REPO_KINDS[$repo]}" == "package" ]]; then
|
||||
PACKAGE_REPOS+=("$repo")
|
||||
else
|
||||
EFFECTIVE_TAG_ONLY_REPOS+=("$repo")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ -z "$TARGET_VERSION" ]]; then
|
||||
BASE_VERSION="${OLD_VERSIONS[$ROOT]}"
|
||||
for repo in "${PACKAGE_REPOS[@]}"; do
|
||||
@@ -744,9 +816,9 @@ 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"
|
||||
echo " tag-only/support repos: ${#EFFECTIVE_TAG_ONLY_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"
|
||||
echo " release lock: resolve core webui/package-lock.release.json from local reviewed module tags"
|
||||
else
|
||||
echo " release lock: skipped"
|
||||
fi
|
||||
@@ -780,9 +852,21 @@ for repo in "${PACKAGE_REPOS[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo "Would update $META_ROOT/requirements-release.txt to $TAG"
|
||||
else
|
||||
update_release_requirements "$TARGET_VERSION"
|
||||
fi
|
||||
|
||||
refresh_development_webui_lock
|
||||
|
||||
for repo in "${MODULE_REPOS[@]}"; do
|
||||
# Source metadata, release refs, and lock roots must agree before creating any
|
||||
# release commit or tag. A second gate below verifies the fully resolved release
|
||||
# lock before any remote mutation.
|
||||
run_version_alignment_gate source
|
||||
|
||||
PRE_CORE_REPOS=("${MODULE_REPOS[@]}" "${SUPPORT_REPOS[@]}")
|
||||
for repo in "${PRE_CORE_REPOS[@]}"; do
|
||||
run git -C "$repo" add -A
|
||||
|
||||
if [[ "$DRY_RUN" -eq 0 ]]; then
|
||||
@@ -806,12 +890,13 @@ for repo in "${MODULE_REPOS[@]}"; do
|
||||
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
|
||||
|
||||
# npm resolves the new module tags from the local repositories. Nothing has
|
||||
# been pushed yet; a failed lock or alignment check leaves only local,
|
||||
# recoverable commits/tags.
|
||||
run_version_alignment_gate
|
||||
|
||||
run git -C "$ROOT" add -A
|
||||
|
||||
if [[ "$DRY_RUN" -eq 0 ]]; then
|
||||
@@ -822,6 +907,10 @@ fi
|
||||
|
||||
run git -C "$ROOT" commit -m "$COMMIT_MESSAGE"
|
||||
run git -C "$ROOT" tag -a "$TAG" -m "$TAG_MESSAGE"
|
||||
|
||||
for repo in "${PRE_CORE_REPOS[@]}"; do
|
||||
run git -C "$repo" push --atomic "$REMOTE" "HEAD:refs/heads/${BRANCHES[$repo]}" "refs/tags/$TAG"
|
||||
done
|
||||
run git -C "$ROOT" push --atomic "$REMOTE" "HEAD:refs/heads/${BRANCHES[$ROOT]}" "refs/tags/$TAG"
|
||||
|
||||
if [[ "$PUBLISH_WEB_CATALOG" -eq 1 ]]; then
|
||||
|
||||
Reference in New Issue
Block a user