# GovOPlaN Release Dependencies

This document owns release package composition, signed package catalogs,
license checks, catalog publishing, migration baselines, and the final release
checklist.

Operator runtime configuration and module install/uninstall execution live in
`DEPLOYMENT_OPERATOR_GUIDE.md`.

## Backend Packages

Release installs must not depend on sibling checkout paths. Local development
can keep editable installs and `file:` WebUI links, but release packaging must
resolve modules from tagged git refs or from a package registry.

Local development:

```bash
cd /mnt/DATA/git/govoplan
./.venv/bin/python -m pip install -r requirements-dev.txt
```

Release install from the meta checkout plus tagged module repositories:

```bash
cd /mnt/DATA/git/govoplan
./.venv/bin/python -m pip install -r requirements-release.txt
```

`../govoplan-core[server]` is resolved relative to the meta requirements file.
If you create the virtualenv elsewhere, still run the install command from the
meta checkout:

```bash
cd /mnt/DATA/git/govoplan
/tmp/govoplan-release-test/bin/python -m pip install -r requirements-release.txt
```

`requirements-release.txt` pins the module repositories to the release tag.
Update those refs when cutting a release:

```text
govoplan-tenancy      git@git.add-ideas.de:GovOPlaN/govoplan-tenancy.git      v0.1.8
govoplan-organizations git@git.add-ideas.de:GovOPlaN/govoplan-organizations.git v0.1.8
govoplan-identity     git@git.add-ideas.de:GovOPlaN/govoplan-identity.git     v0.1.8
govoplan-idm          git@git.add-ideas.de:GovOPlaN/govoplan-idm.git          v0.1.8
govoplan-access       git@git.add-ideas.de:GovOPlaN/govoplan-access.git       v0.1.8
govoplan-admin        git@git.add-ideas.de:GovOPlaN/govoplan-admin.git        v0.1.8
govoplan-policy       git@git.add-ideas.de:GovOPlaN/govoplan-policy.git       v0.1.8
govoplan-audit        git@git.add-ideas.de:GovOPlaN/govoplan-audit.git        v0.1.8
govoplan-dashboard    git@git.add-ideas.de:GovOPlaN/govoplan-dashboard.git    v0.1.8
govoplan-addresses    git@git.add-ideas.de:GovOPlaN/govoplan-addresses.git    v0.1.8
govoplan-files        git@git.add-ideas.de:GovOPlaN/govoplan-files.git        v0.1.8
govoplan-mail         git@git.add-ideas.de:GovOPlaN/govoplan-mail.git         v0.1.8
govoplan-campaign     git@git.add-ideas.de:GovOPlaN/govoplan-campaign.git     v0.1.8
govoplan-calendar     git@git.add-ideas.de:GovOPlaN/govoplan-calendar.git     v0.1.8
govoplan-poll         git@git.add-ideas.de:GovOPlaN/govoplan-poll.git         v0.1.8
govoplan-scheduling   git@git.add-ideas.de:GovOPlaN/govoplan-scheduling.git   v0.1.8
govoplan-notifications git@git.add-ideas.de:GovOPlaN/govoplan-notifications.git v0.1.8
govoplan-evaluation   git@git.add-ideas.de:GovOPlaN/govoplan-evaluation.git   v0.1.8
govoplan-docs         git@git.add-ideas.de:GovOPlaN/govoplan-docs.git         v0.1.8
govoplan-ops          git@git.add-ideas.de:GovOPlaN/govoplan-ops.git          v0.1.8
```

## WebUI Packages

Local development uses `webui/package.json`, which may point at sibling module
checkouts while active development is happening.

Release WebUI installs should use `webui/package.release.json`. It points
module dependencies at the same tagged git repositories. After the module tags
referenced there exist, generate the committed release lockfile without
touching the development package files:

```bash
cd /mnt/DATA/git/govoplan
tools/release/generate-release-lock.sh
cd /mnt/DATA/git/govoplan-core/webui
PATH=/home/zemion/.nvm/versions/node/v22.22.3/bin:$PATH /home/zemion/.nvm/versions/node/v22.22.3/bin/npm run build
```

Module repositories with a frontend include root-level npm package manifests
so the `@govoplan/*-webui` dependencies in `webui/package.release.json` can be
resolved from repository roots even though their source lives below
`webui/src`.

### Release Lockfile Strategy

The supported backend release composition is the set pinned in the meta
repository's `requirements-release.txt`. The supported frontend composition
is the independently buildable module set pinned in
`webui/package.release.json`; backend-only modules do not need a frontend
package entry. Keep one committed full-product release lockfile at
`webui/package-lock.release.json`, generated from
`webui/package.release.json` in a clean release workspace. Development
`package-lock.json` may continue to point at local `file:` dependencies.

