Sync Repo-docs-RELEASE-DEPENDENCIES from project files
@@ -0,0 +1,966 @@
|
||||
<!-- codex-wiki-sync:29fbd26651e01c2d851bf22f -->
|
||||
|
||||
> Mirrored from `/mnt/DATA/git/govoplan-core/docs/RELEASE_DEPENDENCIES.md`.
|
||||
> Origin: `repository`.
|
||||
> Active tasks and changing state belong in Gitea issues; this wiki page is durable project context.
|
||||
|
||||
---
|
||||
# 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.
|
||||
|
||||
If neither source is configured, the Admin package directory discovers the
|
||||
official public stable catalog at
|
||||
`https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json`. Core verifies
|
||||
that fallback against the public key pinned in the installed Core package. An
|
||||
explicit deployment catalog always takes precedence; a configured source that
|
||||
is unavailable or invalid fails closed instead of silently falling back.
|
||||
|
||||
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
|
||||
- `artifact_integrity` for each package, including the HTTPS registry URL,
|
||||
filename, byte size, SHA-256, package identity, source tag, and source commit
|
||||
- `source`, binding the repository and immutable tag/commit identity, with
|
||||
optional HTTPS repository and revision links
|
||||
- `availability`, either `available` or `withdrawn`; a withdrawn entry must
|
||||
carry an operator-readable `availability_reason` and cannot be planned
|
||||
- `configuration_requirements` and an optional HTTPS `release_notes_url` for
|
||||
prerequisites and release-specific operator guidance
|
||||
- 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
|
||||
|
||||
Core validates these fields before exposing the directory. Admin derives a
|
||||
read-only catalog state from the installed package set, catalog dependency
|
||||
closure, named-interface providers, current-version window, availability, and
|
||||
generic license policy. This is an early operator diagnostic; trusted installer
|
||||
preflight remains the authoritative mutation gate.
|
||||
|
||||
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
|
||||
- a saved catalog plan must match the currently validated entry exactly;
|
||||
altered package refs, artifact identities, channel, sequence, trust state, or
|
||||
signing-key identity block the run and require replanning
|
||||
- a trusted remote artifact is downloaded before mutation into a private
|
||||
SHA-256-addressed installer cache, checked for exact size and digest, and
|
||||
passed to `pip` or npm only as that verified local file
|
||||
- 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.
|
||||
|
||||
Official open-source GovOPlaN entries do not declare license features. The
|
||||
license contract remains generic for external catalogs, deployment presets,
|
||||
configuration/package directories, and support offerings; it gates only an
|
||||
entry that explicitly asks for a feature.
|
||||
|
||||
```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.
|
||||
Reference in New Issue
Block a user