# Package Registry Releases GovOPlaN publishes reusable module artifacts through Gitea's native PyPI and npm registries. These packages improve developer installation, release resolution, cacheability, and artifact inspection. They do not replace the signed runtime distribution: the signed manifest and digest-pinned OCI images remain the production deployment authority. ## Publication boundary Every repository with a `pyproject.toml` contains `.gitea/workflows/module-package-release.yml`. The meta repository owns the canonical template and installs it with: ```bash python tools/repo/sync-module-package-workflows.py --write python tools/repo/sync-module-package-workflows.py --check ``` The workflow runs for `v*` tags and may be dispatched manually for an existing tag. The organization preflight verifies that every package repository protects the `v*` namespace. Before building, the workflow itself verifies that: - the tagged commit is contained in `main`; - the tag, Python project version, and optional WebUI package version agree; - package names remain in the `govoplan-*` and `@govoplan/*-webui` namespaces. The workflow binds the repository explicitly from the Gitea Actions context. Do not rely on GitHub-compatible environment variables being injected by the runner image; Gitea runners may expose only the context values. Gitea 1.24 job tokens cannot read repository tag-protection settings, so package jobs must not receive a broad administrator token merely to repeat the organization preflight. Run the following before the first publication and after repository or tag-rule changes: ```bash python tools/gitea/gitea-configure-package-releases.py ``` Preview and dispatch the exact wheel/WebUI versions selected by the developer meta-package with: ```bash python tools/gitea/gitea-dispatch-package-set.py \ --env-file ~/.config/gitea/gitea.env python tools/gitea/gitea-dispatch-package-set.py \ --env-file ~/.config/gitea/gitea.env \ --apply ``` The dispatcher reads exact versions from `packages/govoplan-meta/pyproject.toml`, inspects the selected tag to determine whether a WebUI package is expected, skips complete registry pairs and does not duplicate an active workflow. Use `--repository govoplan-core` for a bounded dispatch or `--verify-existing` to rebuild and hash-verify versions already present in both registries. For coordinated lockstep tags, `push-release-tag.sh` pushes module tags first, Core next, and the meta tag last. This is a dependency guarantee for a single-capacity Actions runner: the developer package cannot run before its exact Core and module versions have entered the queue. The same release entry point first validates the migration graph, then records the reviewed current Alembic heads under the target release version and reruns the strict migration audit before it changes package versions, commits, or tags. The default preflight intentionally does not require those heads to exist in the previous release baseline. A failed candidate-baseline check therefore cannot produce a protected package release. The source gate validates `pyproject.toml`, the module version declaration (`MODULE_VERSION` or the top-level `ModuleManifest.version`), public package `__version__`, and WebUI metadata before creating tags. Release-tag artifact checks run only after the candidate tags and immutable WebUI lock have been created locally. Release-lock regeneration resolves a fresh immutable lock from the reviewed candidate manifests; it does not seed resolution from the previous release lock. This prevents removed transitive packages and stale peer metadata from blocking or contaminating the new release. Candidate resolution also uses an isolated temporary npm cache, so a locally replaced tag cannot reuse metadata from a failed, unpushed release attempt. Modules that retain the same WebUI package identity in both a root publish manifest and `webui/package.json` use the WebUI manifest as the canonical peer contract. The coordinated release synchronizes `peerDependencies` and `peerDependenciesMeta` into the publish manifest before creating the module tag, then synchronizes each lockfile root from the final package metadata. A distinct root package remains independent. Every module referenced by Core's Git-based `package.release.json` must expose its WebUI identity at the repository root, including matching peer requirements and `webui/`-prefixed entry exports (also CSS subpaths). npm resolves Git dependencies from the repository root, while the native-package workflow packs `webui/`; success in one path does not verify the other. Run `python tools/checks/check-webui-package-facades.py` after changing either manifest or the release composition. The focused gate also runs this check. Adding or correcting a facade in an already published repository requires a new patch tag; never repair an existing immutable tag in place. It builds one wheel and, where applicable, one npm tarball. The workflow records the source tag, source commit, filename, size, and SHA-256 in `package-artifacts.json` before publishing. Gitea rejects a second upload of the same package version, so correction requires a new version rather than artifact replacement. A retry after partial publication is safe. Before upload, the workflow reads the native package registry file record and compares its SHA-256 with the artifact rebuilt from the protected tag. An exact existing artifact is skipped; a same-version artifact with another digest or an unexpected file set fails closed. This permits a failed npm publication to resume without weakening package immutability or accepting `--skip-existing` blindly. The npm tarball is always published through an explicit local `./dist/...` path. Without that prefix, npm may interpret a relative tarball name as a Git package shorthand before it ever contacts the configured registry. Published WebUI packages contain registry-compatible dependencies only. The workflow converts an internal dependency pinned to a protected `vX.Y.Z` Git tag into the exact `X.Y.Z` registry version and rejects unresolved `file:` or Git dependencies. Repository development metadata may therefore keep local or Git references without leaking them into the published package contract. Historical `add-ideas` and current `GovOPlaN` organization URLs are accepted for immutable tagged releases; both normalize to the same exact registry dependency and no branch or unversioned Git reference is accepted. ## One-time Gitea setup Protect `v*` tags in every package repository and the meta repository. Allow only the `Owners` team to create or delete those tags. ```bash set -a . ~/.config/gitea/gitea.env set +a python tools/gitea/gitea-configure-package-releases.py --apply ``` Create a dedicated personal access token with only `write:package` scope and store these organization-level Actions secrets on `GovOPlaN`: - `GOVOPLAN_PACKAGE_USERNAME`: account owning the package token; - `GOVOPLAN_PACKAGE_TOKEN`: dedicated package-write token. Do not use an administrator or general release token. Gitea 1.24 does not grant package publication to the automatic Actions job token. Organization secrets allow the same least-privilege credential to serve every module workflow. ## Exact release consumption `tools/release/generate-release-package-set.py` supports two explicit package profiles. `base` translates the reviewed roots in `requirements-release.txt`; `full` reads the exact `govoplan[full]` dependency set from the developer meta-package. Both profiles resolve every version tag to its commit and verify the package metadata from that exact Git tree. The official module directory and immutable runtime distribution use `full`, so every publicly released module can be discovered without rebuilding the application image. `tools/release/resolve-package-artifacts.py` then downloads exactly those wheel and WebUI versions from Gitea. It reads the identity embedded in every wheel and npm tarball, rejects missing, duplicate, unexpected, or oversized artifacts, and writes `package-artifacts.lock.json` with credential-free HTTPS download URLs, SHA-256 values, and npm registry integrity values. The resolver verifies that the bytes downloaded by `npm pack` match the registry's own integrity record. Credentials are accepted only through environment variables and are never written to the lock. Python resolution ignores ambient pip configuration and extra indexes for GovOPlaN roots, preventing an internal package name from being selected from an undeclared registry. The runtime distribution workflow uses the verified full-profile wheelhouse directly and installs every selected module WebUI tarball only after matching it to the lock. It publishes the package set, package lock, and hash-locked requirements as release assets. The WebUI installer receives the absolute runtime-build interpreter path so its directory changes cannot escape the isolated release environment. Gitea 1.24 dispatches this workflow from a branch, but that branch is only the workflow implementation. The job fetches and peels the protected `v` tag explicitly and materializes both `requirements-release.txt` and the developer meta-package from that Git tree. It then binds the signed distribution source and Gitea release assets to the same exact commit. A post-tag workflow repair can therefore retry publication without changing the released package composition or relabelling the later branch commit as released source. The package-lock SHA-256 is part of the signed distribution manifest. Runtime finalization also requires the lock's package versions and hashes to match the wheel composition embedded in the images. OCI assembly remains network-free after package and third-party dependency resolution. The source refs remain in the module catalog for source provenance and release planning. Production installation consumes the signed runtime images rather than invoking `pip`, `npm`, or Git on the target host. ## Public module directory For an operator-reviewed full publication, use `tools/release/release-catalog.py full-registry` followed by the same tool's `publish-candidate` command. Resolve the package set and registry lock first; the older direct-write shell wrapper is not the strict candidate publication path. See [Full registry candidates / Vollständige Registry-Kandidaten](FULL_REGISTRY_CANDIDATES.md) for the private host runtime, exact artifact checks, and legacy keyring transition. Catalog entries are synthesized from the exact tagged module manifests, never from a hand-maintained module list or the current workspace. Each entry binds its Python wheel and optional WebUI tarball to the registry URL, filename, size, SHA-256, package identity, source tag, and source commit before the complete catalog is signed. The same publication transaction regenerates and prunes the browsable static directory under `public/catalogs/v1/modules/`. It writes a global `modules/index.json`, one `/index.json`, and one `//manifest.json` for every entry in the signed channel. These files are derived from that exact signed payload and keyring; stale JSON from an older partial catalog is removed while unrelated static assets are left untouched. The signed channel remains the trust anchor, while the module directory provides stable discovery URLs for browsers and external tooling. Official GovOPlaN modules are open-source directory entries and do not require license entitlements. The generic `license_features` contract remains available for third-party package directories, support/configuration packages, or future deployment-specific presets. A catalog entry is gated only when that entry explicitly declares such features. Core carries the public stable catalog URL and its independently pinned trust anchor. In the absence of an operator-configured catalog, Admin discovers the official directory automatically. Selecting an entry creates a reviewed install/update plan; the trusted installer downloads the exact signed artifacts into a private digest cache, verifies size and hash, and installs only from that cache. A saved plan is rejected if any package ref, artifact identity, catalog channel, sequence, or signing-key identity differs from the currently validated catalog. The Admin directory can be searched by module, package, repository, or tag and filtered by available, installed, update, and blocked/withdrawn states. It shows the source revision, artifact digest, release notes, and configuration requirements. Missing dependency/interface providers and unsupported update windows are surfaced before an operator adds the entry to a plan; installer preflight remains authoritative. Catalog entries also carry the permission definitions declared by the tagged module manifest. Admin groups and exposes their scopes before an install or update is planned. This is disclosure only: installing a module does not grant its permissions to an account, role, group, tenant, or service account. Package lifecycle and availability are intentionally separate: - install, update, and uninstall change the instance-wide package composition; - enable and disable change the active instance runtime graph; - tenant module entitlements define unavailable, available, and forced modules; - group/user presentation is governed through Views and Policy; and - enabling a capability module does not opt data into that capability. Single-process or single-host installations may execute a supervised package plan locally. Shared-state and Kubernetes profiles reject node-local package mutation: operators compose and roll out a new signed full-profile runtime image instead. This prevents replicas from drifting while retaining the same Admin catalog and preflight experience. ## Developer meta-package `packages/govoplan-meta` builds the optional `govoplan` package. Its default dependencies mirror the reviewed runtime roots; `govoplan[full]` adds all currently packageable workspace modules. Regenerate it after changing release requirements or package versions: ```bash python tools/release/generate-developer-meta-package.py python tools/release/generate-developer-meta-package.py --check ``` The direct generator is a development synchronization tool, not a receipt-gated release executor. For release preparation, use the guarded out-of-run stage below. `push-release-tag.sh` performs this synchronization before release commits and tags. The meta-package is for editable/developer setup and composition tests. It does not enable modules, apply migrations, provision services, or establish backup and recovery evidence. ### Shared source-tag contract and Meta composition The shared version collector names Meta's real `packages/govoplan-meta/pyproject.toml` separately from root `pyproject.toml`. Only the registered `govoplan` system/meta repository with nested project name `govoplan` receives this contract. Missing, unknown, or misidentified metadata does not become a versionless exception. Version alignment compares the complete nested file with the canonical operator-tool generator output: its version must match Core, and dependencies and `full` composition must match the reviewed requirements and workspace package versions. Validation never executes a generator from a selected checkout. The shared trusted manifest checker can load reviewed application manifests; these checks are not a code sandbox. Meta's complete generated file is recognized by shared version-mutation discovery, but the generic durable version executor deliberately cannot write it. A durable run freezes the release console's own Meta checkout as trusted runtime code; changing it in place would invalidate that run. The planner therefore places Meta after Core and exposes only non-executable support preparation/publication steps, not misleading automatic Meta version, commit, tag, or push actions. A missing or different Core target produces an actionable preparation prerequisite. Prepare Core and the intended module inputs first, commit their reviewed state, then stop active durable runs for the target workspace. Use trusted operator tools against a separate registered, private source checkout, never the running operator Meta directory. Preview outside any selected source checkout, for example: ```sh python tools/release/prepare-developer-meta-package.py \ --workspace /private/release-workspace --target-version X.Y.Z \ > /private/operator/meta-preview.json python tools/release/prepare-developer-meta-package.py \ --workspace /private/release-workspace --target-version X.Y.Z \ --receipt /private/operator/meta-preview.json --apply --confirm-out-of-run ``` The explicit confirmation attests that no durable run is active for that target workspace; the helper does not discover or stop other processes. Preview/apply requires registered clean main sources, matching origins and live-main ancestry, Core already aligned at the target, the exact nested identity, and no existing or unverifiable target Meta tag. Frozen receipts cover source HEADs/filesystem identities, release requirements, every discovered registered full-composition pyproject, the trusted generator snapshot, and the resulting full-file hash. Inputs are limited to 128 selected files, 2 MiB per file and 16 MiB aggregate. The canonical generator renders copied bounded data in a temporary directory; no generator from the selected checkout executes. Changed receipts block before the file effect. Apply writes only `packages/govoplan-meta/pyproject.toml`, then rechecks the other sources and exact output. A write or post-check failure that may have changed the file reports `needs-reconciliation` and leaves that bounded delta for explicit review; it never retries, rolls back, commits or publishes. Review the complete generated composition and manually commit the resulting file. Complete matching Core publication before guarded Meta source tagging/publication, then start a fresh durable run from reviewed, clean, published operator tooling. Hot self-updating durable Meta release execution remains explicitly unsupported; this out-of-run preparation is the existing developer-meta support contract. For every `tag_repositories` batch, strict checks apply to every selected repository before any tag creation, fetch, or push: registered checkout and origin/push URL, a clean `main` tracking `origin/main`, live remote-main ancestry, exact frozen HEADs, and immutable annotated local/remote tag objects. Git metadata must remain inside the operator's trusted workspace. Missing local knowledge of live remote main is a blocker; fetch and review it separately. Unknown repositories and non-registered remote aliases fail closed. If selected, Meta runs last. For Meta only, the matching annotated Core release must exist before its effect; Core may be an earlier selected repository, or an already tagged dependency. Publication requires that Core's exact tag and main commit are already remote. Non-Meta selections do not acquire Meta's composition or Core-tag prerequisite. Local module-candidate tags still work before Core's final release lock or tag. The existing Core WebUI bundle gate still applies to module publication and batches selecting Core: relevant Core release-package and release-lock inputs must be operator-owned regular files, at most 16 MiB each, and their identities and content hashes are frozen before preflight and rechecked before every effect. When Core is unselected, this does not require its checkout to be clean or tagged; reviewed pending composition inputs retain their previous meaning. Backend-only selections never read irrelevant Core WebUI files. Before even read-only Git commands, source ancestry must be owned by root or the current operator and must not be group/world writable. A sticky shared ancestor such as `/tmp` is permitted only above an owned, protected child; the workspace and checkouts receive no writable-directory exception. The current operator must own source inputs and actual Git/worktree/common metadata, which must be regular files/directories, non-symlinked, and non-writable by other users. Metadata walks are bounded to 500,000 entries and 128 levels, and tracked inputs to 100,000 paths and 16 MiB of listing text. Read-only Git targets, object alternates/grafts and hidden/sparse/unmerged index entries are blocked. Frozen receipts include actual checkout/Git directory paths, devices, inodes, owners and modes, so replacing Git metadata with the same HEAD is still detected. All selected version/composition inputs must be tracked, including root and WebUI package/lock metadata, discovered module manifests and package initializers, and Meta's nested package and release requirements; ignored working files cannot supply declarations absent from a tag. No chmod, ownership repair or global Git trust change is performed. A shared writable workspace must first be recreated or reviewed in the operator's protected release area by an explicitly authorized preparation workflow. Preview is read-only. Local-tag mode creates only pinned annotated tags (or retrieves an identical published annotation); it does not publish main or tags. Publish mode atomically pushes the frozen main commit and annotation object, without force, retagging, fallback, or automatic retry. The complete source receipt is rechecked before every effect and afterward; remote main and the exact annotated tag must both match, not merely the Git exit status. Changes after preflight stop the remaining batch. Atomicity is per repository, not across repositories: earlier successful publications and a newly created local tag can remain after a later failure. Inspect reported receipts and obtain a new review before retrying; do not move immutable tags. Whole-batch revalidation deliberately repeats source and live-remote checks around each repository effect; the number of checks can grow quadratically with batch size. Plan release time accordingly rather than bypassing trust checks. The shared internal preflight is read-only and exposes no legacy mutation path. The fixture suite covers Meta and non-Meta preview/local-tag/ publication using temporary local bare remotes, including stale compositions, unsafe origins, divergent branches, damaged tag identity, changed receipts and false publication success. This is local tooling evidence, not a real release publication or production permission check. Deutsch: Die gemeinsamen Helfer erkennen ausschließlich das registrierte Meta-Repository mit dem echten Paket `packages/govoplan-meta/pyproject.toml` (Projektname `govoplan`). Version und vollständige Zusammensetzung müssen dem kanonischen Generator, Core und den geprüften Anforderungen entsprechen; der Generator stammt niemals aus dem ausgewählten Checkout. Der gemeinsame Manifestprüfer kann geprüften Anwendungscode laden und ist keine Sandbox. Die gemeinsame Änderungsplanung erkennt die vollständig generierte Paketdatei, aber der dauerhafte Versionsausführer darf Meta nicht selbst verändern: sein eingefrorener Lauf bindet den Meta-Checkout als vertrauenswürdigen Programmstand. Meta erscheint deshalb nach Core ausschließlich mit nicht automatisch ausführbaren Vorbereitungs-/Veröffentlichungsschritten. Zuerst Core und Modulquellen vorbereiten und geprüft committen; bei abweichender Core-Zielversion nennt der Plan diese Voraussetzung ausdrücklich. Aktive dauerhafte Läufe des Ziel-Workspaces beenden. Mit `prepare-developer-meta-package.py` zunächst eine Vorschau außerhalb der Quell-Checkouts speichern, dann deren JSON über `--receipt` zusammen mit `--apply` und `--confirm-out-of-run` bestätigen. Das Ziel muss ein separater registrierter privater Checkout sein, niemals das laufende Operator-Meta. Die Bestätigung ist eine Betreibererklärung; der Helfer sucht oder beendet keine fremden Prozesse. Quell-HEADs, Pfadidentitäten, Anforderungen, alle registrierten vollständigen Paket-Eingaben, der vertrauenswürdige Generator und der vollständige Ausgabehash werden eingefroren. Es gelten höchstens 128 Quelldateien, 2 MiB je Datei und 16 MiB insgesamt. Core muss bereits vollständig zur Zielversion passen; vorhandene oder nicht verifizierbare Meta-Zieltags sperren die Vorbereitung. Geänderte Nachweise stoppen vor dem Schreiben. Ausschließlich die verschachtelte Paketdatei wird vollständig generiert und danach geprüft; ein Fehler nach dem Schreiben meldet `needs-reconciliation` und erfordert die manuelle Prüfung dieser begrenzten Änderung, ohne automatisches Zurücksetzen. Kein automatischer Commit, Push oder Wiederholungsversuch findet statt. Zusammensetzung prüfen, manuell committen, Core zuerst veröffentlichen, dann die geschützte Meta-Tag-Route verwenden und einen neuen dauerhaften Lauf starten. Eine Selbstaktualisierung des aktiven dauerhaften Meta-Laufs bleibt ausdrücklich nicht unterstützt. Für jeden Tag-Stapel, auch ohne Meta, gelten Vertrauens-, Origin-, saubere Main- und Live-Abstammungsprüfungen für die gesamte Auswahl vor jeder Änderung. Unbekannte Repositories und nicht registrierte Remote-Aliase sind gesperrt. Nur bei ausgewähltem Meta gelten zusätzlich dessen Zusammensetzungsprüfung und der passende annotierte Core-Tag als Voraussetzung; Meta folgt zuletzt. Für Metas Veröffentlichung müssen Core-Tag und Main-Commit bereits auf dem Remote vorliegen. Lokale Modul-Kandidatentags bleiben vor Cores abschließendem Release-Lock und Tag möglich. Die vorhandene Core-WebUI-Bundleprüfung bleibt bei Modulveröffentlichung und Core-Auswahl erhalten. Relevante Core-Paket- und Lockdateien müssen eigene reguläre Dateien mit höchstens je 16 MiB sein; Identität und Inhaltshash werden eingefroren und vor jeder Aktion erneut geprüft. Nicht ausgewähltes Core benötigt dafür weder einen sauberen Checkout noch einen Tag. Reine Backend-Auswahlen lesen keine irrelevanten Core-WebUI-Dateien. Vor Git-Aufrufen werden Eigentümer, Schreibrechte, sichere Pfadabstammung und echte Git-/Worktree-Metadaten geprüft; veränderbare gemeinsame Verzeichnisse, fremde Eigentümer, Alternates, Grafts und versteckte Indexeinträge sind gesperrt. Ein Sticky-Bit-Vorfahr wie `/tmp` ist nur oberhalb eines eigenen geschützten Unterverzeichnisses zulässig. Es erfolgen weder Rechtereparaturen noch globale Git-Vertrauensänderungen. Ausgewählte Versions- und Zusammensetzungs- dateien müssen versioniert sein: Paket-/Lockdateien, Modulmanifeste und Paketinitialisierer sowie Metas verschachteltes Paket und Release-Anforderungen. Ignorierte Arbeitsdateien dürfen keine vom Tag abweichenden Angaben liefern. Die Vorschau schreibt nichts, lokale Tags veröffentlichen nichts, und die Veröffentlichung überträgt Main und den exakten annotierten Tag atomar je Repository. Unmittelbar vor und nach den Aktionen werden die eingefrorenen Quellnachweise erneut geprüft, einschließlich entferntem Main und Tag-Objekt. Bei Änderungen oder Fehlern stoppt der Rest des Stapels ohne automatischen Wiederholungsversuch. Frühere Veröffentlichungen und neu erzeugte lokale Tags können bestehen bleiben: vor einem neuen Versuch Nachweise prüfen und erneut freigeben, niemals unveränderliche Tags verschieben. Die vollständigen Quell- und Live-Remote-Prüfungen werden um jede Aktion wiederholt; bei großen Stapeln kann deren Anzahl quadratisch wachsen. Diese konservativen Prüfkosten gehören zur Release-Planung. Der interne Vorprüfer ist ausschließlich lesend und besitzt keinen alten Änderungspfad. Tests für Auswahlen mit und ohne Meta verwenden nur temporäre lokale Remotes und ersetzen keine echte Veröffentlichungsprüfung. If the tag-triggered developer meta-package job fails before publication, rerun `publish-developer-meta-package.yml` with the existing protected version. The manual path validates that tag against `main`, checks out its exact commit, and publishes only when the registry does not already contain the same wheel hash. Generic Packages are intentionally not used. Add that transport only when a consumer needs an artifact format unsupported by PyPI, npm, Gitea Releases, or the OCI registry.