Frontend module permutations are regression-tested through
`GOVOPLAN_WEBUI_MODULE_PACKAGES` and temporary build output, not through
committed lockfiles for every possible combination. If a smaller composition
becomes a separately shipped product, add an explicit release manifest and
lockfile pair for that product, for example
`package.release.files-mail.json` and `package-lock.release.files-mail.json`,
generated in a clean release workspace from tagged git dependencies.

## Release Tag Script

The normal release path is automated by `/mnt/DATA/git/govoplan/tools/release/push-release-tag.sh`: it bumps
or accepts the target version, updates Python/WebUI/module manifest versions,
commits/tags/pushes the module repositories first, regenerates
`webui/package-lock.release.json`, and then commits/tags/pushes core. If the
working tree has already been bumped, pass the current version explicitly:

```bash
cd /mnt/DATA/git/govoplan
tools/release/push-release-tag.sh --version 0.1.8
```

`/mnt/DATA/git/govoplan/tools/release/generate-release-catalog.py` reads installed/discovered
`ModuleManifest` objects while writing catalog entries. When a manifest is
available, the catalog entry uses the manifest version, points package refs at
`v<manifest.version>`, and copies `provides_interfaces` /
`requires_interfaces` from the manifest. It also copies module migration order
and `migration_tasks` metadata when present. If a manifest cannot be
discovered, the entry falls back to the release version passed with `--version`
and omits interface and migration-task metadata. This keeps the catalog aligned
with independently versioned module packages instead of relying on a hardcoded
compatibility table.

The script also includes GovOPlaN roadmap/scaffold module repositories that do
not yet have package metadata. Those repositories are committed, tagged, and
pushed with the same release tag, but they are tag-only until they contain
`pyproject.toml`, module manifests, or WebUI packages. Tag-only repositories
are not listed in `requirements-release.txt` or `webui/package.release.json`.

Current tag-only module repositories:

- `govoplan-appointments`
- `govoplan-cases`
- `govoplan-connectors`
- `govoplan-dms`
- `govoplan-erp`
- `govoplan-fit-connect`
- `govoplan-forms`
- `govoplan-identity-trust`
- `govoplan-ledger`
- `govoplan-payments`
- `govoplan-portal`
- `govoplan-postbox`
- `govoplan-reporting`
- `govoplan-search`
- `govoplan-tasks`
- `govoplan-templates`
- `govoplan-workflow-engine` (headless definitions, migrations, and execution)
- `govoplan-workflow` (optional authoring and inspection WebUI)
- `govoplan-xoev`
- `govoplan-xrechnung`
- `govoplan-xta-osci`

## Catalog Trust And Licensing

GovOPlaN module install and uninstall must remain operator-controlled. The
running server may plan and validate package changes, but package mutation is
performed by the separate installer daemon or an operator shell during
maintenance mode.

`addideas-govoplan-website` is the public static distribution surface for
official catalog resources:

- signed module package catalogs, grouped by release channel
- public catalog keyrings
- public license verification keyrings
- examples and operator-facing download paths

`govoplan-core` is the verifier and orchestrator:

- fetches a local or remote module catalog
- verifies catalog signatures against configured trusted keys
- enforces approved release channels
- rejects expired or not-yet-valid catalogs
- records accepted catalog sequence numbers for replay protection
- checks catalog entry license feature requirements before planning installs
- writes installer plans and request records

Feature and platform modules own their package artifacts, manifests, migration
metadata, retirement providers, and optional lifecycle behavior.

Core accepts either a local catalog file or a remote URL:

```bash
GOVOPLAN_MODULE_PACKAGE_CATALOG=/srv/govoplan/catalogs/stable.json
GOVOPLAN_MODULE_PACKAGE_CATALOG_URL=https://govoplan.example/catalogs/v1/channels/stable.json
GOVOPLAN_MODULE_PACKAGE_CATALOG_CACHE=/srv/govoplan/runtime/catalog-cache/stable.json
```

If both file and URL are set, the URL wins. The cache is used when a remote
fetch fails, so an operator can still inspect the last known catalog. A cached
catalog must still pass signature, freshness, channel, and replay validation.

An official catalog is a JSON object with:

- `catalog_version`
- `channel`
- `sequence`
- `generated_at`
- `not_before` when delayed activation is needed
- `expires_at`
- `modules`
- `signatures`

Each module entry can declare:

- backend package name and pinned install reference
- WebUI package name and pinned install reference
- display metadata and tags
- `license_features`, the feature entitlements required to plan that install
- `dependencies` and `optional_dependencies`, the module ids expected in the
  target module set
