264 lines
14 KiB
Markdown
264 lines
14 KiB
Markdown
# 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.
|
|
|
|
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<version>`
|
|
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
|
|
|
|
`tools/release/publish-release-catalog.sh` resolves the selected package set and
|
|
registry lock before it creates a catalog. 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 `<module>/index.json`, and one
|
|
`<module>/<version>/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
|
|
```
|
|
|
|
`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.
|
|
|
|
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.
|