- `migration_safety`, one of `automatic`, `requires_review`, `forward_only`,
  or `destructive`
- `migration_notes`, operator-facing data/migration guidance for review,
  forward-only, or destructive changes
- `migration_after` and `migration_before`, explicit module ids used to order
  module-owned migration heads when a release needs a live-data sequencing rule
- `migration_tasks`, constrained live-data tasks that run around Alembic
  migration phases. Each task declares `task_id`, `phase`, `summary`,
  `task_version`, `safety`, `idempotent`, and optionally `timeout_seconds`.
  The allowed phases are `pre_migration_check`, `pre_migration_prepare`,
  `post_migration_backfill`, and `post_migration_verify`.
- `current_version_min` and `current_version_max_exclusive`, the installed
  version window from which this catalog target may be applied directly
- `bridge_release` and `bridge_notes`, marking a target as an intermediate
  compatibility release in a staged update path
- `allow_downgrade` and `allow_same_version`, explicit opt-ins for reviewed
  rollback or package-refresh plans
- `recovery_tested` and `recovery_notes`, documenting the rehearsal for
  forward-only or destructive data changes
- `provides_interfaces`, named interface contracts exported by this module
- `requires_interfaces`, named interface contracts and version ranges required
  by this module

The signature is Ed25519 over canonical JSON with both `signature` and
`signatures` removed. Core accepts the legacy single `signature` field and the
new `signatures` array.
When `APP_ENV` is `prod` or `production`, module package catalog signature
verification is required by default unless
`GOVOPLAN_MODULE_PACKAGE_CATALOG_REQUIRE_SIGNATURE=false` is set explicitly.

### Independent Module Versions And Interface Ranges

Modules do not need to ship on the same version number. Release catalogs should
pin each package to the exact backend/WebUI ref being installed and declare any
cross-module API compatibility through named interfaces.

Provider shape:

```json
"provides_interfaces": [
  { "name": "files.campaign_attachments", "version": "1.4.0" }
]
```

Requirement shape:

```json
"requires_interfaces": [
  {
    "name": "files.campaign_attachments",
    "version_min": "1.0.0",
    "version_max_exclusive": "2.0.0"
  }
]
```

`version_min` is inclusive. `version_max_exclusive` is exclusive, so the range
above means `>= 1.0.0` and `< 2.0.0`. Use this for SemVer major-version
compatibility lines. Set `"optional": true` only when the module can operate
without that interface being present. If a provider is installed but its
version is outside the declared optional range, activation is still blocked.

Catalog validation normalizes these fields and warns when catalog entries do
not satisfy each other's ranges. Registry activation and installer preflight
perform the blocking checks against the discovered installed manifests before
the desired module set is activated.
The admin module-management UI shows catalog warnings in the package catalog
section and repeats them as warning-level installer preflight issues while a
package install is planned.

Install-plan items carry a `source` field. Manually entered items use
`source: "manual"`; entries planned from the package catalog use
`source: "catalog"`. Catalog-sourced items also carry a `catalog` metadata
object with the validation snapshot used when the item was planned: catalog
source/path, source type, cache path, channel, sequence, generated/validity
timestamps, signature state, trusted key id, and cache state where available.
Catalog provenance changes preflight severity:

- catalog-sourced installs and updates require a configured, valid package
  catalog before activation
- invalid, untrusted, expired, not-yet-valid, replayed, or unapproved-channel
  catalogs block catalog-sourced installs and updates
- the same catalog validation failures remain warnings for manual install
  plans, so operators can still use offline or emergency package refs
- valid-catalog warnings, such as intentionally unsigned local catalogs when
  signature enforcement is disabled, remain warnings
- selected catalog entries with unsatisfied non-optional named interface ranges
  block activation before the installer runs
- selected catalog entries whose target dependencies are neither installed nor
  planned block activation before the installer runs
- catalog update targets older than the installed module version block unless
  the catalog entry declares `allow_downgrade: true`
- catalog update targets equal to the installed module version block unless the
  catalog entry declares `allow_same_version: true`
- catalog update targets with a `current_version_min` /
  `current_version_max_exclusive` window block when the installed version is
  outside that window; publish and apply a bridge release instead
- catalog entries marked `forward_only` or `destructive` block activation until
  the plan row has an explicit data-safety acknowledgement
- catalog entries marked `forward_only` or `destructive` also block unless the
  catalog entry declares `recovery_tested: true` and either the catalog entry or
  operator plan row contains recovery notes
- catalog entries marked `destructive` also require catalog migration notes or
  operator notes describing the cleanup or retirement plan

### Update Paths

Package updates are target-state operations, not one-module-at-a-time runtime
toggles. The safe unit of planning is a desired module version set plus a
catalog validation snapshot. The installer may install multiple packages into
the environment before activation, then validate the discovered manifests and
activate the resulting set together.

Install-plan rows support explicit `install`, `update`, and `uninstall`
actions. Catalog planning writes `update` when the module is already installed.
Preflight resolves the target set from installed manifests plus the planned
catalog entries. Unplanned catalog entries are not treated as installed. When a
catalog entry would satisfy a missing dependency or named interface, preflight
blocks activation with a companion-update issue; the admin catalog planner adds
those companion rows automatically when it can resolve them from the current
catalog. The preflight response also includes a structured `target_plan` summary
with each planned module's action, current version, catalog target version,
package refs, migration-safety level, current-version update window, bridge
metadata, recovery metadata, and acknowledgement state.

Database migrations are planned against that same target module set. When the
installer is run with `--migrate`, it calls `govoplan_core.commands.init_db`
with the target enabled modules rather than the pre-update startup module list,
so newly installed module migration directories are discovered before
activation. Preflight also returns a structured migration plan. Its step order is
derived from:

- manifest and catalog `migration_after` / `migration_before` declarations
- module dependencies and optional dependencies when both modules are in the
  target plan
- named interface provider/consumer relationships when both sides are in the
  target plan

Preflight blocks cycles in that ordering graph. It also blocks non-idempotent
module migration tasks, forward-only/destructive tasks without operator
acknowledgement, and installed manifest tasks that declare no executor.
Catalog-only task executors are marked as pending because they can only be
confirmed after the target package is installed. The migrator runs pre-migration
tasks, upgrades the ordered module heads first, finishes with Alembic `heads`,
and then runs post-migration tasks, so Alembic's revision graph remains
authoritative while GovOPlaN still gives operators a module-aware live-data
order.

This avoids circular "upgrade A first / upgrade B first" traps: named interface
requirements are solved against the target set, not against each intermediate
package-install moment. If the target set cannot satisfy all non-optional
interfaces and module dependencies at once, the plan is invalid. Operators
should add the necessary module updates to the same plan instead of trying to
force an order.

Live data upgrades need an even stricter rule:

- migrations must be idempotent and ordered by module migration metadata
- destructive schema/data changes need an explicit retirement or cleanup plan,
  not an automatic package update side effect
- cross-module data migrations must be compatible with both the old and target
  provider interface until activation finishes
- rollback must restore the package set and database state together, or be
  documented as forward-only with a tested recovery procedure
- if two modules require mutually incompatible live-data states, the catalog
  must publish an intermediate compatibility release rather than a circular
  update chain

The release catalog is the first safety gate for this. Generated catalogs mark
modules with registered migrations as `requires_review` by default. Release
authors should keep that value for ordinary reversible migrations, raise it to
`forward_only` when database rollback requires restoring a snapshot, and raise
it to `destructive` when the update removes or irreversibly rewrites persisted
data. Forward-only and destructive entries must include `recovery_tested: true`
and recovery notes after a verified restore or forward-recovery rehearsal. The
admin install-plan UI exposes the safety level and lets operators record an
explicit acknowledgement; preflight keeps acknowledged forward-only/destructive
changes visible as warnings.

In practice, circular dependencies are avoided by designing interfaces with
compatibility windows and by publishing bridge releases. A bridge release keeps
the old interface while introducing the new one, allowing dependent modules to
move first; a later release can retire the old interface after every dependent
module has a compatible target version. Use `current_version_min` and
`current_version_max_exclusive` to make those direct-update windows explicit in
the catalog, and set `bridge_release: true` on intermediate targets that exist
primarily to carry installations safely across a compatibility gap.

Trusted catalog keys are configured locally:

```bash
GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS_FILE=/srv/govoplan/trust/catalog-keyring.json
GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS='{"release-key-1":"<base64 public key>"}'
```

For development or tightly controlled deployments, a keyring can be read from a
URL and cached:

```bash
GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS_URL=https://govoplan.example/catalogs/v1/keyring.json
GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS_CACHE=/srv/govoplan/runtime/catalog-cache/keyring.json
```

Production installations should pin the trusted keyring locally or ship it
through deployment configuration. Fetching trusted keys from the same public
origin as the catalog is convenient, but that origin must not become the only
trust root.

## Dependency Audits

Dependency vulnerability checks are documented in
[`DEPENDENCY_AUDITS.md`](DEPENDENCY_AUDITS.md). The local audit runner is:

```bash
cd /mnt/DATA/git/govoplan
bash tools/checks/check-dependency-audits.sh
```

The Gitea workflow in `govoplan/.gitea/workflows/dependency-audit.yml` runs the same
check against release dependency refs on pushes, pull requests, and a weekly
schedule.

Keyring entries support:

- `key_id`
- `public_key` or `public_key_base64`
- `status`: `active`, `next`, `retired`, `revoked`, or `disabled`
- `not_before`
- `not_after`

Rotation process:

1. Add the next public key to the local trusted keyring with status `next`.
2. Publish catalogs signed by both current and next keys.
3. Upgrade installations so the next key is locally trusted.
4. Promote the next key to `active`.
5. Retire the old key only after every supported installation trusts the new
   key.
6. Mark a compromised key `revoked` and publish a higher sequence catalog
   signed by an uncompromised key.

Use replay state in production:

```bash
GOVOPLAN_MODULE_PACKAGE_CATALOG_SEQUENCE_STATE=/srv/govoplan/runtime/catalog-sequences.json
GOVOPLAN_MODULE_PACKAGE_CATALOG_ENFORCE_SEQUENCE=true
```

Core records the accepted sequence per channel after a catalog entry is planned
from the admin interface. With strict sequence enforcement, a previously
accepted sequence is rejected; without strict enforcement, only older sequences
are rejected. Catalogs should always expire.

The sequence state file is operational state, not a trust root. Keep it on
persistent storage and include it in normal backups:

```json
{
  "channels": {
    "stable": {
      "last_sequence": 42,
      "accepted_at": "2026-07-07T12:00:00Z",
      "key_id": "release-key-1",
      "source": "https://govoplan.example/catalogs/v1/channels/stable.json"
    }
  }
}
```

If the file is lost, restore it from backup. If no backup exists, reconstruct
each channel from the highest sequence already accepted in installer run
records, release records, or the currently deployed module package set. Do not
lower `last_sequence` to make an older catalog pass; publish a new higher
sequence catalog when the accepted point is uncertain.

If the file is corrupted, copy it aside for incident review, validate the
current signed catalog with channel and freshness enforcement, then rewrite the
state with the known accepted sequence. Keep
`GOVOPLAN_MODULE_PACKAGE_CATALOG_REQUIRE_SIGNATURE=true` and approved-channel
checks enabled during recovery. Temporarily disabling
`GOVOPLAN_MODULE_PACKAGE_CATALOG_ENFORCE_SEQUENCE` allows revalidating the same
sequence, but older sequences remain rejected once the reconstructed
`last_sequence` is in place.

Approved channels are deployment policy:

```bash
GOVOPLAN_MODULE_PACKAGE_CATALOG_APPROVED_CHANNELS=stable,lts
GOVOPLAN_MODULE_PACKAGE_CATALOG_REQUIRE_SIGNATURE=true
```

The admin UI can display other catalog metadata, but core rejects catalogs from
unapproved channels when validation is configured.

Catalog entries can require license features:

```json
"license_features": ["module.mail", "support.standard"]
```

Core checks those requirements against an offline license file before allowing
the entry into the install plan.

```bash
GOVOPLAN_LICENSE_FILE=/srv/govoplan/license.json
GOVOPLAN_LICENSE_ENFORCEMENT=true
GOVOPLAN_LICENSE_TRUSTED_KEYS_FILE=/srv/govoplan/trust/license-keyring.json
```

License files are JSON objects with:

- `license_id`
- `subject`
- `features`
- `valid_from`
- `valid_until`
- `signature`

Issue or renew a license from an operator/release shell that has the Ed25519
private key:

```bash
govoplan-module-installer \
  --issue-license /srv/govoplan/license.json \
  --license-id customer-2026-07 \
  --license-subject "Example Municipality" \
  --license-feature module.mail \
  --license-feature support.standard \
  --license-valid-until 2027-07-31T23:59:59Z \
  --license-signing-key-id license-issuer-1 \
  --license-signing-private-key /srv/govoplan/secrets/license-issuer-1.pem \
  --format json
```

Validate an imported license without exposing secrets:

```bash
govoplan-module-installer \
  --validate-license /srv/govoplan/license.json \
  --license-trusted-key license-issuer-1="<base64 public key>" \
  --require-trusted-license \
  --license-required-feature module.mail \
  --format json
```

The CLI and admin module catalog panel report the license id, subject,
validity window, signing key id, signed/trusted state, available features, and
missing entitlements for the configured package catalog. They do not expose
private signing material.

License enforcement can run in observe-only mode by leaving
`GOVOPLAN_LICENSE_ENFORCEMENT` unset. In that mode, missing or invalid license
data is surfaced as a warning but does not block planning.

Renewal is an ordinary re-issuance with a new `license_id`, extended
`valid_until`, and the full intended feature set. Import the renewed JSON to
`GOVOPLAN_LICENSE_FILE`, keep the previous file for audit, and validate it
before setting enforcement.

Revocation is handled through the trusted license keyring. Mark a compromised
or invalid issuer key as `revoked` or `disabled`, publish or deploy the updated
keyring, then reissue affected licenses with an active key. Installations that
run with `GOVOPLAN_LICENSE_ENFORCEMENT=true` reject licenses signed only by a
revoked key after the local keyring is updated.

Emergency fallback is deliberately explicit. Operators can temporarily unset
`GOVOPLAN_LICENSE_ENFORCEMENT` to keep package planning observable while a
license or keyring is recovered. Record the change in the operational incident
log, keep catalog signature and channel enforcement enabled, and restore
license enforcement after a trusted renewal validates successfully.

Licensing is intentionally separate from open-source code licensing. The
catalog/license mechanism can govern support channels, official release
eligibility, hosted update access, professional support, or commercial
entitlements without changing the source license of the repositories.

Production-grade distribution still needs remote registry/git artifact
resolution before package-manager apply, a hardened catalog publishing pipeline
that writes to `addideas-govoplan-website`, and automated key rotation and
emergency revocation drills.

## Release Catalog Publishing

GovOPlaN release catalogs are published to `addideas-govoplan-website` as
static JSON and verified by `govoplan-core` before installer plans are
accepted. Private signing keys must stay outside all git repositories. Public
keyrings are published with the website.

Create the first catalog signing key on the release machine:

```bash
cd /mnt/DATA/git/govoplan
KEY_DIR="$HOME/.config/govoplan/release-keys"
mkdir -p "$KEY_DIR"
./.venv/bin/python tools/release/generate-catalog-keypair.py \
  --key-id release-key-1 \
  --private-key "$KEY_DIR/release-key-1.pem" \
  --public-key "$KEY_DIR/release-key-1.pub" \
  --keyring "$KEY_DIR/catalog-keyring.json"
```

Keep `release-key-1.pem` private. The generated keyring contains only public
material.

Generate the signed catalog into `addideas-govoplan-website`:

```bash
cd /mnt/DATA/git/govoplan
KEY_DIR="$HOME/.config/govoplan/release-keys"
tools/release/publish-release-catalog.sh \
  --version <x.y.z> \
  --sequence 202607071340 \
  --catalog-signing-key "release-key-1=$KEY_DIR/release-key-1.pem" \
  --build-web
```

This writes:

- `/mnt/DATA/git/addideas-govoplan-website/public/catalogs/v1/channels/stable.json`
- `/mnt/DATA/git/addideas-govoplan-website/public/catalogs/v1/keyring.json`

The wrapper validates the catalog with core using the generated public keyring.

For normal module/core releases, first audit and record migration baselines,
then tag and push the module/core repos. Finally publish the website catalog:

```bash
cd /mnt/DATA/git/govoplan
./.venv/bin/python tools/release/release-migration-audit.py --strict
```

```bash
cd /mnt/DATA/git/govoplan
KEY_DIR="$HOME/.config/govoplan/release-keys"
tools/release/publish-release-catalog.sh \
  --version <x.y.z> \
  --catalog-signing-key "release-key-1=$KEY_DIR/release-key-1.pem" \
  --build-web \
  --commit \
  --tag \
  --push
```

The website tag is `catalog-v<x.y.z>`. The public URL is:

```text
https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json
```

The public keyring URL is:

```text
https://govoplan.add-ideas.de/catalogs/v1/keyring.json
```

`/mnt/DATA/git/govoplan/tools/release/push-release-tag.sh` can publish the web catalog after module and core
tags have been pushed. It runs the migration release audit in automatic mode:
warning-only before the first recorded migration baseline, strict after a
baseline exists. Add `--strict-migration-audit` when you want to force strict
mode explicitly:

```bash
cd /mnt/DATA/git/govoplan
KEY_DIR="$HOME/.config/govoplan/release-keys"
tools/release/push-release-tag.sh \
  --bump subversion \
  --strict-migration-audit \
  --publish-web-catalog \
  --catalog-signing-key "release-key-1=$KEY_DIR/release-key-1.pem" \
  --build-web-catalog
```

Use `--catalog-signing-key` more than once during a key rotation window. The
catalog will contain multiple signatures and the public keyring will include the
corresponding public keys.

## Release Doctor

Use `/mnt/DATA/git/govoplan/tools/release/release-doctor.py` before release preparation and again before
publishing. It inspects repository state, local versions, release tags,
migration audit state, release package refs, stable catalog/keyring state, and
optionally public catalog availability. The default mode is read-only and
offline. Status output is intentionally concise:

```bash
cd /mnt/DATA/git/govoplan
./.venv/bin/python tools/release/release-doctor.py status --target-version <x.y.z>
```

Use `--details` when the full findings should be printed directly:

```bash
./.venv/bin/python tools/release/release-doctor.py status --target-version <x.y.z> --details
```

For concise guidance, print only suggested next commands:

```bash
./.venv/bin/python tools/release/release-doctor.py next --target-version <x.y.z>
```

For an operator-guided session, run interactive mode. The doctor asks which
suggested command to run; mutating commands require typing `RUN` before they
execute:

```bash
./.venv/bin/python tools/release/release-doctor.py interactive --target-version <x.y.z>
```

Interactive mode starts with a compact summary and waits for an action. It can
open the full report in the configured pager, run one suggested command, repair
Git `safe.directory` dubious-ownership blocks after explicit confirmation, push
all clean repositories that are ahead of their upstream, or commit and push all
dirty repositories. The dirty-repository bulk action asks for a commit message,
skips repositories that are behind upstream, and requires typing
`COMMIT AND PUSH` before it stages, commits, and pushes.

Add `--online` when remote tag and public catalog/keyring reachability should be
checked. Add `--json` when a CI job or another tool should consume the report.

On a GovOPlaN installation that should consume the official stable catalog:

```bash
GOVOPLAN_MODULE_PACKAGE_CATALOG_URL=https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json
GOVOPLAN_MODULE_PACKAGE_CATALOG_CACHE=/srv/govoplan/runtime/catalog-cache/stable.json
GOVOPLAN_MODULE_PACKAGE_CATALOG_REQUIRE_SIGNATURE=true
GOVOPLAN_MODULE_PACKAGE_CATALOG_APPROVED_CHANNELS=stable
GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS_FILE=/srv/govoplan/trust/catalog-keyring.json
GOVOPLAN_MODULE_PACKAGE_CATALOG_SEQUENCE_STATE=/srv/govoplan/runtime/catalog-sequences.json
GOVOPLAN_MODULE_PACKAGE_CATALOG_ENFORCE_SEQUENCE=true
```

For production, copy the public keyring into deployment configuration and pin it
locally. Do not rely on a URL-fetched keyring as the only trust root.

`stable.json` includes a top-level `core_release` section for operator/update
tooling. Core is intentionally not listed as a normal module entry because it
must not be added to saved enabled-module state. Core upgrades should remain an
operator-supervised package update with restart and health checks.

Key rotation for published catalogs:

1. Generate the next private key outside git.
2. Run `/mnt/DATA/git/govoplan/tools/release/publish-release-catalog.sh` with both signing keys.
3. Publish the web catalog/keyring.
4. Roll the new public keyring into installations.
5. Stop signing with the old key after the supported fleet trusts the new key.
6. Mark compromised keys as revoked in the public keyring and publish a higher
   sequence catalog signed by a trusted uncompromised key.

## PostgreSQL Release Check

Release candidates should pass a disposable PostgreSQL migration and startup
smoke check before tagging or publishing catalogs. Start the local testbed,
then run the permutation check from the core checkout:

```bash
cd /mnt/DATA/git/govoplan/dev/postgres
cp .env.example .env
docker compose --env-file .env up -d

cd /mnt/DATA/git/govoplan
set -a
. /mnt/DATA/git/govoplan/dev/postgres/.env
set +a
tools/checks/postgres-integration-check.py \
  --database-url "$GOVOPLAN_POSTGRES_DATABASE_URL" \
  --reset-schema
```

The script checks migrations and `/health` startup for core-only, files-only,
mail-only, campaign-only, campaign+files, campaign+mail, and full-product
module sets. `--reset-schema` is destructive and must only be used against a
throwaway database. Before those permutations, the required Core proof runs in
random, test-owned schemas without modifying `public`. It exercises Files' real
credential-owning retirement provider and proves that credential scrubbing,
non-secret audit
insertion, and table retirement commit together; database-injected audit and
DDL failures roll the entire unit back. A 500 ms PostgreSQL `lock_timeout` and
captured backend process IDs also prove that each `DROP TABLE` uses the
installer Session connection instead of waiting through a second connection.
The meta check enables the release-gate flag so missing PostgreSQL configuration
or full-stack test packages are a hard failure; ordinary Core-only test discovery
skips this integration proof. Do not pass `--skip-retirement-atomicity` when
collecting release evidence.

## Migration Baselines

Release migration support follows `COMPATIBILITY_POLICY.md`: every tagged
`0.1.x` installation is a supported upgrade origin, released revision IDs are
immutable, and migration-only reconciliation remains available for at least one
subsequent major release cycle after the matching runtime compatibility path is
removed.

Development migrations may be small and numerous while a feature is moving.
GovOPlaN keeps those detailed migrations on an explicit development track and
publishes reviewed release shortcuts on the release track. Before a stable
release, unreleased development migrations may be squashed into a release-level
baseline or release-to-release upgrade migration. After a release tag has
shipped, released migration revision IDs are immutable.

The release policy is:

- unreleased development migrations live in `dev_versions` and are not deleted
  when a release shortcut is added;
- released migrations live in `versions` and are never rewritten or deleted;
- future release shortcuts are additive. A new release-to-release migration
  starts from the previous recorded release heads, so installations can upgrade
  through sane release steps instead of replaying every development revision;
- each stable release records the public migration head revisions in
  `docs/migration-release-baselines.json`;
- `heads` records the Alembic dependency-leaf graph heads for a full graph,
  while `owner_heads` records the latest migration revision per migration
  owner for subset installs and review;
- fresh installations should apply release-level baselines/upgrades, not
  unreleased create-then-rename churn;
- release-to-release schema changes should be folded into one reviewed
  migration per migration owner where practical.

The default runtime track is `release`:

```bash
cd /mnt/DATA/git/govoplan
GOVOPLAN_MIGRATION_TRACK=release ./.venv/bin/python -m govoplan_core.commands.init_db
```

Use `dev` only for local/disposable development databases that intentionally
need the detailed chain:

```bash
cd /mnt/DATA/git/govoplan
GOVOPLAN_MIGRATION_TRACK=dev ./.venv/bin/python -m govoplan_core.commands.init_db
```

Production, release checks, operator install flows, and the normal development
launcher should stay on the `release` track unless a fresh/disposable database
is intentionally being used to test the detailed development chain.

Audit the current graph during release preparation:

```bash
cd /mnt/DATA/git/govoplan
./.venv/bin/python tools/release/release-migration-audit.py
```

Audit the detailed development graph separately when a squash is prepared:

```bash
./.venv/bin/python tools/release/release-migration-audit.py --track dev
```

Generate the reviewed/manual squash checklist:

```bash
./.venv/bin/python tools/release/release-migration-audit.py --squash-plan
```

After the release migrations have been reviewed and the graph is final, record
the release baseline:

```bash
./.venv/bin/python tools/release/release-migration-audit.py --record-release <x.y.z>
```

Use strict mode to verify that the current heads are recorded:

```bash
./.venv/bin/python tools/release/release-migration-audit.py --strict
```

`/mnt/DATA/git/govoplan/tools/release/push-release-tag.sh` runs the audit by default in automatic mode:
non-strict while no release baseline exists, strict after the first baseline is
recorded. Pass `--warn-migration-audit` for an explicit non-strict audit,
`--strict-migration-audit` to force strict mode, or `--skip-migration-audit`
only for emergency/manual release work.

The first public baseline is v0.1.7. It intentionally adds release-track
shortcuts for the unreleased v0.0.0 -> v0.1.7 development chains while keeping
the detailed chains on the `dev` track. No production installations existed
before that baseline, so pre-v0.1.7 development revisions are not release
upgrade targets. Future release-to-release changes must start from a recorded
release baseline and add a new release-track step-up instead of replacing prior
release shortcuts. The tracking issue is
`GovOPlaN/govoplan-core#223`.

## Related Operator Documents

- `DEPLOYMENT_OPERATOR_GUIDE.md`: runtime environment, explicit migrations,
  backup/restore commands, module installer daemon/supervisor operation, and
  rollback drills.
- `REMOTE_WEBUI_BUNDLES.md`: experimental browser-loaded module bundles for
  controlled deployments; normal releases use package builds.

## Release Checklist

- Keep Python package versions, WebUI package versions, and git tags aligned.
- Tag core, access, admin, tenancy, policy, audit, files, mail, campaign,
  calendar, and scaffold module repositories together.
- Update meta `requirements-release.txt` and core `webui/package.release.json` when the
  release tag changes.
- Generate the committed full-product release lockfile from
  `package.release.json` with `/mnt/DATA/git/govoplan/tools/release/generate-release-lock.sh`.
- Run `/mnt/DATA/git/govoplan/tools/release/release-migration-audit.py --strict` after recording a release
  baseline.
- Run the PostgreSQL release check against a disposable database.
- Publish the signed catalog through the release catalog publishing flow above.
- Add separate release manifest/lockfile pairs only for module compositions
  that are shipped as their own products.
- Do not commit local sibling paths into release manifests.
