Compare commits
126
Commits
cd15aa514e
...
v0.1.15
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a24c94435e | ||
|
|
774793976c | ||
|
|
492449a4e2 | ||
|
|
8e890b37ed | ||
|
|
077735bc24 | ||
|
|
d9522d3cc4 | ||
|
|
bad0ea37a7 | ||
|
|
9ffd46fe22 | ||
|
|
be51a9c347 | ||
|
|
e36a6573bf | ||
|
|
629bfec1f1 | ||
|
|
9e956eec6f | ||
|
|
9bb2c808a6 | ||
|
|
f7590a7b8b | ||
|
|
1a68565ba0 | ||
|
|
d9f67b5c26 | ||
|
|
1cfdaec250 | ||
|
|
62501d399a | ||
|
|
ce5528e3b8 | ||
|
|
1f039dd39c | ||
|
|
d107d94fec | ||
|
|
6163c5992f | ||
|
|
2f28f22fd1 | ||
|
|
909862afdb | ||
|
|
eb04804d36 | ||
|
|
017aa7a702 | ||
|
|
af27b9fbdf | ||
|
|
cb45251c59 | ||
|
|
3b3d5b3386 | ||
|
|
313249b8fc | ||
|
|
282c90c54b | ||
|
|
25424187a8 | ||
|
|
ff8ee991c3 | ||
|
|
a5a0731d20 | ||
|
|
a0f161041d | ||
|
|
cb85999a14 | ||
|
|
cbfe8b03a7 | ||
|
|
768e9a51c9 | ||
|
|
087561ee12 | ||
|
|
11c1aa1815 | ||
|
|
478ecb5d0e | ||
|
|
32689d027a | ||
|
|
b7cc2d2df4 | ||
|
|
b70869e747 | ||
|
|
0c84afb158 | ||
|
|
145aa58c11 | ||
|
|
a3566c9311 | ||
|
|
3219460064 | ||
|
|
4b2a15adb5 | ||
|
|
acc5ffc247 | ||
|
|
f4f9836a09 | ||
|
|
758fa1bba7 | ||
|
|
0acc8cfc31 | ||
|
|
344bcaf1bc | ||
|
|
794622e4ed | ||
|
|
7653e9851f | ||
|
|
6f896d9c04 | ||
|
|
8f5ac52b58 | ||
|
|
2bc9ad7f00 | ||
|
|
fa1a4bacfb | ||
|
|
0c0669768d | ||
|
|
7b6ceeb185 | ||
|
|
ff12f676a1 | ||
|
|
eb9ab9ef1c | ||
|
|
935c1fe162 | ||
|
|
c7d1cd0e8f | ||
|
|
ac80d7e4e3 | ||
|
|
5e449b0983 | ||
|
|
d4bf07b446 | ||
|
|
adc4db9fdf | ||
|
|
484f2af3ac | ||
|
|
abf9564cee | ||
|
|
5bef966119 | ||
|
|
5e80b39bbd | ||
|
|
9370f501a0 | ||
|
|
e8f7e2c194 | ||
|
|
cbbe08d912 | ||
|
|
2c515f73c2 | ||
|
|
b40f1428fd | ||
|
|
43380eb068 | ||
|
|
29acb55b7c | ||
|
|
4f08b52333 | ||
|
|
d3713bf2ee | ||
|
|
be4410ef1a | ||
|
|
29d07fe375 | ||
|
|
d9003bf63a | ||
|
|
ed31409034 | ||
|
|
50e9607e72 | ||
|
|
f7a30682b3 | ||
|
|
2c56a0fc11 | ||
|
|
be8ba10ae3 | ||
|
|
d78b13f9d3 | ||
|
|
3c658fa32d | ||
|
|
efd734fad2 | ||
|
|
186aa104ce | ||
|
|
ff9d2404ab | ||
|
|
ba82a85547 | ||
|
|
f1fd143ef5 | ||
|
|
b4248a849e | ||
|
|
ff47659899 | ||
|
|
908090dd0f | ||
|
|
3864ce28b1 | ||
|
|
857dbe55f7 | ||
|
|
c9fcdc90c1 | ||
|
|
82e836b720 | ||
|
|
fcb8296812 | ||
|
|
f2e2eb5517 | ||
|
|
1aea3e7c4f | ||
|
|
3f9567af18 | ||
|
|
de16f11ce8 | ||
|
|
9d6cdff4b8 | ||
|
|
aa4050c0ca | ||
|
|
d3cdbd8c7a | ||
|
|
7115c4711d | ||
|
|
a3beca6fc5 | ||
|
|
11d45bce25 | ||
|
|
7b6135b89b | ||
|
|
5b79e7d377 | ||
|
|
6afb8fea76 | ||
|
|
163b35c0af | ||
|
|
603e07cec5 | ||
|
|
97dfd333c6 | ||
|
|
ba88c574b9 | ||
|
|
8d292184d4 | ||
|
|
52bd3527cd | ||
|
|
ff49dabf8f |
+13
-2
@@ -2,17 +2,24 @@
|
|||||||
# Copy to a deployment-local .env or secret store. Do not commit populated secrets.
|
# Copy to a deployment-local .env or secret store. Do not commit populated secrets.
|
||||||
|
|
||||||
APP_ENV=production
|
APP_ENV=production
|
||||||
|
# Live graph changes are useful in development. Production should apply saved
|
||||||
|
# module state through a coordinated restart of all API and worker processes.
|
||||||
|
GOVOPLAN_MODULE_LIVE_APPLY_ENABLED=
|
||||||
GOVOPLAN_INSTALL_PROFILE=self-hosted
|
GOVOPLAN_INSTALL_PROFILE=self-hosted
|
||||||
MASTER_KEY_B64=<generate-with-govoplan-config-env-template-generate-secrets>
|
MASTER_KEY_B64=<generate-with-govoplan-config-env-template-generate-secrets>
|
||||||
|
|
||||||
DATABASE_URL=postgresql+psycopg://govoplan:change-me@127.0.0.1:5432/govoplan
|
DATABASE_URL=postgresql+psycopg://govoplan:change-me@127.0.0.1:5432/govoplan
|
||||||
GOVOPLAN_DATABASE_URL_PGTOOLS=postgresql://govoplan:change-me@127.0.0.1:5432/govoplan
|
GOVOPLAN_DATABASE_URL_PGTOOLS=postgresql://govoplan:change-me@127.0.0.1:5432/govoplan
|
||||||
|
GOVOPLAN_DB_POOL_SIZE=5
|
||||||
|
GOVOPLAN_DB_MAX_OVERFLOW=10
|
||||||
|
GOVOPLAN_DB_POOL_TIMEOUT_SECONDS=30
|
||||||
|
GOVOPLAN_DB_POOL_RECYCLE_SECONDS=1800
|
||||||
|
|
||||||
ENABLED_MODULES=tenancy,organizations,identity,access,admin,dashboard,policy,audit,files,mail,campaigns,calendar,docs,ops
|
ENABLED_MODULES=tenancy,organizations,identity,idm,access,admin,dashboard,policy,audit,files,templates,mail,campaigns,calendar,poll,scheduling,connectors,datasources,dataflow,dist_lists,workflow_engine,workflow,views,search,risk_compliance,postbox,notifications,services,parties,mandates,decisions,portal,cases,committee,docs,ops
|
||||||
|
|
||||||
CELERY_ENABLED=true
|
CELERY_ENABLED=true
|
||||||
REDIS_URL=redis://127.0.0.1:6379/0
|
REDIS_URL=redis://127.0.0.1:6379/0
|
||||||
CELERY_QUEUES=send_email,append_sent,notifications,calendar,default
|
CELERY_QUEUES=send_email,append_sent,notifications,calendar,dataflow,events,default
|
||||||
CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90
|
CALENDAR_OUTBOX_TERMINAL_RETENTION_DAYS=90
|
||||||
|
|
||||||
GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS=false
|
GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS=false
|
||||||
@@ -24,10 +31,14 @@ GOVOPLAN_HTTP_MAX_REQUEST_BODY_BYTES=536870912
|
|||||||
GOVOPLAN_HTTP_HSTS_SECONDS=31536000
|
GOVOPLAN_HTTP_HSTS_SECONDS=31536000
|
||||||
|
|
||||||
AUTH_LOGIN_THROTTLE_ENABLED=true
|
AUTH_LOGIN_THROTTLE_ENABLED=true
|
||||||
|
AUTH_ACTIVITY_TOUCH_INTERVAL_SECONDS=300
|
||||||
AUTH_LOGIN_THROTTLE_IDENTITY_LIMIT=10
|
AUTH_LOGIN_THROTTLE_IDENTITY_LIMIT=10
|
||||||
AUTH_LOGIN_THROTTLE_CLIENT_LIMIT=100
|
AUTH_LOGIN_THROTTLE_CLIENT_LIMIT=100
|
||||||
AUTH_LOGIN_THROTTLE_WINDOW_SECONDS=900
|
AUTH_LOGIN_THROTTLE_WINDOW_SECONDS=900
|
||||||
AUTH_LOGIN_THROTTLE_REDIS_RETRY_SECONDS=30
|
AUTH_LOGIN_THROTTLE_REDIS_RETRY_SECONDS=30
|
||||||
|
# Production startup fails without Redis unless this explicit single-process
|
||||||
|
# risk acknowledgement is enabled.
|
||||||
|
GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE=false
|
||||||
|
|
||||||
CORS_ORIGINS=https://govoplan.example.org
|
CORS_ORIGINS=https://govoplan.example.org
|
||||||
GOVOPLAN_TRUSTED_HOSTS=govoplan.example.org
|
GOVOPLAN_TRUSTED_HOSTS=govoplan.example.org
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
name: Dependency Audit
|
name: Dependency Audit
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
@@ -21,13 +23,13 @@ jobs:
|
|||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
- name: Use anonymous HTTPS for public GovOPlaN repositories
|
- name: Use HTTPS for GovOPlaN repositories
|
||||||
run: |
|
run: |
|
||||||
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "git@git.add-ideas.de:add-ideas/govoplan"
|
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "git@git.add-ideas.de:GovOPlaN/govoplan"
|
||||||
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "ssh://git@git.add-ideas.de/add-ideas/govoplan"
|
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "ssh://git@git.add-ideas.de/GovOPlaN/govoplan"
|
||||||
- name: Bootstrap GovOPlaN repositories
|
- name: Bootstrap GovOPlaN repositories
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --exclude-repo addideas-govoplan-website
|
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --reuse-checkout-auth --exclude-repo addideas-govoplan-website
|
||||||
- name: Install backend dev audit dependencies
|
- name: Install backend dev audit dependencies
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: Deployment Installer
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deployment-installer:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
|
with:
|
||||||
|
path: govoplan
|
||||||
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Compile deployment tooling
|
||||||
|
working-directory: govoplan
|
||||||
|
run: python -m py_compile tools/deployment/govoplan-deploy.py tools/deployment/govoplan_deploy/*.py
|
||||||
|
- name: Test declarative deployment bundle
|
||||||
|
working-directory: govoplan
|
||||||
|
run: python -m unittest -v tests.test_deployment_installer
|
||||||
|
- name: Build single-file deployer artifact
|
||||||
|
working-directory: govoplan
|
||||||
|
run: |
|
||||||
|
python tools/deployment/build-deployer-zipapp.py --output /tmp/govoplan-deploy.pyz
|
||||||
|
python /tmp/govoplan-deploy.pyz --help
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
name: Module Matrix
|
name: Module Matrix
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
@@ -7,6 +9,25 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
module-matrix:
|
module-matrix:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: govoplan_test
|
||||||
|
POSTGRES_USER: govoplan
|
||||||
|
POSTGRES_PASSWORD: govoplan_test
|
||||||
|
options: >-
|
||||||
|
--health-cmd "pg_isready -U govoplan -d govoplan_test"
|
||||||
|
--health-interval 5s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 20
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
options: >-
|
||||||
|
--health-cmd "redis-cli ping"
|
||||||
|
--health-interval 5s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 20
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
with:
|
with:
|
||||||
@@ -17,22 +38,48 @@ jobs:
|
|||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
- name: Use anonymous HTTPS for public GovOPlaN repositories
|
- name: Use HTTPS for GovOPlaN repositories
|
||||||
run: |
|
run: |
|
||||||
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "git@git.add-ideas.de:add-ideas/govoplan"
|
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "git@git.add-ideas.de:GovOPlaN/govoplan"
|
||||||
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "ssh://git@git.add-ideas.de/add-ideas/govoplan"
|
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "ssh://git@git.add-ideas.de/GovOPlaN/govoplan"
|
||||||
- name: Bootstrap GovOPlaN repositories
|
- name: Bootstrap GovOPlaN repositories
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --exclude-repo addideas-govoplan-website
|
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --reuse-checkout-auth --exclude-repo addideas-govoplan-website
|
||||||
- name: Install backend release dependencies
|
- name: Install backend release dependencies
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: |
|
run: |
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
.venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-release.txt --python .venv/bin/python --upgrade-pip
|
.venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-release.txt --python .venv/bin/python --upgrade-pip
|
||||||
.venv/bin/python -m pip install '../govoplan-core[dev]'
|
.venv/bin/python -m pip install '../govoplan-core[dev]'
|
||||||
|
.venv/bin/python -m pip install --no-deps ../govoplan-search
|
||||||
- name: Install WebUI release dependencies with test scripts
|
- name: Install WebUI release dependencies with test scripts
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
|
run: bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
|
||||||
|
- name: Validate platform interface and endpoint declarations
|
||||||
|
working-directory: govoplan
|
||||||
|
run: .venv/bin/python tools/inventory/platform-interface-inventory.py --strict-declarations --strict-endpoints
|
||||||
|
- name: Validate Search against PostgreSQL
|
||||||
|
working-directory: govoplan
|
||||||
|
env:
|
||||||
|
GOVOPLAN_SEARCH_POSTGRES_URL: postgresql+psycopg://govoplan:govoplan_test@postgres:5432/govoplan_test
|
||||||
|
run: |
|
||||||
|
GOVOPLAN_CORE_ROOT="$PWD/../govoplan-core" .venv/bin/python tools/checks/postgres-integration-check.py \
|
||||||
|
--database-url "$GOVOPLAN_SEARCH_POSTGRES_URL" \
|
||||||
|
--module-set search=tenancy,access,search \
|
||||||
|
--reset-schema \
|
||||||
|
--skip-retirement-atomicity
|
||||||
|
PYTHONPATH="$PWD/../govoplan-search/src:$PWD/../govoplan-core/src" \
|
||||||
|
.venv/bin/python -m unittest discover \
|
||||||
|
-s ../govoplan-search/tests \
|
||||||
|
-p test_postgres_search.py \
|
||||||
|
-v
|
||||||
|
- name: Prove worker delivery and shutdown guarantees
|
||||||
|
working-directory: govoplan
|
||||||
|
env:
|
||||||
|
GOVOPLAN_WORKER_DRILL_REDIS_URL: redis://redis:6379/15
|
||||||
|
run: |
|
||||||
|
.venv/bin/python tools/checks/worker-runtime-drill.py \
|
||||||
|
--output audit-reports/worker-runtime.json
|
||||||
- name: Run module matrix and contract tests
|
- name: Run module matrix and contract tests
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: GOVOPLAN_CORE_ROOT="$PWD/../govoplan-core" PYTHON="$PWD/.venv/bin/python" bash tools/checks/check-module-matrix.sh
|
run: GOVOPLAN_CORE_ROOT="$PWD/../govoplan-core" PYTHON="$PWD/.venv/bin/python" bash tools/checks/check-module-matrix.sh
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
name: Developer Meta-package Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-package:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GITEA_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Validate protected release tag and package version
|
||||||
|
run: |
|
||||||
|
python - <<'PY'
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import tomllib
|
||||||
|
|
||||||
|
tag = os.environ["GITEA_REF_NAME"]
|
||||||
|
project = tomllib.loads(Path("packages/govoplan-meta/pyproject.toml").read_text(encoding="utf-8"))["project"]
|
||||||
|
if tag != f"v{project['version']}":
|
||||||
|
raise SystemExit("meta-package version does not match the release tag")
|
||||||
|
if subprocess.run(["git", "merge-base", "--is-ancestor", "HEAD", "origin/main"]).returncode:
|
||||||
|
raise SystemExit("release tag is not contained in main")
|
||||||
|
PY
|
||||||
|
- name: Build and publish developer package
|
||||||
|
env:
|
||||||
|
PACKAGE_USERNAME: ${{ secrets.GOVOPLAN_PACKAGE_USERNAME }}
|
||||||
|
PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
test -n "$PACKAGE_USERNAME"
|
||||||
|
test -n "$PACKAGE_TOKEN"
|
||||||
|
python -m pip install --disable-pip-version-check build==1.5.0 twine==7.0.0
|
||||||
|
python -m build --wheel --outdir dist packages/govoplan-meta
|
||||||
|
python -m twine check dist/*.whl
|
||||||
|
TWINE_USERNAME="$PACKAGE_USERNAME" TWINE_PASSWORD="$PACKAGE_TOKEN" \
|
||||||
|
python -m twine upload --non-interactive \
|
||||||
|
--repository-url https://git.add-ideas.de/api/packages/GovOPlaN/pypi \
|
||||||
|
dist/*.whl
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
name: Release Integration
|
name: Release Integration
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -16,19 +18,25 @@ jobs:
|
|||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
- name: Use anonymous HTTPS for public GovOPlaN repositories
|
- name: Use HTTPS for GovOPlaN repositories
|
||||||
run: |
|
run: |
|
||||||
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "git@git.add-ideas.de:add-ideas/govoplan"
|
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "git@git.add-ideas.de:GovOPlaN/govoplan"
|
||||||
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "ssh://git@git.add-ideas.de/add-ideas/govoplan"
|
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "ssh://git@git.add-ideas.de/GovOPlaN/govoplan"
|
||||||
- name: Bootstrap GovOPlaN repositories
|
- name: Bootstrap GovOPlaN repositories
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --exclude-repo addideas-govoplan-website
|
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --reuse-checkout-auth --exclude-repo addideas-govoplan-website
|
||||||
|
- name: Validate package publication contracts
|
||||||
|
working-directory: govoplan
|
||||||
|
run: |
|
||||||
|
python tools/repo/sync-module-package-workflows.py --check
|
||||||
|
python tools/release/generate-developer-meta-package.py --check
|
||||||
|
python -m unittest tests.test_module_package_workflows tests.test_package_registry_release
|
||||||
- name: Install backend release integration dependencies
|
- name: Install backend release integration dependencies
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: |
|
run: |
|
||||||
python -m venv .venv
|
python -m venv .venv
|
||||||
.venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-release.txt --python .venv/bin/python --upgrade-pip
|
.venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-release.txt --python .venv/bin/python --upgrade-pip
|
||||||
.venv/bin/python -m pip install '../govoplan-core[dev]'
|
.venv/bin/python -m pip install -r requirements-release-tests.txt '../govoplan-core[dev]'
|
||||||
- name: Install WebUI release dependencies
|
- name: Install WebUI release dependencies
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
|
run: bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
|
||||||
|
|||||||
@@ -0,0 +1,388 @@
|
|||||||
|
name: Runtime Distribution
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: Release version without leading v
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
python_image:
|
||||||
|
description: Digest-pinned multi-architecture Python 3.12 slim image
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
nginx_image:
|
||||||
|
description: Digest-pinned multi-architecture nginx-unprivileged image
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
postgres_image:
|
||||||
|
description: Digest-pinned PostgreSQL image
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
redis_image:
|
||||||
|
description: Digest-pinned Redis image
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
load_balancer_image:
|
||||||
|
description: Digest-pinned HAProxy image
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
managed_ingress_image:
|
||||||
|
description: Digest-pinned Caddy image
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
garage_image:
|
||||||
|
description: Digest-pinned Garage image
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
test_mail_image:
|
||||||
|
description: Digest-pinned GreenMail image
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
binfmt_image:
|
||||||
|
description: Digest-pinned tonistiigi/binfmt image for arm64 CI execution
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish-runtime:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
|
with:
|
||||||
|
path: govoplan
|
||||||
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
- name: Validate immutable release inputs
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
PYTHON_IMAGE: ${{ inputs.python_image }}
|
||||||
|
NGINX_IMAGE: ${{ inputs.nginx_image }}
|
||||||
|
POSTGRES_IMAGE: ${{ inputs.postgres_image }}
|
||||||
|
REDIS_IMAGE: ${{ inputs.redis_image }}
|
||||||
|
LOAD_BALANCER_IMAGE: ${{ inputs.load_balancer_image }}
|
||||||
|
MANAGED_INGRESS_IMAGE: ${{ inputs.managed_ingress_image }}
|
||||||
|
GARAGE_IMAGE: ${{ inputs.garage_image }}
|
||||||
|
TEST_MAIL_IMAGE: ${{ inputs.test_mail_image }}
|
||||||
|
BINFMT_IMAGE: ${{ inputs.binfmt_image }}
|
||||||
|
run: |
|
||||||
|
python - <<'PY'
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
version = os.environ["VERSION"]
|
||||||
|
if re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?", version) is None:
|
||||||
|
raise SystemExit("version must be a SemVer value without a leading v")
|
||||||
|
image_pattern = re.compile(r"^[^@\s]+@sha256:[0-9a-f]{64}$")
|
||||||
|
for name in (
|
||||||
|
"PYTHON_IMAGE",
|
||||||
|
"NGINX_IMAGE",
|
||||||
|
"POSTGRES_IMAGE",
|
||||||
|
"REDIS_IMAGE",
|
||||||
|
"LOAD_BALANCER_IMAGE",
|
||||||
|
"MANAGED_INGRESS_IMAGE",
|
||||||
|
"GARAGE_IMAGE",
|
||||||
|
"TEST_MAIL_IMAGE",
|
||||||
|
"BINFMT_IMAGE",
|
||||||
|
):
|
||||||
|
if image_pattern.fullmatch(os.environ[name]) is None:
|
||||||
|
raise SystemExit(f"{name} must be an exact sha256 image reference")
|
||||||
|
PY
|
||||||
|
- name: Use HTTPS for GovOPlaN repositories
|
||||||
|
run: |
|
||||||
|
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "git@git.add-ideas.de:GovOPlaN/govoplan"
|
||||||
|
git config --global --add url."https://git.add-ideas.de/GovOPlaN/govoplan".insteadOf "ssh://git@git.add-ideas.de/GovOPlaN/govoplan"
|
||||||
|
- name: Bootstrap release sources
|
||||||
|
working-directory: govoplan
|
||||||
|
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --reuse-checkout-auth --exclude-repo addideas-govoplan-website
|
||||||
|
- name: Build release wheel roots and WebUI
|
||||||
|
working-directory: govoplan
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
GOVOPLAN_PACKAGE_USERNAME: ${{ secrets.GOVOPLAN_PACKAGE_USERNAME }}
|
||||||
|
GOVOPLAN_PACKAGE_TOKEN: ${{ secrets.GOVOPLAN_PACKAGE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
python -m venv .runtime-build
|
||||||
|
.runtime-build/bin/python -m pip install --upgrade pip cryptography
|
||||||
|
.runtime-build/bin/python tools/release/generate-release-package-set.py \
|
||||||
|
--version "$VERSION" \
|
||||||
|
--output runtime-output/release-packages.json
|
||||||
|
.runtime-build/bin/python tools/release/resolve-package-artifacts.py \
|
||||||
|
--package-set runtime-output/release-packages.json \
|
||||||
|
--wheelhouse runtime-output/local-wheels \
|
||||||
|
--webui-packages runtime-output/webui-packages \
|
||||||
|
--lock-output runtime-output/package-artifacts.lock.json \
|
||||||
|
--requirements-output runtime-output/requirements-release.packages.txt \
|
||||||
|
--python .runtime-build/bin/python
|
||||||
|
PYTHON=.runtime-build/bin/python \
|
||||||
|
GOVOPLAN_WEBUI_PACKAGE_LOCK="$PWD/runtime-output/package-artifacts.lock.json" \
|
||||||
|
GOVOPLAN_WEBUI_PACKAGE_DIR="$PWD/runtime-output/webui-packages" \
|
||||||
|
bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
|
||||||
|
npm --prefix ../govoplan-core/webui run build
|
||||||
|
.runtime-build/bin/python tools/release/prepare-runtime-context.py \
|
||||||
|
--wheelhouse runtime-output/local-wheels \
|
||||||
|
--web-dist ../govoplan-core/webui/dist \
|
||||||
|
--output runtime-output/common \
|
||||||
|
--required-module tenancy \
|
||||||
|
--required-module organizations \
|
||||||
|
--required-module identity \
|
||||||
|
--required-module idm \
|
||||||
|
--required-module access \
|
||||||
|
--required-module admin \
|
||||||
|
--required-module dashboard \
|
||||||
|
--required-module policy \
|
||||||
|
--required-module audit \
|
||||||
|
--required-module docs \
|
||||||
|
--required-module ops
|
||||||
|
- name: Resolve architecture-specific offline wheelhouses
|
||||||
|
working-directory: govoplan
|
||||||
|
run: |
|
||||||
|
mkdir -p runtime-output/wheels-amd64 runtime-output/wheels-arm64
|
||||||
|
cp runtime-output/local-wheels/*.whl runtime-output/wheels-amd64/
|
||||||
|
cp runtime-output/local-wheels/*.whl runtime-output/wheels-arm64/
|
||||||
|
.runtime-build/bin/python -m pip download --only-binary=:all: \
|
||||||
|
--platform manylinux_2_17_x86_64 --platform manylinux2014_x86_64 \
|
||||||
|
--implementation cp --python-version 3.12 --abi cp312 \
|
||||||
|
--find-links runtime-output/local-wheels \
|
||||||
|
--dest runtime-output/wheels-amd64 \
|
||||||
|
--requirement runtime-output/common/requirements-runtime.txt
|
||||||
|
.runtime-build/bin/python -m pip download --only-binary=:all: \
|
||||||
|
--platform manylinux_2_17_aarch64 --platform manylinux2014_aarch64 \
|
||||||
|
--implementation cp --python-version 3.12 --abi cp312 \
|
||||||
|
--find-links runtime-output/local-wheels \
|
||||||
|
--dest runtime-output/wheels-arm64 \
|
||||||
|
--requirement runtime-output/common/requirements-runtime.txt
|
||||||
|
.runtime-build/bin/python tools/release/prepare-runtime-context.py \
|
||||||
|
--wheelhouse runtime-output/wheels-amd64 \
|
||||||
|
--web-dist ../govoplan-core/webui/dist \
|
||||||
|
--output runtime-output/context-amd64
|
||||||
|
.runtime-build/bin/python tools/release/prepare-runtime-context.py \
|
||||||
|
--wheelhouse runtime-output/wheels-arm64 \
|
||||||
|
--web-dist ../govoplan-core/webui/dist \
|
||||||
|
--output runtime-output/context-arm64
|
||||||
|
cmp runtime-output/context-amd64/composition.json runtime-output/context-arm64/composition.json
|
||||||
|
- name: Build one-file deployer
|
||||||
|
working-directory: govoplan
|
||||||
|
run: python tools/deployment/build-deployer-zipapp.py --output runtime-output/govoplan-deploy.pyz
|
||||||
|
- name: Authenticate OCI publication
|
||||||
|
working-directory: govoplan
|
||||||
|
env:
|
||||||
|
REGISTRY_USERNAME: ${{ secrets.GOVOPLAN_REGISTRY_USERNAME }}
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.GOVOPLAN_REGISTRY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
test -n "$REGISTRY_USERNAME"
|
||||||
|
test -n "$REGISTRY_TOKEN"
|
||||||
|
printf '%s' "$REGISTRY_TOKEN" | docker login git.add-ideas.de --username "$REGISTRY_USERNAME" --password-stdin
|
||||||
|
docker buildx create --name govoplan-runtime --use
|
||||||
|
- name: Build and publish architecture images
|
||||||
|
working-directory: govoplan
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
PYTHON_IMAGE: ${{ inputs.python_image }}
|
||||||
|
NGINX_IMAGE: ${{ inputs.nginx_image }}
|
||||||
|
run: |
|
||||||
|
COMPOSITION_SHA256="$(sha256sum runtime-output/context-amd64/composition.json | cut -d' ' -f1)"
|
||||||
|
for ARCH in amd64 arm64; do
|
||||||
|
docker buildx build --platform "linux/$ARCH" --push \
|
||||||
|
--file tools/release/runtime/Dockerfile.api \
|
||||||
|
--build-arg "PYTHON_IMAGE=$PYTHON_IMAGE" \
|
||||||
|
--build-arg "GOVOPLAN_RELEASE_VERSION=$VERSION" \
|
||||||
|
--build-arg "GOVOPLAN_COMPOSITION_SHA256=$COMPOSITION_SHA256" \
|
||||||
|
--tag "git.add-ideas.de/govoplan/runtime-api:$VERSION-$ARCH" \
|
||||||
|
"runtime-output/context-$ARCH"
|
||||||
|
docker buildx build --platform "linux/$ARCH" --push \
|
||||||
|
--file tools/release/runtime/Dockerfile.web \
|
||||||
|
--build-arg "NGINX_IMAGE=$NGINX_IMAGE" \
|
||||||
|
--build-arg "GOVOPLAN_RELEASE_VERSION=$VERSION" \
|
||||||
|
--build-arg "GOVOPLAN_COMPOSITION_SHA256=$COMPOSITION_SHA256" \
|
||||||
|
--tag "git.add-ideas.de/govoplan/runtime-web:$VERSION-$ARCH" \
|
||||||
|
"runtime-output/context-$ARCH"
|
||||||
|
done
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag "git.add-ideas.de/govoplan/runtime-api:$VERSION" \
|
||||||
|
"git.add-ideas.de/govoplan/runtime-api:$VERSION-amd64" \
|
||||||
|
"git.add-ideas.de/govoplan/runtime-api:$VERSION-arm64"
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag "git.add-ideas.de/govoplan/runtime-web:$VERSION" \
|
||||||
|
"git.add-ideas.de/govoplan/runtime-web:$VERSION-amd64" \
|
||||||
|
"git.add-ideas.de/govoplan/runtime-web:$VERSION-arm64"
|
||||||
|
docker buildx imagetools inspect "git.add-ideas.de/govoplan/runtime-api:$VERSION" --raw > runtime-output/api-index.json
|
||||||
|
docker buildx imagetools inspect "git.add-ideas.de/govoplan/runtime-web:$VERSION" --raw > runtime-output/web-index.json
|
||||||
|
API_DIGEST="sha256:$(sha256sum runtime-output/api-index.json | cut -d' ' -f1)"
|
||||||
|
WEB_DIGEST="sha256:$(sha256sum runtime-output/web-index.json | cut -d' ' -f1)"
|
||||||
|
python tools/release/resolve-oci-platforms.py --repository git.add-ideas.de/govoplan/runtime-api --index-digest "$API_DIGEST" --index runtime-output/api-index.json --output runtime-output/api-metadata.json
|
||||||
|
python tools/release/resolve-oci-platforms.py --repository git.add-ideas.de/govoplan/runtime-web --index-digest "$WEB_DIGEST" --index runtime-output/web-index.json --output runtime-output/web-metadata.json
|
||||||
|
- name: Resolve managed dependency platform images
|
||||||
|
working-directory: govoplan
|
||||||
|
env:
|
||||||
|
POSTGRES_IMAGE: ${{ inputs.postgres_image }}
|
||||||
|
REDIS_IMAGE: ${{ inputs.redis_image }}
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools inspect "$POSTGRES_IMAGE" --raw > runtime-output/postgres-index.json
|
||||||
|
docker buildx imagetools inspect "$REDIS_IMAGE" --raw > runtime-output/redis-index.json
|
||||||
|
python tools/release/resolve-oci-platforms.py \
|
||||||
|
--repository "${POSTGRES_IMAGE%@*}" \
|
||||||
|
--index-digest "${POSTGRES_IMAGE##*@}" \
|
||||||
|
--index runtime-output/postgres-index.json \
|
||||||
|
--output runtime-output/postgres-metadata.json
|
||||||
|
python tools/release/resolve-oci-platforms.py \
|
||||||
|
--repository "${REDIS_IMAGE%@*}" \
|
||||||
|
--index-digest "${REDIS_IMAGE##*@}" \
|
||||||
|
--index runtime-output/redis-index.json \
|
||||||
|
--output runtime-output/redis-metadata.json
|
||||||
|
- name: Register arm64 execution for runtime smoke
|
||||||
|
working-directory: govoplan
|
||||||
|
env:
|
||||||
|
BINFMT_IMAGE: ${{ inputs.binfmt_image }}
|
||||||
|
run: docker run --privileged --rm "$BINFMT_IMAGE" --install arm64
|
||||||
|
- name: Exercise amd64 and arm64 runtime images
|
||||||
|
working-directory: govoplan
|
||||||
|
run: |
|
||||||
|
for ARCH in amd64 arm64; do
|
||||||
|
.runtime-build/bin/python tools/checks/runtime-image-smoke.py \
|
||||||
|
--api-metadata runtime-output/api-metadata.json \
|
||||||
|
--web-metadata runtime-output/web-metadata.json \
|
||||||
|
--postgres-metadata runtime-output/postgres-metadata.json \
|
||||||
|
--redis-metadata runtime-output/redis-metadata.json \
|
||||||
|
--platform "linux/$ARCH" \
|
||||||
|
--output "runtime-output/evidence/runtime-smoke-$ARCH.json"
|
||||||
|
done
|
||||||
|
- name: Generate and sign distribution evidence
|
||||||
|
working-directory: govoplan
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
SOURCE_COMMIT: ${{ gitea.sha }}
|
||||||
|
SIGNING_KEY: ${{ secrets.RUNTIME_DISTRIBUTION_SIGNING_KEY }}
|
||||||
|
SIGNING_KEY_ID: ${{ secrets.RUNTIME_DISTRIBUTION_SIGNING_KEY_ID }}
|
||||||
|
TRUSTED_KEYRING: ${{ secrets.RUNTIME_DISTRIBUTION_KEYRING }}
|
||||||
|
POSTGRES_IMAGE: ${{ inputs.postgres_image }}
|
||||||
|
REDIS_IMAGE: ${{ inputs.redis_image }}
|
||||||
|
LOAD_BALANCER_IMAGE: ${{ inputs.load_balancer_image }}
|
||||||
|
MANAGED_INGRESS_IMAGE: ${{ inputs.managed_ingress_image }}
|
||||||
|
GARAGE_IMAGE: ${{ inputs.garage_image }}
|
||||||
|
TEST_MAIL_IMAGE: ${{ inputs.test_mail_image }}
|
||||||
|
run: |
|
||||||
|
test -n "$SIGNING_KEY"
|
||||||
|
test -n "$SIGNING_KEY_ID"
|
||||||
|
test -n "$TRUSTED_KEYRING"
|
||||||
|
printf '%s\n' "$SIGNING_KEY" > runtime-output/signing-key.pem
|
||||||
|
printf '%s\n' "$TRUSTED_KEYRING" > runtime-output/distribution-keyring.json
|
||||||
|
chmod 600 runtime-output/signing-key.pem
|
||||||
|
ARTIFACT_BASE="https://git.add-ideas.de/GovOPlaN/govoplan/releases/download/v$VERSION"
|
||||||
|
python tools/release/finalize-runtime-distribution.py \
|
||||||
|
--composition runtime-output/context-amd64/composition.json \
|
||||||
|
--api-metadata runtime-output/api-metadata.json \
|
||||||
|
--web-metadata runtime-output/web-metadata.json \
|
||||||
|
--deployer runtime-output/govoplan-deploy.pyz \
|
||||||
|
--deployer-url "$ARTIFACT_BASE/govoplan-deploy.pyz" \
|
||||||
|
--package-lock runtime-output/package-artifacts.lock.json \
|
||||||
|
--artifact-base-url "$ARTIFACT_BASE" \
|
||||||
|
--source-commit "$SOURCE_COMMIT" \
|
||||||
|
--version "$VERSION" \
|
||||||
|
--sequence "$(date -u +%Y%m%d%H%M)" \
|
||||||
|
--dependency "postgres=$POSTGRES_IMAGE" \
|
||||||
|
--dependency "redis=$REDIS_IMAGE" \
|
||||||
|
--dependency "load_balancer=$LOAD_BALANCER_IMAGE" \
|
||||||
|
--dependency "managed_ingress=$MANAGED_INGRESS_IMAGE" \
|
||||||
|
--dependency "garage=$GARAGE_IMAGE" \
|
||||||
|
--dependency "test_mail=$TEST_MAIL_IMAGE" \
|
||||||
|
--output-directory runtime-output/evidence \
|
||||||
|
--descriptor runtime-output/distribution-descriptor.json
|
||||||
|
.runtime-build/bin/python tools/release/generate-runtime-distribution.py \
|
||||||
|
--descriptor runtime-output/distribution-descriptor.json \
|
||||||
|
--signing-key "$SIGNING_KEY_ID=runtime-output/signing-key.pem" \
|
||||||
|
--output runtime-output/distribution-manifest.json
|
||||||
|
openssl pkeyutl -sign -inkey runtime-output/signing-key.pem -rawin \
|
||||||
|
-in runtime-output/govoplan-deploy.pyz \
|
||||||
|
-out runtime-output/govoplan-deploy.pyz.sig
|
||||||
|
(cd runtime-output && sha256sum govoplan-deploy.pyz > govoplan-deploy.pyz.sha256)
|
||||||
|
(cd runtime-output && sha256sum distribution-manifest.json > distribution-manifest.json.sha256)
|
||||||
|
rm runtime-output/signing-key.pem
|
||||||
|
- name: Verify the published bundle contract with the zipapp
|
||||||
|
working-directory: govoplan
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
SIGNING_KEY_ID: ${{ secrets.RUNTIME_DISTRIBUTION_SIGNING_KEY_ID }}
|
||||||
|
run: |
|
||||||
|
(cd runtime-output && sha256sum --check govoplan-deploy.pyz.sha256)
|
||||||
|
(cd runtime-output && sha256sum --check distribution-manifest.json.sha256)
|
||||||
|
.runtime-build/bin/python - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
keyring = json.loads(
|
||||||
|
Path("runtime-output/distribution-keyring.json").read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
key_id = os.environ["SIGNING_KEY_ID"]
|
||||||
|
matches = [item for item in keyring["keys"] if item.get("key_id") == key_id]
|
||||||
|
if len(matches) != 1 or matches[0].get("status") != "active":
|
||||||
|
raise SystemExit("runtime signing key is not uniquely active in the keyring")
|
||||||
|
Path("runtime-output/runtime-release-public.pem").write_text(
|
||||||
|
matches[0]["public_key_pem"], encoding="utf-8"
|
||||||
|
)
|
||||||
|
PY
|
||||||
|
openssl pkeyutl -verify -pubin \
|
||||||
|
-inkey runtime-output/runtime-release-public.pem -rawin \
|
||||||
|
-in runtime-output/govoplan-deploy.pyz \
|
||||||
|
-sigfile runtime-output/govoplan-deploy.pyz.sig
|
||||||
|
cp runtime-output/govoplan-deploy.pyz runtime-output/govoplan-deploy.tampered.pyz
|
||||||
|
printf '\0' >> runtime-output/govoplan-deploy.tampered.pyz
|
||||||
|
if openssl pkeyutl -verify -pubin \
|
||||||
|
-inkey runtime-output/runtime-release-public.pem -rawin \
|
||||||
|
-in runtime-output/govoplan-deploy.tampered.pyz \
|
||||||
|
-sigfile runtime-output/govoplan-deploy.pyz.sig >/dev/null 2>&1; then
|
||||||
|
echo "Tampered deployment bootstrap unexpectedly verified" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
MANIFEST_SHA256="$(cut -d' ' -f1 runtime-output/distribution-manifest.json.sha256)"
|
||||||
|
python runtime-output/govoplan-deploy.pyz init \
|
||||||
|
--directory runtime-output/acceptance-install \
|
||||||
|
--non-interactive --module-set base
|
||||||
|
python runtime-output/govoplan-deploy.pyz verify-release \
|
||||||
|
--directory runtime-output/acceptance-install \
|
||||||
|
--manifest runtime-output/distribution-manifest.json \
|
||||||
|
--manifest-sha256 "$MANIFEST_SHA256" \
|
||||||
|
--trusted-keyring runtime-output/distribution-keyring.json \
|
||||||
|
--adopt
|
||||||
|
- name: Exercise the managed ingress boundary
|
||||||
|
working-directory: govoplan
|
||||||
|
env:
|
||||||
|
MANAGED_INGRESS_IMAGE: ${{ inputs.managed_ingress_image }}
|
||||||
|
LOAD_BALANCER_IMAGE: ${{ inputs.load_balancer_image }}
|
||||||
|
run: >-
|
||||||
|
python tools/checks/managed-ingress-drill.py
|
||||||
|
--caddy-image "$MANAGED_INGRESS_IMAGE"
|
||||||
|
--load-balancer-image "$LOAD_BALANCER_IMAGE"
|
||||||
|
--probe-image "$(jq -r '.platforms["linux/amd64"]' runtime-output/api-metadata.json)"
|
||||||
|
- name: Publish immutable Gitea release assets
|
||||||
|
working-directory: govoplan
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
SOURCE_COMMIT: ${{ gitea.sha }}
|
||||||
|
GITEA_RELEASE_TOKEN: ${{ secrets.GOVOPLAN_RELEASE_TOKEN }}
|
||||||
|
run: |
|
||||||
|
python tools/release/publish-runtime-release.py \
|
||||||
|
--tag "v$VERSION" \
|
||||||
|
--target-commit "$SOURCE_COMMIT" \
|
||||||
|
--title "GovOPlaN v$VERSION runtime distribution" \
|
||||||
|
--asset runtime-output/govoplan-deploy.pyz \
|
||||||
|
--asset runtime-output/govoplan-deploy.pyz.sig \
|
||||||
|
--asset runtime-output/govoplan-deploy.pyz.sha256 \
|
||||||
|
--asset runtime-output/distribution-manifest.json \
|
||||||
|
--asset runtime-output/distribution-manifest.json.sha256 \
|
||||||
|
--asset runtime-output/distribution-keyring.json \
|
||||||
|
--asset runtime-output/context-amd64/composition.json \
|
||||||
|
--asset runtime-output/release-packages.json \
|
||||||
|
--asset runtime-output/package-artifacts.lock.json \
|
||||||
|
--asset runtime-output/requirements-release.packages.txt \
|
||||||
|
--asset runtime-output/evidence/api-sbom.cdx.json \
|
||||||
|
--asset runtime-output/evidence/web-sbom.cdx.json \
|
||||||
|
--asset runtime-output/evidence/api-provenance.json \
|
||||||
|
--asset runtime-output/evidence/web-provenance.json \
|
||||||
|
--asset runtime-output/evidence/runtime-smoke-amd64.json \
|
||||||
|
--asset runtime-output/evidence/runtime-smoke-arm64.json
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Runtime Ingress Drill
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
caddy_image:
|
||||||
|
description: Digest-pinned Caddy image
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
load_balancer_image:
|
||||||
|
description: Digest-pinned HAProxy image
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
probe_image:
|
||||||
|
description: Digest-pinned amd64 GovOPlaN API image
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
managed-ingress:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
|
with:
|
||||||
|
path: govoplan
|
||||||
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Authenticate runtime image pull
|
||||||
|
env:
|
||||||
|
REGISTRY_USERNAME: ${{ secrets.GOVOPLAN_REGISTRY_USERNAME }}
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.GOVOPLAN_REGISTRY_TOKEN }}
|
||||||
|
run: echo "$REGISTRY_TOKEN" | docker login git.add-ideas.de --username "$REGISTRY_USERNAME" --password-stdin
|
||||||
|
- name: Exercise the managed ingress boundary
|
||||||
|
working-directory: govoplan
|
||||||
|
env:
|
||||||
|
CADDY_IMAGE: ${{ inputs.caddy_image }}
|
||||||
|
LOAD_BALANCER_IMAGE: ${{ inputs.load_balancer_image }}
|
||||||
|
PROBE_IMAGE: ${{ inputs.probe_image }}
|
||||||
|
run: >-
|
||||||
|
python tools/checks/managed-ingress-drill.py
|
||||||
|
--caddy-image "$CADDY_IMAGE"
|
||||||
|
--load-balancer-image "$LOAD_BALANCER_IMAGE"
|
||||||
|
--probe-image "$PROBE_IMAGE"
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
name: Security Audit
|
name: Security Audit
|
||||||
|
|
||||||
|
permissions: read-all
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -12,35 +14,25 @@ jobs:
|
|||||||
security-audit:
|
security-audit:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
SECURITY_AUDIT_MODE: ci
|
SECURITY_AUDIT_MODE: full
|
||||||
SECURITY_AUDIT_SCOPE: govoplan
|
SECURITY_AUDIT_SCOPE: govoplan
|
||||||
SECURITY_AUDIT_FAIL_ON_FINDINGS: "0"
|
SECURITY_AUDIT_FAIL_ON_FINDINGS: "0"
|
||||||
SECURITY_AUDIT_REQUIRE_TOOLS: "1"
|
SECURITY_AUDIT_REQUIRE_TOOLS: "1"
|
||||||
|
GIT_CONFIG_COUNT: "2"
|
||||||
|
GIT_CONFIG_KEY_0: url.https://git.add-ideas.de/GovOPlaN/govoplan.insteadOf
|
||||||
|
GIT_CONFIG_VALUE_0: git@git.add-ideas.de:GovOPlaN/govoplan
|
||||||
|
GIT_CONFIG_KEY_1: url.https://git.add-ideas.de/GovOPlaN/govoplan.insteadOf
|
||||||
|
GIT_CONFIG_VALUE_1: ssh://git@git.add-ideas.de/GovOPlaN/govoplan
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||||
with:
|
with:
|
||||||
path: govoplan
|
path: govoplan
|
||||||
- name: Use anonymous HTTPS for public GovOPlaN repositories
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
||||||
run: |
|
with:
|
||||||
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "git@git.add-ideas.de:add-ideas/govoplan"
|
python-version: "3.12"
|
||||||
git config --global --add url."https://git.add-ideas.de/add-ideas/govoplan".insteadOf "ssh://git@git.add-ideas.de/add-ideas/govoplan"
|
|
||||||
- name: Configure SSH for repository bootstrap
|
|
||||||
env:
|
|
||||||
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
chmod 700 ~/.ssh
|
|
||||||
if [ -z "${GOVOPLAN_RELEASE_SSH_KEY_B64:-}" ]; then
|
|
||||||
echo "GOVOPLAN_RELEASE_SSH_KEY_B64 secret is required for git+ssh repository bootstrap."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
printf '%s' "$GOVOPLAN_RELEASE_SSH_KEY_B64" | base64 -d > ~/.ssh/id_ed25519
|
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
|
||||||
echo 'git.add-ideas.de ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDe48IOof2fJS1dTbJtLWQnWnr+JorZXKIFdOAM9ct8G' > ~/.ssh/known_hosts
|
|
||||||
chmod 600 ~/.ssh/known_hosts
|
|
||||||
- name: Bootstrap GovOPlaN repositories
|
- name: Bootstrap GovOPlaN repositories
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https
|
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --reuse-checkout-auth --exclude-repo addideas-govoplan-website
|
||||||
- name: Run whole-system security audit
|
- name: Run whole-system security audit
|
||||||
working-directory: govoplan
|
working-directory: govoplan
|
||||||
run: tools/checks/security-audit/run.sh --mode "$SECURITY_AUDIT_MODE" --scope "$SECURITY_AUDIT_SCOPE" --reports-dir audit-reports
|
run: tools/checks/security-audit/run.sh --mode "$SECURITY_AUDIT_MODE" --scope "$SECURITY_AUDIT_SCOPE" --reports-dir audit-reports
|
||||||
|
|||||||
@@ -4,7 +4,15 @@
|
|||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
.venv/
|
.venv/
|
||||||
runtime/
|
runtime/
|
||||||
|
!tools/release/runtime/
|
||||||
|
tools/release/runtime/*
|
||||||
|
!tools/release/runtime/Dockerfile.api
|
||||||
|
!tools/release/runtime/Dockerfile.web
|
||||||
|
!tools/release/runtime/nginx.conf
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
audit-reports/
|
audit-reports/
|
||||||
coverage/
|
coverage/
|
||||||
htmlcov/
|
htmlcov/
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# GovOPlaN Workspace Codex Guide
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This repository coordinates the GovOPlaN workspace, release catalog, shared
|
||||||
|
checks, development environment, and cross-repository automation. Business and
|
||||||
|
platform behavior remains owned by the corresponding module repository.
|
||||||
|
|
||||||
|
## Documentation Contract
|
||||||
|
|
||||||
|
- Treat documentation as part of every behavior change. Update the owning module's manifest-driven `DocumentationTopic` contributions for each affected user and administrator workflow, setting, permission, limitation, and operational consequence.
|
||||||
|
- Keep feature content in the owning module. The optional `govoplan-docs` module projects module contributions and must not import feature internals.
|
||||||
|
- Every module manifest must provide a static user and administrator baseline, even when `documentation_providers` add configured-state details.
|
||||||
|
- Run `tools/checks/check-manifest-shapes.py` after module behavior or manifest changes. Run `tools/checks/check-focused.sh` for cross-module changes.
|
||||||
|
|
||||||
|
## Working Rules
|
||||||
|
|
||||||
|
- Treat Gitea issues as the canonical backlog and state log.
|
||||||
|
- Preserve optional module boundaries and use Core contracts or capabilities for integrations.
|
||||||
|
- Prefer targeted checks before full workspace scans.
|
||||||
|
- Do not start persistent development servers unless requested.
|
||||||
@@ -4,10 +4,11 @@
|
|||||||
**Repository type:** system (meta).
|
**Repository type:** system (meta).
|
||||||
<!-- govoplan-repository-type:end -->
|
<!-- govoplan-repository-type:end -->
|
||||||
|
|
||||||
[](https://git.add-ideas.de/add-ideas/govoplan/actions?workflow=module-matrix.yml&actor=0&status=0)
|
[](https://git.add-ideas.de/GovOPlaN/govoplan/actions?workflow=module-matrix.yml&actor=0&status=0)
|
||||||
[](https://git.add-ideas.de/add-ideas/govoplan/actions?workflow=release-integration.yml&actor=0&status=0)
|
[](https://git.add-ideas.de/GovOPlaN/govoplan/actions?workflow=release-integration.yml&actor=0&status=0)
|
||||||
[](https://git.add-ideas.de/add-ideas/govoplan/actions?workflow=dependency-audit.yml&actor=0&status=0)
|
[](https://git.add-ideas.de/GovOPlaN/govoplan/actions?workflow=deployment-installer.yml&actor=0&status=0)
|
||||||
[](https://git.add-ideas.de/add-ideas/govoplan/actions?workflow=security-audit.yml&actor=0&status=0)
|
[](https://git.add-ideas.de/GovOPlaN/govoplan/actions?workflow=dependency-audit.yml&actor=0&status=0)
|
||||||
|
[](https://git.add-ideas.de/GovOPlaN/govoplan/actions?workflow=security-audit.yml&actor=0&status=0)
|
||||||
|
|
||||||
This is the GovOPlaN meta repository. It is the operator entry point for
|
This is the GovOPlaN meta repository. It is the operator entry point for
|
||||||
whole-product development, release orchestration, repository bootstrap, and
|
whole-product development, release orchestration, repository bootstrap, and
|
||||||
@@ -41,6 +42,16 @@ Open the WebUI in a browser after launch only when explicitly requested:
|
|||||||
GOVOPLAN_OPEN_BROWSER=1 ./tools/launch/launch-dev.sh
|
GOVOPLAN_OPEN_BROWSER=1 ./tools/launch/launch-dev.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Limit backend reload triggers during focused module work without changing the
|
||||||
|
enabled module graph:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
GOVOPLAN_BACKEND_RELOAD_MODULES=calendar,campaign ./tools/launch/launch-dev.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `GOVOPLAN_BACKEND_RELOAD_MODULES=none` to watch only core/config sources.
|
||||||
|
Leaving it unset keeps the broad default and watches all enabled modules.
|
||||||
|
|
||||||
Start the shared development PostgreSQL service:
|
Start the shared development PostgreSQL service:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -59,6 +70,12 @@ Clone missing repositories listed in `repositories.json`:
|
|||||||
./tools/repo/bootstrap-repositories.py
|
./tools/repo/bootstrap-repositories.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Gitea Actions jobs bootstrap the registered repositories over HTTPS and reuse
|
||||||
|
only the checkout job's short-lived authentication header. If registered
|
||||||
|
modules are private, allow the meta repository read access under
|
||||||
|
`GovOPlaN -> Settings -> Actions -> General -> Cross-Repository Access`; no
|
||||||
|
long-lived personal token is stored by the workflow or bootstrap tool.
|
||||||
|
|
||||||
Update generated repository type notes in all READMEs:
|
Update generated repository type notes in all READMEs:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -96,6 +113,18 @@ Generate the CycloneDX dependency inventory from a resolved release environment:
|
|||||||
./.venv/bin/python tools/release/generate-release-sbom.py --python ./.venv/bin/python
|
./.venv/bin/python tools/release/generate-release-sbom.py --python ./.venv/bin/python
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Synchronize module package workflows and inspect the registry release contract:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./.venv/bin/python tools/repo/sync-module-package-workflows.py --check
|
||||||
|
./.venv/bin/python tools/release/generate-release-package-set.py \
|
||||||
|
--output /tmp/govoplan-release-packages.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Package publication, exact artifact locking, and the optional `govoplan`
|
||||||
|
developer meta-package are documented in
|
||||||
|
[Package Registry Releases](docs/PACKAGE_REGISTRY_RELEASES.md).
|
||||||
|
|
||||||
For reproducible release artifacts, set `SOURCE_DATE_EPOCH` to the release
|
For reproducible release artifacts, set `SOURCE_DATE_EPOCH` to the release
|
||||||
commit timestamp (or pass an explicit timezone-qualified `--timestamp`):
|
commit timestamp (or pass an explicit timezone-qualified `--timestamp`):
|
||||||
|
|
||||||
@@ -131,6 +160,23 @@ Start the local release console:
|
|||||||
./.venv/bin/python tools/release/release-console.py
|
./.venv/bin/python tools/release/release-console.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Create and validate a private, declarative installation bundle:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./.venv/bin/python tools/deployment/govoplan-deploy.py init \
|
||||||
|
--directory ~/.local/share/govoplan/installations/default
|
||||||
|
./.venv/bin/python tools/deployment/govoplan-deploy.py doctor \
|
||||||
|
--directory ~/.local/share/govoplan/installations/default
|
||||||
|
```
|
||||||
|
|
||||||
|
The current executable slice and remaining production gates are documented in
|
||||||
|
[Installation and Deployment Architecture](docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md).
|
||||||
|
Same-host replica balancing and the multi-host promotion boundary are documented
|
||||||
|
in [Scaling and Multi-Host Deployment](docs/SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
|
||||||
|
The recovery state machine, migration rollback boundary, and required restore
|
||||||
|
drills are documented in
|
||||||
|
[Recovery and Rollback Guarantees](docs/RECOVERY_AND_ROLLBACK_GUARANTEES.md).
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The repository root `.env.example` is the self-hosted operator template for a
|
The repository root `.env.example` is the self-hosted operator template for a
|
||||||
@@ -151,20 +197,38 @@ Meta ownership and module install/contract boundaries are documented in
|
|||||||
`docs/META_REPO_SCAN.md` and `docs/MODULE_CONTRACTS_AND_INSTALLS.md`.
|
`docs/META_REPO_SCAN.md` and `docs/MODULE_CONTRACTS_AND_INSTALLS.md`.
|
||||||
Frontend layout principles for module pages are documented in
|
Frontend layout principles for module pages are documented in
|
||||||
`docs/FRONTEND_LAYOUT_PRINCIPLES.md`.
|
`docs/FRONTEND_LAYOUT_PRINCIPLES.md`.
|
||||||
|
The provider-neutral datasource boundary and reusable Dataflow/Workflow graph
|
||||||
|
contract are documented in
|
||||||
|
`docs/DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md`.
|
||||||
The cross-product destination, stakeholder visions, configuration archetypes,
|
The cross-product destination, stakeholder visions, configuration archetypes,
|
||||||
connected outcome stories, and capability horizons are documented in
|
connected outcome stories, and capability horizons are documented in
|
||||||
the [Connected Governance Platform Roadmap](docs/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md).
|
the [Connected Governance Platform Roadmap](docs/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md).
|
||||||
|
The reconciled institutional semantics, source-authority modes, module layers,
|
||||||
|
candidate Mandates/Services/Parties/Decisions boundaries, and migration
|
||||||
|
sequence are documented in the
|
||||||
|
[Institutional Governance Target Architecture](docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).
|
||||||
The selected Campaign-to-Postbox-to-data-to-collaboration implementation path,
|
The selected Campaign-to-Postbox-to-data-to-collaboration implementation path,
|
||||||
including stage gates and shared documentation expectations, is in the
|
including stage gates and shared documentation expectations, is in the
|
||||||
[Reference Journey Program](docs/REFERENCE_JOURNEY_PROGRAM.md).
|
[Reference Journey Program](docs/REFERENCE_JOURNEY_PROGRAM.md).
|
||||||
The administrator journey from Core-only bootstrap through online module
|
The administrator journey from Core-only bootstrap through online module
|
||||||
installation, scale-out, and reversible environment promotion is defined in
|
installation, scale-out, and reversible environment promotion is defined in
|
||||||
[System Administrator Lifecycle User Story](docs/SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md).
|
[System Administrator Lifecycle User Story](docs/SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md).
|
||||||
|
The corresponding host deployment compiler, managed/external component choices,
|
||||||
|
reconfiguration semantics, and safe Web update boundary are defined in
|
||||||
|
[Installation and Deployment Architecture](docs/INSTALLATION_AND_DEPLOYMENT_ARCHITECTURE.md).
|
||||||
|
The concrete replica, worker-node, load-balancer, and shared-state topology is
|
||||||
|
defined in [Scaling and Multi-Host Deployment](docs/SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
|
||||||
|
Durable deployment journals, Core recovery evidence, and the distinction
|
||||||
|
between pre-migration configuration restore and post-migration forward recovery
|
||||||
|
are defined in
|
||||||
|
[Recovery and Rollback Guarantees](docs/RECOVERY_AND_ROLLBACK_GUARANTEES.md).
|
||||||
The first Campaign-centric capability and infrastructure fit assessment is in
|
The first Campaign-centric capability and infrastructure fit assessment is in
|
||||||
`docs/CAPABILITY_AND_INFRASTRUCTURE_FIT.md`. Its rerun tooling can collect and
|
`docs/CAPABILITY_AND_INFRASTRUCTURE_FIT.md`. Its rerun tooling can collect and
|
||||||
verify a bounded installed composition; target, provider and production claims
|
verify a bounded installed composition; target, provider and production claims
|
||||||
remain separate, expiring attestations signed by independently scoped proof
|
remain separate, expiring attestations signed by independently scoped proof
|
||||||
authorities.
|
authorities. The operational issuance, target-run, recovery-measurement, key
|
||||||
|
custody, and promotion-gate procedure is in
|
||||||
|
[Target Maturity Evidence Runbook](docs/TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
|
||||||
|
|
||||||
# GovOPlaN Docker
|
# GovOPlaN Docker
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ Current shared profiles:
|
|||||||
- `govoplan/dev/postgres`
|
- `govoplan/dev/postgres`
|
||||||
- `govoplan/dev/production-like`
|
- `govoplan/dev/production-like`
|
||||||
|
|
||||||
|
The generated whole-product Compose profile is owned by
|
||||||
|
`tools/deployment/govoplan-deploy.py`. It renders a deployment-specific
|
||||||
|
`compose.json` from a versioned installation specification; generated files and
|
||||||
|
secrets remain outside the repository.
|
||||||
|
|
||||||
Module-specific Docker test beds remain in their owning repositories:
|
Module-specific Docker test beds remain in their owning repositories:
|
||||||
|
|
||||||
- `govoplan-campaign/dev/mail-testbed`
|
- `govoplan-campaign/dev/mail-testbed`
|
||||||
|
|||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# Backup And Restore Evidence
|
||||||
|
|
||||||
|
## Boundary
|
||||||
|
|
||||||
|
`govoplan-deploy` verifies backup and restore evidence; it does not receive
|
||||||
|
database, object-store, KMS, or orchestrator administration credentials and it
|
||||||
|
does not create the backup. A provider-owned backup controller creates one
|
||||||
|
coordinated recovery point, a separate drill runner restores it into an
|
||||||
|
isolated target, and an evidence authority signs the resulting receipt.
|
||||||
|
|
||||||
|
The application containers receive only a sanitized projection: evidence,
|
||||||
|
recovery-point and drill identifiers, hashes, timestamps, component count, and
|
||||||
|
measured RPO/RTO. Artifact locations, provider credentials, encryption-key
|
||||||
|
references, the public trust keyring, and private signing keys remain in the
|
||||||
|
deployment/evidence boundary.
|
||||||
|
|
||||||
|
The machine-readable contracts are:
|
||||||
|
|
||||||
|
- [`backup-evidence.schema.json`](backup-evidence.schema.json);
|
||||||
|
- [`backup-evidence-keyring.schema.json`](backup-evidence-keyring.schema.json).
|
||||||
|
|
||||||
|
One evidence document is bound to the installation id, deployment profile,
|
||||||
|
topology subject, exact signed release manifest, image digests, and composition
|
||||||
|
digest. It covers PostgreSQL, objects, protected configuration, and recoverable
|
||||||
|
key custody at one recovery point. It contains references, never key material.
|
||||||
|
|
||||||
|
## Production Sequence
|
||||||
|
|
||||||
|
1. Establish the provider snapshot, application quiesce, or transaction
|
||||||
|
boundary and retain a hash of its fencing token.
|
||||||
|
2. Capture PostgreSQL, object storage, protected deployment configuration, and
|
||||||
|
key-custody state within five minutes of that recovery point.
|
||||||
|
3. Restore all four components into a target isolated from production write
|
||||||
|
endpoints and production queues.
|
||||||
|
4. Start the exact immutable release named in the evidence, verify migration
|
||||||
|
heads, verify a deterministic manifest of representative object hashes, and
|
||||||
|
execute the documented semantic journey checks.
|
||||||
|
5. Record actual data loss and elapsed recovery as measured RPO and RTO. A
|
||||||
|
measured RPO above the declared objective invalidates the evidence.
|
||||||
|
6. Sign the canonical receipt using an evidence-authority Ed25519 key held
|
||||||
|
outside the application and deployment host. During key rotation, include
|
||||||
|
both accepted signatures.
|
||||||
|
7. Transfer the evidence SHA-256 through an independent approved channel, then
|
||||||
|
verify and adopt it on the deployment host.
|
||||||
|
|
||||||
|
Provider automation can sign and validate an unsigned receipt with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python tools/deployment/sign-backup-evidence.py \
|
||||||
|
--input unsigned-backup-evidence.json \
|
||||||
|
--output backup-evidence.json \
|
||||||
|
--trusted-keyring backup-evidence-keyring.json \
|
||||||
|
--signing-key backup-authority-2026=/run/keys/backup-authority.pem
|
||||||
|
```
|
||||||
|
|
||||||
|
The private key file must be owner-only. The tool refuses an unexpected key
|
||||||
|
type, an inactive/untrusted signer, malformed or partial evidence, stale
|
||||||
|
recovery points, failed drill checks, mismatched releases, and non-canonical
|
||||||
|
output.
|
||||||
|
|
||||||
|
Adopt the result using the independently obtained digest:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 govoplan-deploy.pyz verify-backup \
|
||||||
|
--directory /srv/govoplan/default \
|
||||||
|
--evidence ./backup-evidence.json \
|
||||||
|
--evidence-sha256 "$APPROVED_BACKUP_EVIDENCE_SHA256" \
|
||||||
|
--trusted-keyring ./backup-evidence-keyring.json \
|
||||||
|
--adopt
|
||||||
|
```
|
||||||
|
|
||||||
|
Evidence is fresh for at most 24 hours and may declare an earlier expiry. Every
|
||||||
|
self-hosted release identity change is conservatively treated as a migration
|
||||||
|
boundary. `doctor`, Compose `apply`, and `render-kubernetes` fail closed when
|
||||||
|
fresh evidence for the previously applied immutable release is unavailable.
|
||||||
|
Compose verifies once before changing runtime state and again after API/worker
|
||||||
|
quiescing immediately before migration. The exported Kubernetes migration Job
|
||||||
|
is generated only after verification and is annotated with the sanitized
|
||||||
|
evidence digest, recovery-point id, and drill id.
|
||||||
|
|
||||||
|
## Provider Runbooks
|
||||||
|
|
||||||
|
### PostgreSQL
|
||||||
|
|
||||||
|
Use a managed transaction-consistent snapshot or a base backup plus retained
|
||||||
|
WAL sufficient to reconstruct the declared point. Record the provider,
|
||||||
|
protected artifact reference and digest, snapshot identity, and PostgreSQL LSN.
|
||||||
|
The restore drill must connect only to the isolated database and must compare
|
||||||
|
the resulting migration-head digest with the release expectation.
|
||||||
|
|
||||||
|
### Object Storage
|
||||||
|
|
||||||
|
Use provider snapshots/versioning or an immutable object copy. Build a sorted
|
||||||
|
manifest containing object key, version, size, and content digest, then record
|
||||||
|
its digest, object count, total bytes, provider version identity, and protected
|
||||||
|
artifact reference. Verify representative objects from every owning module
|
||||||
|
after restore. Single-node managed Garage is persistent but not highly
|
||||||
|
available; copy its coordinated recovery material to an independent failure
|
||||||
|
domain.
|
||||||
|
|
||||||
|
### Configuration And Key Custody
|
||||||
|
|
||||||
|
Back up the private installation bundle and external secret-manager bindings as
|
||||||
|
an encrypted artifact. Record only its reference and digest. For KMS/HSM/vault
|
||||||
|
state, record the provider keyset reference, version, and a successful
|
||||||
|
recoverability assertion. Never put a key, recovery share, token, password, or
|
||||||
|
credential-bearing URL in evidence. The isolated drill must prove that the
|
||||||
|
restored release can decrypt representative protected content without
|
||||||
|
exporting the key material into the report.
|
||||||
|
|
||||||
|
## Ownership And Retention
|
||||||
|
|
||||||
|
The deployment owner approves the RPO/RTO objectives. State-service owners
|
||||||
|
operate backup capture and restoration. Module owners define representative
|
||||||
|
objects and semantic checks. Security owns evidence-authority keys and
|
||||||
|
revocation. Operations schedules drills and retains sanitized status.
|
||||||
|
|
||||||
|
Retain backup artifacts for the approved legal/operational period and at least
|
||||||
|
through the release's rollback window. Retain signed evidence, drill reports,
|
||||||
|
and deletion receipts for the audit period. Disposal must remove every backup
|
||||||
|
copy and provider version according to policy, then revoke or retire references
|
||||||
|
without deleting the audit receipt. Cryptographic erasure is valid only when
|
||||||
|
key-destruction evidence and provider-copy coverage are independently proven.
|
||||||
|
|
||||||
|
## Failure Handling
|
||||||
|
|
||||||
|
Missing components, component-time skew, stale or expired evidence, revocation,
|
||||||
|
signature/key mismatch, changed stored files, release mismatch, failed semantic
|
||||||
|
checks, or an RPO breach block migration. The deployment journal records the
|
||||||
|
rejection without private provider details. If migration has not started, the
|
||||||
|
operator may supply fresh evidence and retry. Once migration starts, recovery
|
||||||
|
is explicitly forward-only until the verified coordinated recovery point is
|
||||||
|
restored with its matching release.
|
||||||
@@ -14,6 +14,14 @@
|
|||||||
| Machine-readable companion | [`capability-fit-current.json`](capability-fit-current.json) |
|
| Machine-readable companion | [`capability-fit-current.json`](capability-fit-current.json) |
|
||||||
| Input schema | [`capability-fit.schema.json`](capability-fit.schema.json) |
|
| Input schema | [`capability-fit.schema.json`](capability-fit.schema.json) |
|
||||||
|
|
||||||
|
**Snapshot notice:** this assessment remains valid only for the pinned
|
||||||
|
2026-07-22 composition above. Workflow Engine, the optional Workflow editor,
|
||||||
|
Datasources, Dataflow, Search, encryption contracts, and other later main-branch
|
||||||
|
work must not be inferred into this evidence record. The current product
|
||||||
|
direction and implemented-state reconciliation are documented separately in
|
||||||
|
the
|
||||||
|
[Institutional Governance Target Architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).
|
||||||
|
|
||||||
This is a fit assessment, not a production approval or security certification.
|
This is a fit assessment, not a production approval or security certification.
|
||||||
It deliberately does not infer implementation from a repository, issue, or
|
It deliberately does not infer implementation from a repository, issue, or
|
||||||
manifest existing. A conclusion needs code plus a route/contract, test,
|
manifest existing. A conclusion needs code plus a route/contract, test,
|
||||||
@@ -57,7 +65,7 @@ profile intentionally runs only PostgreSQL and Redis in containers; API,
|
|||||||
WebUI, worker, and scheduler processes still run from editable source trees. A
|
WebUI, worker, and scheduler processes still run from editable source trees. A
|
||||||
target deployment must supply TLS termination, process supervision, secret
|
target deployment must supply TLS termination, process supervision, secret
|
||||||
injection, monitoring, backup storage, and recovery procedures. The open
|
injection, monitoring, backup storage, and recovery procedures. The open
|
||||||
[Core backup/restore issue #29](https://git.add-ideas.de/add-ideas/govoplan-core/issues/29)
|
[Core backup/restore issue #29](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/29)
|
||||||
is a production gate. Identity federation, external audit export, and a tested
|
is a production gate. Identity federation, external audit export, and a tested
|
||||||
disaster-recovery plan are also not complete.
|
disaster-recovery plan are also not complete.
|
||||||
|
|
||||||
@@ -134,7 +142,7 @@ persistence, and durable shared or object storage. Run one scheduler only when a
|
|||||||
selected module needs scheduled recovery. Multiple scheduler replicas require
|
selected module needs scheduled recovery. Multiple scheduler replicas require
|
||||||
leader election or an external lock, which is not established by this report.
|
leader election or an external lock, which is not established by this report.
|
||||||
|
|
||||||
This topology is a recommendation from the [Ops scalability profiles](https://git.add-ideas.de/add-ideas/govoplan-ops/src/branch/main/docs/SCALABILITY_PROFILES.md),
|
This topology is a recommendation from the [Ops scalability profiles](https://git.add-ideas.de/GovOPlaN/govoplan-ops/src/branch/main/docs/SCALABILITY_PROFILES.md),
|
||||||
not a currently shipped production Compose/Kubernetes/systemd package.
|
not a currently shipped production Compose/Kubernetes/systemd package.
|
||||||
|
|
||||||
## Functional capability matrix
|
## Functional capability matrix
|
||||||
@@ -154,7 +162,7 @@ not a currently shipped production Compose/Kubernetes/systemd package.
|
|||||||
| Configured-system documentation and Ops status pages | `verified` | Docs/Ops manifests contribute protected routes and WebUI packages; Ops code checks database, Redis, workers, storage and deployment-security settings. | This does not replace external monitoring or a target runbook. |
|
| Configured-system documentation and Ops status pages | `verified` | Docs/Ops manifests contribute protected routes and WebUI packages; Ops code checks database, Redis, workers, storage and deployment-security settings. | This does not replace external monitoring or a target runbook. |
|
||||||
| Calendar/CalDAV integration | `partial` | Calendar `v0.1.8` supplies the catalogued storage/sync foundation. The durable external-write outbox, worker recovery, reconciliation, and retention work is committed, tested, and pushed on Calendar `main` after that tag. | The post-tag outbox work is remote-integrated source, not local-only WIP, but it is not in the signed stable package baseline and has not passed a target CalDAV drill. Bulk synchronized-calendar migration semantics remain separate work. |
|
| Calendar/CalDAV integration | `partial` | Calendar `v0.1.8` supplies the catalogued storage/sync foundation. The durable external-write outbox, worker recovery, reconciliation, and retention work is committed, tested, and pushed on Calendar `main` after that tag. | The post-tag outbox work is remote-integrated source, not local-only WIP, but it is not in the signed stable package baseline and has not passed a target CalDAV drill. Bulk synchronized-calendar migration semantics remain separate work. |
|
||||||
| External LDAP/AD, OIDC/SAML or SCIM identity integration | `scaffold` | IDM owns normalized assignment APIs and documents connector boundaries. | Provider connectors, login callback flow and target directory reconciliation are not an implemented end-to-end capability. Use local accounts for this pilot. |
|
| External LDAP/AD, OIDC/SAML or SCIM identity integration | `scaffold` | IDM owns normalized assignment APIs and documents connector boundaries. | Provider connectors, login callback flow and target directory reconciliation are not an implemented end-to-end capability. Use local accounts for this pilot. |
|
||||||
| Export-control/embargo-list screening | `planned` | Product-level [GovOPlaN #12](https://git.add-ideas.de/add-ideas/govoplan/issues/12) defines the consumer-independent user story. | No screening provider, list provenance, matching policy, review flow or legal evidence exists in this composition. |
|
| Export-control/embargo-list screening | `planned` | Product-level [GovOPlaN #12](https://git.add-ideas.de/GovOPlaN/govoplan/issues/12) defines the consumer-independent user story. | No screening provider, list provenance, matching policy, review flow or legal evidence exists in this composition. |
|
||||||
| Workflow-driven journeys and views | `planned` | Workflow contracts/concepts exist outside this assessment. | Explicitly postponed. Do not include Workflow in pilot or production claims from this report. |
|
| Workflow-driven journeys and views | `planned` | Workflow contracts/concepts exist outside this assessment. | Explicitly postponed. Do not include Workflow in pilot or production claims from this report. |
|
||||||
|
|
||||||
## Infrastructure matrix
|
## Infrastructure matrix
|
||||||
@@ -177,13 +185,13 @@ not a currently shipped production Compose/Kubernetes/systemd package.
|
|||||||
| Health/readiness | `verified` | `/health`, protected `/health/details`, and Ops checks for DB, Redis, workers, storage, maintenance and cookie/CORS posture. | Add external probes and distinguish liveness from dependency readiness for the chosen orchestrator. |
|
| Health/readiness | `verified` | `/health`, protected `/health/details`, and Ops checks for DB, Redis, workers, storage, maintenance and cookie/CORS posture. | Add external probes and distinguish liveness from dependency readiness for the chosen orchestrator. |
|
||||||
| Metrics, logs and alerting | `partial` | Correlation IDs, slow-request/query metrics in logs, worker inspection and operator status are implemented. | No bundled metrics exporter, log collector, dashboards, queue-depth alerts, pager route or SLO is verified. |
|
| Metrics, logs and alerting | `partial` | Correlation IDs, slow-request/query metrics in logs, worker inspection and operator status are implemented. | No bundled metrics exporter, log collector, dashboards, queue-depth alerts, pager route or SLO is verified. |
|
||||||
| Audit | `partial` | Local audit tables and retry outbox are verified. | Retention, tamper-evident export, privileged access review and SIEM integration are unproved. |
|
| Audit | `partial` | Local audit tables and retry outbox are verified. | Retention, tamper-evident export, privileged access review and SIEM integration are unproved. |
|
||||||
| Backup and restore | `partial` | Operator guide and installer hooks describe `pg_dump`/`pg_restore`; SQLite and simulated installer rollback drills exist. | [Core #29](https://git.add-ideas.de/add-ideas/govoplan-core/issues/29) remains open. No target PostgreSQL + files + secrets restore drill or measured RTO/RPO exists. |
|
| Backup and restore | `partial` | Operator guide and installer hooks describe `pg_dump`/`pg_restore`; SQLite and simulated installer rollback drills exist. | [Core #29](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/29) remains open. No target PostgreSQL + files + secrets restore drill or measured RTO/RPO exists. |
|
||||||
| Disaster recovery | `not_assessed` | The Ops guide asks for RPO/RTO and restore drills. | No agreed RPO/RTO, off-site copy, failover topology, dependency recovery order, communications plan or exercise evidence was supplied. |
|
| Disaster recovery | `not_assessed` | The Ops guide asks for RPO/RTO and restore drills. | No agreed RPO/RTO, off-site copy, failover topology, dependency recovery order, communications plan or exercise evidence was supplied. |
|
||||||
|
|
||||||
The scalability and sizing documentation delivered the documentation portions
|
The scalability and sizing documentation delivered the documentation portions
|
||||||
of [Core #217](https://git.add-ideas.de/add-ideas/govoplan-core/issues/217) and
|
of [Core #217](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/217) and
|
||||||
[Core #219](https://git.add-ideas.de/add-ideas/govoplan-core/issues/219).
|
[Core #219](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/219).
|
||||||
[Core #28](https://git.add-ideas.de/add-ideas/govoplan-core/issues/28) records the
|
[Core #28](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/28) records the
|
||||||
operator-documentation slice. These closed tickets are evidence of documented
|
operator-documentation slice. These closed tickets are evidence of documented
|
||||||
models, not evidence that an organization's production environment has passed
|
models, not evidence that an organization's production environment has passed
|
||||||
them.
|
them.
|
||||||
@@ -468,10 +476,11 @@ which modules a running service activated, migrations, configuration, health,
|
|||||||
or reference-journey behavior. Runtime activation therefore remains an explicit
|
or reference-journey behavior. Runtime activation therefore remains an explicit
|
||||||
unchecked boundary.
|
unchecked boundary.
|
||||||
|
|
||||||
### Target, provider and production proof boundary
|
### Reference-readiness, provider and production proof boundary
|
||||||
|
|
||||||
Installed evidence cannot approve a target environment, an external provider,
|
Installed evidence cannot establish target acceptance, accessibility, privacy,
|
||||||
or production use. These scopes use a separate, expiring
|
security, operations, recovery, an external provider, or production use. These
|
||||||
|
scopes use a separate, expiring
|
||||||
[`capability-fit-boundary-evidence.schema.json`](capability-fit-boundary-evidence.schema.json)
|
[`capability-fit-boundary-evidence.schema.json`](capability-fit-boundary-evidence.schema.json)
|
||||||
bundle. The bundle is bound to the assessment ID, assessment release and exact
|
bundle. The bundle is bound to the assessment ID, assessment release and exact
|
||||||
installed-evidence SHA-256 digest. It contains only opaque subject/control/result
|
installed-evidence SHA-256 digest. It contains only opaque subject/control/result
|
||||||
@@ -480,8 +489,9 @@ IDs and content hashes, not endpoints, credentials, people or raw result files.
|
|||||||
Boundary evidence is accepted only when at least one Ed25519 signature validates
|
Boundary evidence is accepted only when at least one Ed25519 signature validates
|
||||||
against a separately provisioned
|
against a separately provisioned
|
||||||
[`capability-fit-proof-authority-keyring.schema.json`](capability-fit-proof-authority-keyring.schema.json).
|
[`capability-fit-proof-authority-keyring.schema.json`](capability-fit-proof-authority-keyring.schema.json).
|
||||||
Each authority key explicitly lists the scopes it may attest. Target and provider
|
Each authority key explicitly lists the scopes it may attest. Target,
|
||||||
claims use `passed` or `failed`; production claims use `approved` or `rejected`.
|
accessibility, privacy, security, operations, recovery, and provider claims use
|
||||||
|
`passed` or `failed`; production claims use `approved` or `rejected`.
|
||||||
One claim per scope, unique control/artifact IDs, `issued_at < expires_at`, current
|
One claim per scope, unique control/artifact IDs, `issued_at < expires_at`, current
|
||||||
validity and exact digest binding are mandatory. Any schema, binding, time,
|
validity and exact digest binding are mandatory. Any schema, binding, time,
|
||||||
signature or authority blocker leaves every supplied boundary claim unchecked;
|
signature or authority blocker leaves every supplied boundary claim unchecked;
|
||||||
@@ -493,6 +503,20 @@ Signatures cover UTF-8 JSON with the `signatures` member omitted, object keys
|
|||||||
sorted, compact `,`/`:` separators and non-ASCII characters escaped, matching
|
sorted, compact `,`/`:` separators and non-ASCII characters escaped, matching
|
||||||
the tool's deterministic canonicalization.
|
the tool's deterministic canonicalization.
|
||||||
|
|
||||||
|
`tools/assessments/boundary-evidence.py` is the bounded issuance path. It
|
||||||
|
accepts a private target-run manifest conforming to
|
||||||
|
[`capability-fit-boundary-run.schema.json`](capability-fit-boundary-run.schema.json),
|
||||||
|
hashes each retained result file without following a final-component symlink,
|
||||||
|
and excludes all paths and raw results from the signed receipt. Issuance is
|
||||||
|
refused unless an independently trusted catalog, exact installed payload,
|
||||||
|
signed installer receipt, and role-scoped installer authority already pass.
|
||||||
|
Every claim must be covered by a supplied Ed25519 private key whose public key
|
||||||
|
is authorized for the full proof interval; catalog and installer key reuse is
|
||||||
|
rejected. The command immediately verifies its own result and atomically writes
|
||||||
|
both the proof and a sanitized review. The complete operator procedure and
|
||||||
|
recovery measurement definition are in
|
||||||
|
[`TARGET_MATURITY_EVIDENCE_RUNBOOK.md`](TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./.venv/bin/python tools/assessments/capability-fit.py \
|
./.venv/bin/python tools/assessments/capability-fit.py \
|
||||||
--public \
|
--public \
|
||||||
@@ -503,6 +527,14 @@ the tool's deterministic canonicalization.
|
|||||||
--expected-external-provider-subject provider-production
|
--expected-external-provider-subject provider-production
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Promotion automation must opt into its required boundaries. Add
|
||||||
|
`--require-reference-readiness` to require all six target scopes,
|
||||||
|
`--require-external-provider-proof` when the product depends on a provider, and
|
||||||
|
`--require-production-approval` for production admission. These switches turn
|
||||||
|
missing, expired, revoked, mismatched, negative, or otherwise unchecked claims
|
||||||
|
into a blocking exit status rather than merely reporting them as an unproven
|
||||||
|
boundary.
|
||||||
|
|
||||||
With `--installed-evidence`, this command performs comparison and proof-binding
|
With `--installed-evidence`, this command performs comparison and proof-binding
|
||||||
diagnostics. Without an installer receipt, the imported document remains
|
diagnostics. Without an installer receipt, the imported document remains
|
||||||
unsigned, so neither it nor the boundary claim becomes accepted proof. Direct
|
unsigned, so neither it nor the boundary claim becomes accepted proof. Direct
|
||||||
@@ -554,6 +586,10 @@ Target-environment and production-approval claims must use the assessment's
|
|||||||
operator to supply a bounded opaque expected subject with
|
operator to supply a bounded opaque expected subject with
|
||||||
`--expected-external-provider-subject`; without it, such a claim remains
|
`--expected-external-provider-subject`; without it, such a claim remains
|
||||||
unchecked and blocks. Expected and observed IDs are retained in proof scope.
|
unchecked and blocks. Expected and observed IDs are retained in proof scope.
|
||||||
|
Accessibility, privacy, security, operations, and recovery claims use the same
|
||||||
|
deployment subject. The report emits a cumulative `reference_readiness` verdict
|
||||||
|
only when all six required scopes are checked and positive. This verdict remains
|
||||||
|
separate from production approval and from provider-specific acceptance.
|
||||||
|
|
||||||
Both authority keyrings are governance trust roots. Installer receipt keys use
|
Both authority keyrings are governance trust roots. Installer receipt keys use
|
||||||
the strict
|
the strict
|
||||||
@@ -575,22 +611,22 @@ cannot relabel it as current. Live admission must omit it and use the actual
|
|||||||
current time.
|
current time.
|
||||||
|
|
||||||
No boundary bundle or production authority has been supplied for this current
|
No boundary bundle or production authority has been supplied for this current
|
||||||
assessment. Target environment, provider and production proof therefore remain
|
assessment. Reference-readiness, provider, and production proof therefore
|
||||||
explicitly unchecked rather than inferred from the local GreenMail journey,
|
remain explicitly unchecked rather than inferred from the local GreenMail
|
||||||
source tests or signed release metadata.
|
journey, source tests, or signed release metadata.
|
||||||
|
|
||||||
## Evidence used in this slice
|
## Evidence used in this slice
|
||||||
|
|
||||||
- [Production-like profile](../dev/production-like/README.md) and
|
- [Production-like profile](../dev/production-like/README.md) and
|
||||||
[Compose dependencies](../dev/production-like/docker-compose.yml)
|
[Compose dependencies](../dev/production-like/docker-compose.yml)
|
||||||
- [Module contracts and install boundaries](MODULE_CONTRACTS_AND_INSTALLS.md)
|
- [Module contracts and install boundaries](MODULE_CONTRACTS_AND_INSTALLS.md)
|
||||||
- [Core deployment operator guide](https://git.add-ideas.de/add-ideas/govoplan-core/src/branch/main/docs/DEPLOYMENT_OPERATOR_GUIDE.md)
|
- [Core deployment operator guide](https://git.add-ideas.de/GovOPlaN/govoplan-core/src/branch/main/docs/DEPLOYMENT_OPERATOR_GUIDE.md)
|
||||||
- [Ops scalability profiles](https://git.add-ideas.de/add-ideas/govoplan-ops/src/branch/main/docs/SCALABILITY_PROFILES.md)
|
- [Ops scalability profiles](https://git.add-ideas.de/GovOPlaN/govoplan-ops/src/branch/main/docs/SCALABILITY_PROFILES.md)
|
||||||
- Actual module manifests in the pinned repositories and the static contract
|
- Actual module manifests in the pinned repositories and the static contract
|
||||||
checker in this meta repository
|
checker in this meta repository
|
||||||
- Core module-system/API smoke/auth/install-config tests, plus focused Campaign,
|
- Core module-system/API smoke/auth/install-config tests, plus focused Campaign,
|
||||||
Files, Mail, Audit and Addresses tests
|
Files, Mail, Audit and Addresses tests
|
||||||
- [Campaign delivery runbook](https://git.add-ideas.de/add-ideas/govoplan-campaign/src/branch/main/docs/CAMPAIGN_DELIVERY_RUNBOOK.md)
|
- [Campaign delivery runbook](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/src/branch/main/docs/CAMPAIGN_DELIVERY_RUNBOOK.md)
|
||||||
- [Live signed stable catalog](https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json)
|
- [Live signed stable catalog](https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json)
|
||||||
and [published keyring](https://govoplan.add-ideas.de/catalogs/v1/keyring.json),
|
and [published keyring](https://govoplan.add-ideas.de/catalogs/v1/keyring.json),
|
||||||
verified against a separately provisioned local trust keyring
|
verified against a separately provisioned local trust keyring
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ baseline to that destination.
|
|||||||
|
|
||||||
It is a durable direction, not a release promise or a substitute for issue
|
It is a durable direction, not a release promise or a substitute for issue
|
||||||
tracking. Live work state belongs in Gitea issues. The
|
tracking. Live work state belongs in Gitea issues. The
|
||||||
[Core master roadmap](https://git.add-ideas.de/add-ideas/govoplan-core/src/branch/main/docs/GOVOPLAN_MASTER_ROADMAP.md)
|
[Core master roadmap](https://git.add-ideas.de/GovOPlaN/govoplan-core/src/branch/main/docs/GOVOPLAN_MASTER_ROADMAP.md)
|
||||||
remains the technical module and wave sequence; this document supplies the
|
remains the technical module and wave sequence; this document supplies the
|
||||||
cross-product vision that sequence serves.
|
cross-product vision that sequence serves.
|
||||||
|
|
||||||
Read it together with:
|
Read it together with:
|
||||||
|
|
||||||
|
- the [institutional governance target architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md)
|
||||||
- the [selected reference-journey program](REFERENCE_JOURNEY_PROGRAM.md)
|
- the [selected reference-journey program](REFERENCE_JOURNEY_PROGRAM.md)
|
||||||
- the [current capability and infrastructure fit assessment](CAPABILITY_AND_INFRASTRUCTURE_FIT.md)
|
- the [current capability and infrastructure fit assessment](CAPABILITY_AND_INFRASTRUCTURE_FIT.md)
|
||||||
- the [interface pattern language](INTERFACE_PATTERN_LANGUAGE.md)
|
- the [interface pattern language](INTERFACE_PATTERN_LANGUAGE.md)
|
||||||
@@ -151,7 +152,7 @@ responsibilities, not a new dependency hierarchy.
|
|||||||
| Participation and channels | Let internal and external actors enter, receive, discuss, schedule, and respond through suitable channels. | Portal, postbox, mail, notifications, calendar, scheduling, appointments, campaign, consultation, poll |
|
| Participation and channels | Let internal and external actors enter, receive, discuss, schedule, and respond through suitable channels. | Portal, postbox, mail, notifications, calendar, scheduling, appointments, campaign, consultation, poll |
|
||||||
| Work coordination | Turn an input into owned, reviewable work and make exceptions visible. | Forms/runtime, cases, tasks, approvals, workflow, booking, resources, domain modules |
|
| Work coordination | Turn an input into owned, reviewable work and make exceptions visible. | Forms/runtime, cases, tasks, approvals, workflow, booking, resources, domain modules |
|
||||||
| Evidence and institutional memory | Preserve what was known, decided, produced, sent, received, retained, corrected, and disclosed. | Files, templates, DMS, records, audit, search, reporting, transparency |
|
| Evidence and institutional memory | Preserve what was known, decided, produced, sent, received, retained, corrected, and disclosed. | Files, templates, DMS, records, audit, search, reporting, transparency |
|
||||||
| Institutional governance | Establish who may act, in which capacity, for which organization and tenant, under which policy. | Identity, access, IDM, tenancy, organizations, policy, identity trust, risk/compliance |
|
| Institutional governance | Establish who may act, in which capacity, for which organization and tenant, under which mandate, jurisdiction, and policy. | Identity, access, IDM, tenancy, organizations, policy, identity trust, risk/compliance; candidate mandate and decision contracts |
|
||||||
| Integration and operations | Connect sources and destinations, operate them safely, and prove their health and recovery. | Connectors, REST/SOAP and public-sector protocols, mail/calendar/file adapters, ERP handoffs, ops, release and configuration packages |
|
| Integration and operations | Connect sources and destinations, operate them safely, and prove their health and recovery. | Connectors, REST/SOAP and public-sector protocols, mail/calendar/file adapters, ERP handoffs, ops, release and configuration packages |
|
||||||
|
|
||||||
Across these planes, GovOPlaN should maintain a connected context graph rather
|
Across these planes, GovOPlaN should maintain a connected context graph rather
|
||||||
@@ -448,7 +449,7 @@ without creating disconnected ticket systems.
|
|||||||
- As a manager, I can distinguish demand, backlog, SLA risk, recurring cause,
|
- As a manager, I can distinguish demand, backlog, SLA risk, recurring cause,
|
||||||
and verified resolution.
|
and verified resolution.
|
||||||
|
|
||||||
**Composition.** Issue reporting, helpdesk, cases, tasks, facilities, assets,
|
**Composition.** Tickets, helpdesk profiles, cases, tasks, facilities, assets,
|
||||||
inspections, booking, resources, calendar, files, notifications, connectors,
|
inspections, booking, resources, calendar, files, notifications, connectors,
|
||||||
reporting, policy, and audit.
|
reporting, policy, and audit.
|
||||||
|
|
||||||
@@ -530,7 +531,7 @@ access, no hidden cross-module coupling, correction rather than fictional undo,
|
|||||||
and a durable action/effect trail.
|
and a durable action/effect trail.
|
||||||
|
|
||||||
**Roadmap role.** The canonical product story is
|
**Roadmap role.** The canonical product story is
|
||||||
[meta issue #12](https://git.add-ideas.de/add-ideas/govoplan/issues/12). It is a
|
[meta issue #12](https://git.add-ideas.de/GovOPlaN/govoplan/issues/12). It is a
|
||||||
future cross-product configuration and contract program, not Campaign work and
|
future cross-product configuration and contract program, not Campaign work and
|
||||||
not a claim that screening is implemented today.
|
not a claim that screening is implemented today.
|
||||||
|
|
||||||
@@ -1136,11 +1137,12 @@ boundary, and retain operational, security, and evidence guarantees.
|
|||||||
| Addresses and directories | Implemented adapters exist but configuration/target proof varies | Directory source, privacy, conflict and lifecycle package | Reusable people/contact source capability |
|
| Addresses and directories | Implemented adapters exist but configuration/target proof varies | Directory source, privacy, conflict and lifecycle package | Reusable people/contact source capability |
|
||||||
| Docs/admin/ops/dashboard | Useful cross-product surfaces with incomplete rollout | Configured-system inventory, guided config, monitoring/recovery evidence | Explainability and operation of the configured product |
|
| Docs/admin/ops/dashboard | Useful cross-product surfaces with incomplete rollout | Configured-system inventory, guided config, monitoring/recovery evidence | Explainability and operation of the configured product |
|
||||||
| Organizations/identity/IDM/access/postbox | Normalized ownership concepts and uneven runtime slices | Function-bound delivery, reassignment/delegation, vacancy and access-evidence proof | Institutional responsibility and durable communication spine |
|
| Organizations/identity/IDM/access/postbox | Normalized ownership concepts and uneven runtime slices | Function-bound delivery, reassignment/delegation, vacancy and access-evidence proof | Institutional responsibility and durable communication spine |
|
||||||
| Forms/cases/tasks/approvals/search | Concepts and uneven first slices | Extend the proven responsibility path into one manual end-to-end work/evidence journey | Reusable administrative coordination layer |
|
| Forms/cases/tasks/approvals | Concepts and uneven first slices | Extend the proven responsibility path into one manual end-to-end work/evidence journey, with shared service, party, mandate, and decision semantics | Reusable administrative coordination layer |
|
||||||
| Templates/reporting/data sources | Boundary concepts or scaffolds | One reproducible data-backed document/report and safe HIS-style launch | Governed document production, reports, dashboards, and analytical consumption |
|
| Search | PostgreSQL-backed permission-aware provider and global/contextual UI foundation | Complete provider rollout, indexing operations, and target authorization/performance evidence | Optional cross-module discovery with OpenSearch only as an adapter |
|
||||||
| Analytical data products/dataflow | Selected direction; platform contracts not yet implemented | One bounded university source-to-indicator path with staging, quality, lineage and promotion proof | Transparent institutional BI and cross-process reporting |
|
| Templates/reporting/data sources | Templates and governed Datasources foundations exist; Reporting remains an early semantic-model slice | One reproducible data-backed document/report and safe HIS-style launch | Governed document production, reports, dashboards, and analytical consumption |
|
||||||
|
| Analytical data products/dataflow | Typed graph/runtime foundations, triggers, staging integration, reusable definitions, and golden-flow fixtures exist | One bounded university source-to-indicator path with quality, lineage, promotion, and target-performance proof | Transparent institutional BI and cross-process reporting |
|
||||||
| DMS/collaborative editing | Boundary concept and tag-only scaffold | One Files-backed version lifecycle, then one provider-neutral editing session | Collaborative documents, review, approval and records-ready renditions |
|
| DMS/collaborative editing | Boundary concept and tag-only scaffold | One Files-backed version lifecycle, then one provider-neutral editing session | Collaborative documents, review, approval and records-ready renditions |
|
||||||
| Workflow/automation | Concept only; no discoverable Workflow runtime; program postponed | Stable action/effect providers and an explicitly reprioritized bounded journey | Configurable governed process coordination |
|
| Workflow/automation | Headless Workflow Engine and optional editor are implemented foundations with versioned module baselines, BPMN interchange, action/effect execution, and reconciliation | Prove one resumable human/system journey with target recovery and conformance evidence | Configurable governed process coordination without becoming a second domain layer |
|
||||||
| Domain modules | Mostly boundary concepts or seeds | Only the modules required by a reference package | Reusable semantics above the shared spine |
|
| Domain modules | Mostly boundary concepts or seeds | Only the modules required by a reference package | Reusable semantics above the shared spine |
|
||||||
| Connectors and protocols | Catalogue/strategy plus several module-specific adapters | Profile/runtime, source-of-truth, health and one real landscape | Coexistence with institutional IT |
|
| Connectors and protocols | Catalogue/strategy plus several module-specific adapters | Profile/runtime, source-of-truth, health and one real landscape | Coexistence with institutional IT |
|
||||||
| Records/transparency/risk-compliance/export screening | Planned or early concepts | Evidence contracts and one regulated reference story | Institutional memory, oversight, and assurance |
|
| Records/transparency/risk-compliance/export screening | Planned or early concepts | Evidence contracts and one regulated reference story | Institutional memory, oversight, and assurance |
|
||||||
@@ -1216,8 +1218,9 @@ provides all applicable evidence below.
|
|||||||
## Near-term portfolio order
|
## Near-term portfolio order
|
||||||
|
|
||||||
This order is now selected. Detailed slices and gates are in the
|
This order is now selected. Detailed slices and gates are in the
|
||||||
[Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md). Workflow-driven
|
[Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md). Workflow Engine and
|
||||||
user-story implementation remains paused.
|
the optional editor may support these stages, but Workflow work enters the
|
||||||
|
portfolio only through an explicit bounded package or reference journey.
|
||||||
|
|
||||||
0. **Continuous safe foundation.** Keep the composition green and version
|
0. **Continuous safe foundation.** Keep the composition green and version
|
||||||
aligned; close connector destination pinning, response limits, throttling,
|
aligned; close connector destination pinning, response limits, throttling,
|
||||||
@@ -1250,8 +1253,9 @@ user-story implementation remains paused.
|
|||||||
target acceptance for calendar/scheduling/poll and other shipped foundations
|
target acceptance for calendar/scheduling/poll and other shipped foundations
|
||||||
without displacing the selected reference path; implement new feature
|
without displacing the selected reference path; implement new feature
|
||||||
programs only when they are required by a stage or explicitly reprioritized.
|
programs only when they are required by a stage or explicitly reprioritized.
|
||||||
7. **Resume Workflow only by explicit priority decision.** When resumed, start
|
7. **Use Workflow only through bounded journeys.** The headless engine and
|
||||||
with stable actions from a demonstrated package; do not turn it into a
|
optional editor now exist. Extend them through stable module-owned actions,
|
||||||
|
versioned baselines, and demonstrated packages; do not turn Workflow into a
|
||||||
second domain layer.
|
second domain layer.
|
||||||
|
|
||||||
## Product decisions to make progressively
|
## Product decisions to make progressively
|
||||||
@@ -1263,8 +1267,8 @@ them; they do not block the product vision today.
|
|||||||
reference release?
|
reference release?
|
||||||
- Which real identity, groupware, file/DMS, and deployment stack should define
|
- Which real identity, groupware, file/DMS, and deployment stack should define
|
||||||
the first integration profile?
|
the first integration profile?
|
||||||
- When should the postponed Workflow program resume, and which single package
|
- Which demonstrated package should provide the first target-accepted Workflow
|
||||||
will constrain its first implementation?
|
execution and recovery profile?
|
||||||
- Which objects and fields remain authoritative in GovOPlaN versus each target
|
- Which objects and fields remain authoritative in GovOPlaN versus each target
|
||||||
system, and which conflict/failure behavior is acceptable?
|
system, and which conflict/failure behavior is acceptable?
|
||||||
- Which default participant privacy profiles should ship for scheduling,
|
- Which default participant privacy profiles should ship for scheduling,
|
||||||
@@ -1279,8 +1283,9 @@ them; they do not block the product vision today.
|
|||||||
- Which exact HIS/CampusOnline interfaces, student-statistics fields and
|
- Which exact HIS/CampusOnline interfaces, student-statistics fields and
|
||||||
official keys, accepted calculation, freeze/correction policy, privacy
|
official keys, accepted calculation, freeze/correction policy, privacy
|
||||||
profile, and drill-down level should define the first analytical data product?
|
profile, and drill-down level should define the first analytical data product?
|
||||||
- After the first source-to-report proof, do repeated source/dataflow contracts
|
- Which database, REST, directory, and managed-file providers should follow the
|
||||||
justify separate `govoplan-datasources` and `govoplan-dataflow` modules?
|
implemented separation of `govoplan-connectors`, `govoplan-datasources`, and
|
||||||
|
`govoplan-dataflow`, and which quality/promotion policy should prove it first?
|
||||||
- Which collaborative editor should be the first target, and should its first
|
- Which collaborative editor should be the first target, and should its first
|
||||||
accepted experience emphasize concurrent editing, controlled check-out, or
|
accepted experience emphasize concurrent editing, controlled check-out, or
|
||||||
both?
|
both?
|
||||||
@@ -1381,7 +1386,7 @@ integration. Conversely, 30 repositories had no open issue; for many
|
|||||||
later-wave modules this meant no implementation program had been opened, not
|
later-wave modules this meant no implementation program had been opened, not
|
||||||
that the capability was complete.
|
that the capability was complete.
|
||||||
|
|
||||||
[Poll #2](https://git.add-ideas.de/add-ideas/govoplan-poll/issues/2) was a clear
|
[Poll #2](https://git.add-ideas.de/GovOPlaN/govoplan-poll/issues/2) was a clear
|
||||||
tracker-drift example: its configurable transition engine, agreed transition
|
tracker-drift example: its configurable transition engine, agreed transition
|
||||||
matrix/history, idempotent keyed retries, re-decision audit, archive/unarchive,
|
matrix/history, idempotent keyed retries, re-decision audit, archive/unarchive,
|
||||||
and preservation behavior were implemented and pushed while the issue still
|
and preservation behavior were implemented and pushed while the issue still
|
||||||
@@ -1389,27 +1394,27 @@ reported `needs-info`.
|
|||||||
|
|
||||||
Issue anchors that informed the bridge from the baseline into this roadmap:
|
Issue anchors that informed the bridge from the baseline into this roadmap:
|
||||||
|
|
||||||
- [Meta #10](https://git.add-ideas.de/add-ideas/govoplan/issues/10) for the
|
- [Meta #10](https://git.add-ideas.de/GovOPlaN/govoplan/issues/10) for the
|
||||||
capability/infrastructure assessment and its target proof;
|
capability/infrastructure assessment and its target proof;
|
||||||
- [Meta #11](https://git.add-ideas.de/add-ideas/govoplan/issues/11) for the
|
- [Meta #11](https://git.add-ideas.de/GovOPlaN/govoplan/issues/11) for the
|
||||||
universal interface and focused-view direction;
|
universal interface and focused-view direction;
|
||||||
- [Core #225](https://git.add-ideas.de/add-ideas/govoplan-core/issues/225) for
|
- [Core #225](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/225) for
|
||||||
guided, safe configuration;
|
guided, safe configuration;
|
||||||
- [Core #29](https://git.add-ideas.de/add-ideas/govoplan-core/issues/29) for the
|
- [Core #29](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/29) for the
|
||||||
backup/restore production gate;
|
backup/restore production gate;
|
||||||
- [Core #263](https://git.add-ideas.de/add-ideas/govoplan-core/issues/263) and
|
- [Core #263](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/263) and
|
||||||
[Campaign #63](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/63),
|
[Campaign #63](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/63),
|
||||||
[#62](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/62),
|
[#62](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/62),
|
||||||
[#65](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/65), and
|
[#65](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/65), and
|
||||||
[#69](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/69) for the
|
[#69](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/69) for the
|
||||||
reference interface/delivery vocabulary and behavior;
|
reference interface/delivery vocabulary and behavior;
|
||||||
- [Poll #1](https://git.add-ideas.de/add-ideas/govoplan-poll/issues/1) for the
|
- [Poll #1](https://git.add-ideas.de/GovOPlaN/govoplan-poll/issues/1) for the
|
||||||
database-enforced respondent invariant exposed by Scheduling;
|
database-enforced respondent invariant exposed by Scheduling;
|
||||||
- [Connectors #6](https://git.add-ideas.de/add-ideas/govoplan-connectors/issues/6)
|
- [Connectors #6](https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/6)
|
||||||
for the governed connector configuration/simulation foundation;
|
for the governed connector configuration/simulation foundation;
|
||||||
- [Meta #9](https://git.add-ideas.de/add-ideas/govoplan/issues/9) for the first
|
- [Meta #9](https://git.add-ideas.de/GovOPlaN/govoplan/issues/9) for the first
|
||||||
permit-to-payment reference process; and
|
permit-to-payment reference process; and
|
||||||
- [Meta #12](https://git.add-ideas.de/add-ideas/govoplan/issues/12) for the
|
- [Meta #12](https://git.add-ideas.de/GovOPlaN/govoplan/issues/12) for the
|
||||||
deliberately deferred, consumer-independent export-control story.
|
deliberately deferred, consumer-independent export-control story.
|
||||||
|
|
||||||
Live Gitea issue state remains canonical. These dated facts explain the roadmap
|
Live Gitea issue state remains canonical. These dated facts explain the roadmap
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# Datasource And Definition Graph Architecture
|
||||||
|
|
||||||
|
## Two-Layer Data Boundary
|
||||||
|
|
||||||
|
GovOPlaN separates governed data identity from external acquisition:
|
||||||
|
|
||||||
|
| Layer | Owner | Responsibilities |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Datasource layer | `govoplan-datasources` | Governed data/register catalogue, tenant visibility, live/cached/static mode, source authority, staging, immutable materializations, frozen states, schema, quality/freshness policy, institutional provenance, dependencies, and bounded reads |
|
||||||
|
| Connector layer | `govoplan-connectors` and protocol/provider modules | External protocols, endpoints, connection profiles, credentials, discovery, provider maturity/authority support, health, source-side filtering, query pushdown, and effect reconciliation |
|
||||||
|
|
||||||
|
Connectors publish versioned datasource origins. Datasources registers those
|
||||||
|
origins and presents one stable capability to Dataflow, Workflow, Reporting,
|
||||||
|
Risk Compliance, and other consumers. Consumers must not import connector
|
||||||
|
implementations or retain credentials.
|
||||||
|
|
||||||
|
The initial provider path is:
|
||||||
|
|
||||||
|
1. Connectors imports a bounded JSON/CSV snapshot and exposes it as an origin.
|
||||||
|
2. Datasources registers it as live or cached, or accepts a direct static upload
|
||||||
|
through staging.
|
||||||
|
3. Cached refreshes and static promotions append immutable materializations.
|
||||||
|
4. Any datasource may expose a frozen state for reproducible execution evidence.
|
||||||
|
5. Dataflow stores an opaque datasource reference, state policy, and expected
|
||||||
|
fingerprint.
|
||||||
|
6. A pinned Dataflow run may publish a complete bounded result as a new
|
||||||
|
immutable materialization through an idempotent Datasources capability.
|
||||||
|
|
||||||
|
Database, REST/HTTP, LDAP/directory, managed file, watched-directory, feed, and
|
||||||
|
stream providers fit behind the same origin contract. Provider-specific
|
||||||
|
configuration remains in Connectors.
|
||||||
|
|
||||||
|
## Shared Definition Graph
|
||||||
|
|
||||||
|
Core owns domain-neutral graph primitives:
|
||||||
|
|
||||||
|
- nodes, typed ports, edges, and configuration field descriptors;
|
||||||
|
- node libraries and category labels;
|
||||||
|
- graph size, connectivity, cycle, and node-count constraints;
|
||||||
|
- shared backend validation and frontend connection checks.
|
||||||
|
|
||||||
|
Domain modules own their semantics:
|
||||||
|
|
||||||
|
- Dataflow provides load, combine, filter, transform, and output nodes. Its
|
||||||
|
graph is acyclic and has one output.
|
||||||
|
- Workflow Engine provides trigger, activity, review, decision, wait,
|
||||||
|
module-action, Dataflow, and outcome semantics. It permits governed loops and
|
||||||
|
has exactly one trigger plus one or more outcomes. The optional Workflow
|
||||||
|
module supplies the editor over the same native graph/BPMN language.
|
||||||
|
|
||||||
|
This division permits a shared editor shell without making Workflow a special
|
||||||
|
kind of Dataflow or leaking either module into Core.
|
||||||
|
|
||||||
|
## Current Implementation
|
||||||
|
|
||||||
|
- Core graph and datasource contracts are versioned at `0.1.0`.
|
||||||
|
- Workflow Engine owns tenant-isolated definitions, immutable revisions,
|
||||||
|
activation pinning, module-contributed versioned baselines, runtime instances,
|
||||||
|
governed action/effect execution, retries, waits, and reconciliation. The
|
||||||
|
optional Workflow module exposes the reusable native BPMN graph editor.
|
||||||
|
- Datasources exposes catalogue, origins, staging, promotion, preview,
|
||||||
|
materialization history, refresh, freeze, retirement, and producer
|
||||||
|
publication APIs.
|
||||||
|
- Datasources WebUI exposes all current lifecycle views.
|
||||||
|
- Connectors adapts existing tabular snapshots to datasource origins.
|
||||||
|
- Dataflow consumes only Datasources catalogue/lifecycle capabilities and can
|
||||||
|
request current, live, or latest-frozen state.
|
||||||
|
- Dataflow exposes typed graph/IR, registry-driven validation/execution/SQL
|
||||||
|
compilation, expressions and reusable subflows, a pinned run-lifecycle
|
||||||
|
capability, production worker boundary, and Run/Publish surface. Runs record
|
||||||
|
lineage and intermediate artifacts and publish only complete bounded results.
|
||||||
|
- The focused composition check proves Connector origin -> Datasource ->
|
||||||
|
pinned Dataflow run -> frozen published materialization, including replay.
|
||||||
|
|
||||||
|
## Next Slices
|
||||||
|
|
||||||
|
1. Add the provider declaration and source-authority binding used consistently
|
||||||
|
by Connectors, Datasources, configuration packages, Ops, and Docs.
|
||||||
|
2. Add typed datasource owner/steward, legal/purpose, quality/freshness,
|
||||||
|
classification, correction, service/process, and downstream dependency
|
||||||
|
metadata under
|
||||||
|
[Datasources #6](https://git.add-ideas.de/GovOPlaN/govoplan-datasources/issues/6).
|
||||||
|
3. Add SQL database and governed REST origin providers with credential-envelope
|
||||||
|
references and bounded pushdown.
|
||||||
|
4. Add managed-file and directory origins.
|
||||||
|
5. Complete datasource quality rules, schema compatibility policy, retention, and
|
||||||
|
promotion approvals.
|
||||||
|
6. Complete scheduled/event/API/chained Dataflow trigger governance, reusable
|
||||||
|
template inheritance, Reporting publication, human reconciliation transforms,
|
||||||
|
and target resource/recovery evidence for large runs.
|
||||||
@@ -8,11 +8,11 @@ The same pattern is reusable outside GovOPlaN for any project where Codex works
|
|||||||
|
|
||||||
The repository contains Gitea issue templates in `.gitea/ISSUE_TEMPLATE`, a pull request template in `.gitea/PULL_REQUEST_TEMPLATE.md`, and the label taxonomy in `docs/gitea-labels.json`.
|
The repository contains Gitea issue templates in `.gitea/ISSUE_TEMPLATE`, a pull request template in `.gitea/PULL_REQUEST_TEMPLATE.md`, and the label taxonomy in `docs/gitea-labels.json`.
|
||||||
|
|
||||||
The scripts infer this repository from `origin` (`git@git.add-ideas.de:add-ideas/govoplan.git`). Override inference when needed:
|
The scripts infer this repository from `origin` (`git@git.add-ideas.de:GovOPlaN/govoplan.git`). Override inference when needed:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export GITEA_URL=https://git.add-ideas.de
|
export GITEA_URL=https://git.add-ideas.de
|
||||||
export GITEA_OWNER=add-ideas
|
export GITEA_OWNER=GovOPlaN
|
||||||
export GITEA_REPO=govoplan
|
export GITEA_REPO=govoplan
|
||||||
export GITEA_TOKEN=...
|
export GITEA_TOKEN=...
|
||||||
```
|
```
|
||||||
@@ -23,7 +23,7 @@ The API scripts also read `GITEA_*` values from the target repository's `.env` f
|
|||||||
GITEA_TOKEN=...
|
GITEA_TOKEN=...
|
||||||
# Optional if origin inference is not enough:
|
# Optional if origin inference is not enough:
|
||||||
GITEA_URL=https://git.add-ideas.de
|
GITEA_URL=https://git.add-ideas.de
|
||||||
GITEA_OWNER=add-ideas
|
GITEA_OWNER=GovOPlaN
|
||||||
GITEA_REPO=govoplan
|
GITEA_REPO=govoplan
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,555 @@
|
|||||||
|
# Installation And Deployment Architecture
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
A supported GovOPlaN installation starts with one downloaded, verified
|
||||||
|
bootstrap artifact. The administrator answers a bounded set of questions and
|
||||||
|
receives a working base system. Re-running the same tool repairs or
|
||||||
|
reconfigures that installation instead of creating unrelated state.
|
||||||
|
|
||||||
|
The canonical product journey remains
|
||||||
|
[System Administrator Lifecycle User Story](SYSTEM_ADMINISTRATOR_LIFECYCLE_USER_STORY.md).
|
||||||
|
This document defines the deployer boundary and the first executable slice.
|
||||||
|
|
||||||
|
## First Executable Slice
|
||||||
|
|
||||||
|
`tools/deployment/govoplan-deploy.py` is a standard-library-only deployment
|
||||||
|
compiler and reconciler. It can be tested without installing GovOPlaN itself.
|
||||||
|
|
||||||
|
It currently supports:
|
||||||
|
|
||||||
|
- evaluation and self-hosted profiles;
|
||||||
|
- managed or external PostgreSQL;
|
||||||
|
- managed, external, or evaluation-only disabled Redis;
|
||||||
|
- disabled mail, an external relay declaration, or an evaluation-only
|
||||||
|
GreenMail service;
|
||||||
|
- durable local file storage, managed single-node Garage S3, or external
|
||||||
|
S3-compatible storage;
|
||||||
|
- an explicit HAProxy service that load-balances configured WebUI and API
|
||||||
|
replicas without access to the Docker socket;
|
||||||
|
- declarative API, WebUI, and worker replica counts while keeping migrations
|
||||||
|
and the scheduler singleton;
|
||||||
|
- Core, base, or full initial module selections;
|
||||||
|
- deterministic Compose JSON accepted by Compose v2;
|
||||||
|
- generated secrets stored in a private `0600` file;
|
||||||
|
- service-specific environment allowlists so infrastructure containers do not
|
||||||
|
receive unrelated application credentials;
|
||||||
|
- plan, render, doctor, status, apply, Kubernetes export, operation history,
|
||||||
|
and bounded recovery commands;
|
||||||
|
- an installation lock, migration-before-start ordering, readiness polling,
|
||||||
|
and an applied-state receipt;
|
||||||
|
- a durable hash-chained deployment journal captured before runtime mutation;
|
||||||
|
- PostgreSQL advisory serialization for Core and module migrations;
|
||||||
|
- runtime initialization that waits for exact configured migration heads
|
||||||
|
without mutating schema;
|
||||||
|
- runtime node registration, heartbeats, drain state, and a fenced scheduler;
|
||||||
|
- idempotent reconfiguration that preserves generated secrets;
|
||||||
|
- a keyed environment fingerprint that detects private binding changes without
|
||||||
|
writing secret values to plans or receipts;
|
||||||
|
- host CPU, memory, disk, entropy, architecture, Docker daemon, Compose,
|
||||||
|
listen-port, and external endpoint preflight checks;
|
||||||
|
- service removal without implicit data-volume deletion.
|
||||||
|
|
||||||
|
Create a local evaluation bundle:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./.venv/bin/python tools/deployment/govoplan-deploy.py init \
|
||||||
|
--directory /tmp/govoplan-evaluation \
|
||||||
|
--profile evaluation \
|
||||||
|
--postgres managed \
|
||||||
|
--redis managed \
|
||||||
|
--storage garage \
|
||||||
|
--mail test-mail \
|
||||||
|
--api-replicas 2 \
|
||||||
|
--web-replicas 2 \
|
||||||
|
--worker-replicas 2 \
|
||||||
|
--module-set base
|
||||||
|
```
|
||||||
|
|
||||||
|
Inspect the generated intent and host requirements:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./.venv/bin/python tools/deployment/govoplan-deploy.py doctor \
|
||||||
|
--directory /tmp/govoplan-evaluation
|
||||||
|
```
|
||||||
|
|
||||||
|
Change a component without rotating existing generated secrets:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./.venv/bin/python tools/deployment/govoplan-deploy.py configure \
|
||||||
|
--directory /tmp/govoplan-evaluation \
|
||||||
|
--redis external \
|
||||||
|
--redis-url 'rediss://:password@redis.example.org:6379/0'
|
||||||
|
```
|
||||||
|
|
||||||
|
The private installation directory contains:
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `installation.json` | Versioned, non-secret desired state |
|
||||||
|
| `secrets.env` | Deployment-local secrets and external service bindings |
|
||||||
|
| `compose.json` | Deterministic generated Compose definition |
|
||||||
|
| `garage.toml` | Non-secret managed Garage server configuration |
|
||||||
|
| `load-balancer.cfg` | Non-secret HAProxy WebUI/API discovery configuration |
|
||||||
|
| `Caddyfile` | Non-secret managed-ingress route and ACME policy |
|
||||||
|
| `existing-proxy.json` | Exact upstream, trusted-source, header, and health contract for an operator-owned proxy |
|
||||||
|
| `plan.json` | Latest desired-state diff and readiness findings |
|
||||||
|
| `receipt.json` | Last successfully applied immutable identities |
|
||||||
|
| `distribution-manifest.json` | Canonical signed runtime/image selection adopted by the installer |
|
||||||
|
| `distribution-keyring.json` | Explicitly installed public trust anchor for runtime releases |
|
||||||
|
| `backup-evidence.json` | Signed provider-neutral coordinated backup and isolated-restore receipt |
|
||||||
|
| `backup-keyring.json` | Explicit public trust anchor for backup evidence authorities |
|
||||||
|
| `backup-verification.json` | Sanitized local verification/adoption receipt |
|
||||||
|
| `applied-state/` | Checksum-verified snapshot of the last healthy deployment bundle |
|
||||||
|
| `operations/<id>/` | Private hash-chained deployment progress and recovery evidence |
|
||||||
|
| `kubernetes.json` | Optional stateless multi-host Kubernetes export |
|
||||||
|
| `.deployment.lock` | Same-host operation exclusion |
|
||||||
|
|
||||||
|
The specification contract is
|
||||||
|
[`installation-spec.schema.json`](installation-spec.schema.json).
|
||||||
|
|
||||||
|
Build the same dependency-free tool as one downloadable artifact:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./.venv/bin/python tools/deployment/build-deployer-zipapp.py \
|
||||||
|
--output /tmp/govoplan-deploy.pyz
|
||||||
|
python /tmp/govoplan-deploy.pyz --help
|
||||||
|
```
|
||||||
|
|
||||||
|
To exercise reconciliation with locally available evaluation images:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python /tmp/govoplan-deploy.pyz init \
|
||||||
|
--non-interactive \
|
||||||
|
--directory /tmp/govoplan-evaluation \
|
||||||
|
--profile evaluation \
|
||||||
|
--api-image local/govoplan-api:test \
|
||||||
|
--web-image local/govoplan-web:test
|
||||||
|
python /tmp/govoplan-deploy.pyz apply \
|
||||||
|
--directory /tmp/govoplan-evaluation \
|
||||||
|
--allow-unverified-images \
|
||||||
|
--skip-pull
|
||||||
|
```
|
||||||
|
|
||||||
|
Those images must already contain the selected module set. The override exists
|
||||||
|
only to exercise local orchestration before release artifacts exist; it is
|
||||||
|
rejected for `self-hosted`.
|
||||||
|
|
||||||
|
## Runtime Distribution Boundary
|
||||||
|
|
||||||
|
The protected `Runtime Distribution` workflow builds GovOPlaN wheels first,
|
||||||
|
resolves architecture-specific third-party wheels into offline wheelhouses, and
|
||||||
|
then assembles the API images with `pip --no-index`. The target host never
|
||||||
|
clones Git repositories and neither runtime image performs network package
|
||||||
|
installation. Separate amd64/arm64 API and WebUI images are joined into OCI
|
||||||
|
indexes and run as non-root identities. The release assets include CycloneDX
|
||||||
|
application SBOMs, SLSA-style provenance, exact composition evidence, the
|
||||||
|
single-file deployer, its detached Ed25519 signature, and a signed, expiring
|
||||||
|
distribution manifest. Evidence generation and signing run through the
|
||||||
|
workflow's isolated release Python environment so their cryptographic tooling
|
||||||
|
is explicit and independent of packages preinstalled in the Actions runner.
|
||||||
|
The API image points Core at the migration scripts installed from the verified
|
||||||
|
wheel under `/opt/govoplan/runtime/govoplan_core_runtime`; migrations therefore
|
||||||
|
do not depend on a source checkout or the build host's Python installation
|
||||||
|
scheme.
|
||||||
|
Before publication, the exact amd64 and arm64 image manifests each run release
|
||||||
|
migrations against the pinned PostgreSQL image, reach API and WebUI readiness
|
||||||
|
as non-root/read-only processes, and complete a task through the pinned Redis
|
||||||
|
image and packaged worker. Sanitized per-platform smoke receipts are retained
|
||||||
|
as immutable release assets.
|
||||||
|
PostgreSQL and Redis indexes are resolved to untagged platform-child digests
|
||||||
|
before each smoke run. This keeps the evidence architecture-specific and
|
||||||
|
avoids retargeting one local Docker tag between incompatible platforms.
|
||||||
|
The CI host registers arm64 execution with an explicitly supplied,
|
||||||
|
digest-pinned `tonistiigi/binfmt` image immediately before the smoke. This
|
||||||
|
privileged helper is confined to the release runner and is never part of a
|
||||||
|
GovOPlaN target deployment or its runtime image set.
|
||||||
|
Because QEMU user-mode execution triggers Redis's arm64 host-kernel COW guard,
|
||||||
|
the arm64 smoke suppresses only `ARM64-COW-BUG` while persistence, snapshots,
|
||||||
|
and append-only files are disabled. Target Redis services never inherit this
|
||||||
|
test-only option.
|
||||||
|
The smoke also proves a bounded post-migration table contract and aborts as
|
||||||
|
soon as a required container exits, rather than allowing a dead process to
|
||||||
|
consume the full readiness timeout.
|
||||||
|
|
||||||
|
Ingress acceptance streams generated configuration into Docker-managed
|
||||||
|
volumes before starting the read-only containers. It therefore also works when
|
||||||
|
an Actions job reaches a host or remote Docker daemon through a mounted socket;
|
||||||
|
the drill never assumes that a job-container path is visible to that daemon.
|
||||||
|
The drill allocates explicit loopback-only host ports and verifies Docker's
|
||||||
|
host binding configuration, avoiding daemon-specific random-port shorthand
|
||||||
|
behavior. Because an Actions job and deployment containers may be Docker
|
||||||
|
siblings, functional HTTP/TLS checks run from the digest-pinned API image on
|
||||||
|
the deployment network instead of assuming the Docker host is job-local.
|
||||||
|
The dispatch-only `Runtime Ingress Drill` workflow exposes the same bounded
|
||||||
|
check independently so ingress changes can be diagnosed before an immutable
|
||||||
|
runtime publication; it accepts only digest-pinned Caddy, HAProxy, and API
|
||||||
|
images and has no push trigger.
|
||||||
|
The official Caddy binary carries the `NET_BIND_SERVICE` file capability. The
|
||||||
|
managed-ingress container therefore drops every capability and adds back only
|
||||||
|
`NET_BIND_SERVICE`; otherwise Linux rejects the binary at `execve` before its
|
||||||
|
high-port configuration can start. `no-new-privileges`, a read-only root
|
||||||
|
filesystem, and non-privileged container ports remain enforced.
|
||||||
|
The bounded setup helper writes only generated public configuration as root so
|
||||||
|
it can initialize a new volume; the actual HAProxy process retains the image's
|
||||||
|
non-root identity and runs read-only with all capabilities dropped.
|
||||||
|
|
||||||
|
The manifest contract is
|
||||||
|
[`runtime-distribution-manifest.schema.json`](runtime-distribution-manifest.schema.json),
|
||||||
|
and its separately distributed trust-anchor contract is
|
||||||
|
[`runtime-distribution-keyring.schema.json`](runtime-distribution-keyring.schema.json).
|
||||||
|
Publication is immutable: an existing Gitea release asset must have the same
|
||||||
|
size and SHA-256 digest or publication fails.
|
||||||
|
|
||||||
|
Adopt a downloaded or prefetched release only after obtaining the manifest
|
||||||
|
digest and trusted keyring through the documented independent channel:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 govoplan-deploy.pyz verify-release \
|
||||||
|
--directory /srv/govoplan/installation \
|
||||||
|
--manifest ./distribution-manifest.json \
|
||||||
|
--manifest-sha256 "$(cut -d' ' -f1 distribution-manifest.json.sha256)" \
|
||||||
|
--trusted-keyring ./distribution-keyring.json \
|
||||||
|
--adopt
|
||||||
|
```
|
||||||
|
|
||||||
|
`doctor` and `apply` rehash both stored files, re-run OpenSSL Ed25519
|
||||||
|
verification, enforce channel/expiry/revocation, compare every selected image,
|
||||||
|
and prove that all enabled module ids occur in the signed image composition.
|
||||||
|
An offline image index can bind prefetched OCI archives to the same exact image
|
||||||
|
references and archive hashes; mutable tags or incomplete bundles are rejected.
|
||||||
|
|
||||||
|
## Current Production Gates
|
||||||
|
|
||||||
|
The first immutable production-distribution baseline is published as
|
||||||
|
[`v0.1.14`](https://git.add-ideas.de/GovOPlaN/govoplan/releases/tag/v0.1.14)
|
||||||
|
from source commit `1f039dd39c1ce2672f4978c8abc6dff862ef1445`. Runtime
|
||||||
|
Distribution [run #459](https://git.add-ideas.de/GovOPlaN/govoplan/actions/runs/459)
|
||||||
|
proved migrations, schema compatibility, non-root API/Web readiness, and worker
|
||||||
|
delivery/shutdown on both `linux/amd64` and `linux/arm64`. Its signed manifest
|
||||||
|
has SHA-256
|
||||||
|
`d703267e01855dee63200cb20921c91c3f95fbff550c8ca76e9a35cba3f69109`
|
||||||
|
and pins these runtime indexes:
|
||||||
|
|
||||||
|
- API: `git.add-ideas.de/govoplan/runtime-api@sha256:197ed01790986f2bc927eaa5d8348fa118702e5d2dc05feb851fc2643c23764a`
|
||||||
|
- WebUI: `git.add-ideas.de/govoplan/runtime-web@sha256:e936cca124f1fad29a067834cf17627d4c236410fdc3fa129e0ccb26b8193812`
|
||||||
|
|
||||||
|
The signed bootstrap has SHA-256
|
||||||
|
`1ff946fba82b0895d153b23352d06e30fe18388450dfd37fed6fb9912310efc5`
|
||||||
|
and key id `runtime-distribution-2026-01`. The managed-ingress boundary passed
|
||||||
|
the same publication run and the independently dispatchable Runtime Ingress
|
||||||
|
Drill [run #458](https://git.add-ideas.de/GovOPlaN/govoplan/actions/runs/458).
|
||||||
|
Every later release must renew this evidence; the following target-specific
|
||||||
|
gates remain:
|
||||||
|
|
||||||
|
1. **First administrator.** Production needs a one-time, restricted enrollment
|
||||||
|
identity. The development bootstrap must not be enabled in production.
|
||||||
|
2. **Image/module composition.** The deployer enforces the signed
|
||||||
|
composition. A selected module not shipped by that release cannot be
|
||||||
|
enabled.
|
||||||
|
3. **Deployment agent.** Web updates need a separate privileged reconciler with
|
||||||
|
a typed command allowlist. The API and browser must never receive the Docker
|
||||||
|
socket or arbitrary shell access.
|
||||||
|
4. **Target reachability evidence.** Managed Caddy ingress and the
|
||||||
|
existing-proxy contract are implemented. A production claim still requires
|
||||||
|
running `doctor` from the target host after public DNS/firewall changes and
|
||||||
|
retaining public TLS/readiness evidence for that deployment.
|
||||||
|
|
||||||
|
`apply --allow-unverified-images` is therefore restricted to the evaluation
|
||||||
|
profile. It explicitly acknowledges both mutable image identities and
|
||||||
|
unverified image/module composition. It is a local test escape hatch, not a
|
||||||
|
production setting.
|
||||||
|
|
||||||
|
## Component Choices
|
||||||
|
|
||||||
|
### PostgreSQL
|
||||||
|
|
||||||
|
`managed` creates a persistent PostgreSQL container and private generated
|
||||||
|
credentials. `external` requires an explicit `DATABASE_URL`; switching from
|
||||||
|
managed to external cannot reuse the old `postgres` Docker hostname
|
||||||
|
accidentally.
|
||||||
|
|
||||||
|
Interactive entry hides external URLs because they commonly contain
|
||||||
|
credentials. For unattended automation, provide them through a protected
|
||||||
|
operator mechanism and avoid storing secret-bearing flags in shell history.
|
||||||
|
|
||||||
|
Production policy should support external managed databases and local managed
|
||||||
|
PostgreSQL equally at the application boundary. Backup, point-in-time recovery,
|
||||||
|
high availability, and major-version upgrades remain deployment properties.
|
||||||
|
|
||||||
|
### Redis
|
||||||
|
|
||||||
|
`managed` creates an authenticated, append-only Redis container. `external`
|
||||||
|
requires an explicit `REDIS_URL`. `disabled` is evaluation-only and disables
|
||||||
|
workers while recording the single-process login-throttle risk acknowledgement.
|
||||||
|
|
||||||
|
`doctor` performs a bounded TCP connection check for external PostgreSQL,
|
||||||
|
Redis, and S3 endpoints. This verifies DNS, routing, and that the port accepts a
|
||||||
|
connection; it is not an authentication or semantic health check.
|
||||||
|
|
||||||
|
Production base installations include Redis because durable queues, distributed
|
||||||
|
throttling, notifications, scheduled work, and transactional event delivery
|
||||||
|
must survive API restarts.
|
||||||
|
|
||||||
|
### Mail
|
||||||
|
|
||||||
|
The first slice distinguishes:
|
||||||
|
|
||||||
|
- `disabled`;
|
||||||
|
- `external-relay`, which records the infrastructure decision but leaves Mail
|
||||||
|
server/credential creation as a visible post-install task;
|
||||||
|
- `test-mail`, an evaluation-only GreenMail service.
|
||||||
|
|
||||||
|
A bundled production mail server is intentionally not a default. Operating one
|
||||||
|
requires DNS, reverse DNS, TLS, DKIM, SPF, DMARC, reputation, abuse handling,
|
||||||
|
queue monitoring, and upgrade policy. A later profile may support an
|
||||||
|
operator-selected MTA/relay, but it must expose these requirements rather than
|
||||||
|
presenting a container as a complete mail service.
|
||||||
|
|
||||||
|
### File Storage
|
||||||
|
|
||||||
|
`local` uses a durable Compose volume and is appropriate for one-host
|
||||||
|
installations. `garage` provisions Garage 2.3 in its supported single-node
|
||||||
|
bootstrap mode, generates a private application key and bucket, and connects
|
||||||
|
the Files S3 backend to the exact installer-owned internal endpoint. The
|
||||||
|
managed trust marker cannot authorize another S3 host.
|
||||||
|
Garage metadata and object data use separate persistent volumes. `s3` requires
|
||||||
|
an external endpoint, region, access key, secret key, and bucket values.
|
||||||
|
Self-hosted external S3 endpoints must be clean HTTPS origins. The generated
|
||||||
|
runtime explicitly sets `FILE_STORAGE_S3_ENDPOINT_TRUSTED=true` for that
|
||||||
|
operator-selected endpoint. The trust flag is not accepted for local storage
|
||||||
|
and cannot be combined with installer-managed Garage trust.
|
||||||
|
|
||||||
|
Local storage must be included in backup and restore drills. Horizontal API or
|
||||||
|
worker scale-out requires shared/object storage. The managed Garage profile is
|
||||||
|
persistent but has no data redundancy; availability-sensitive installations
|
||||||
|
must use a tested multi-node Garage cluster or another external S3 service.
|
||||||
|
|
||||||
|
### Load Balancing And Replicas
|
||||||
|
|
||||||
|
The generated Compose topology publishes only `load-balancer` for local or
|
||||||
|
existing-proxy profiles. With managed ingress, only Caddy publishes host ports
|
||||||
|
and HAProxy remains private. HAProxy uses
|
||||||
|
Docker DNS service discovery to distribute public traffic across WebUI replicas
|
||||||
|
and WebUI API proxy traffic across API replicas. The WebUI and API services do
|
||||||
|
not publish host ports. HAProxy has no Docker socket and discovers only the
|
||||||
|
bounded replica slots rendered into `load-balancer.cfg`.
|
||||||
|
|
||||||
|
Replica counts are desired state:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./.venv/bin/python tools/deployment/govoplan-deploy.py configure \
|
||||||
|
--directory /tmp/govoplan-evaluation \
|
||||||
|
--api-replicas 3 \
|
||||||
|
--web-replicas 2 \
|
||||||
|
--worker-replicas 4
|
||||||
|
./.venv/bin/python tools/deployment/govoplan-deploy.py apply \
|
||||||
|
--directory /tmp/govoplan-evaluation
|
||||||
|
```
|
||||||
|
|
||||||
|
Workers are queue consumers, so they are scaled through Redis rather than put
|
||||||
|
behind an HTTP load balancer. Migrations are serialized with a deployment-wide
|
||||||
|
PostgreSQL advisory lock. The Celery scheduler is run under a renewable,
|
||||||
|
fencing-token lease. Multiple API replicas are rejected when Redis is disabled
|
||||||
|
because distributed throttling and queued work cannot then be shared correctly.
|
||||||
|
|
||||||
|
### Public Ingress And TLS
|
||||||
|
|
||||||
|
A self-hosted installation is fail-closed until one of these boundaries is
|
||||||
|
selected:
|
||||||
|
|
||||||
|
- `existing-proxy` publishes HAProxy at `listen.address:listen.port` and emits
|
||||||
|
`existing-proxy.json`. The operator-owned proxy must use the recorded host,
|
||||||
|
upstream, and health paths. Only the exact CIDRs listed with repeated
|
||||||
|
`--trusted-proxy-cidr` values may supply `X-Forwarded-*` headers. Public
|
||||||
|
proxy addresses must be `/32` or `/128`; private ranges are limited to `/24`
|
||||||
|
or narrower for IPv4 and `/64` or narrower for IPv6.
|
||||||
|
- `managed` publishes Caddy on the selected HTTP/HTTPS ports, redirects HTTP to
|
||||||
|
HTTPS, obtains and renews certificates through ACME, and keeps certificate
|
||||||
|
material exclusively in the private `caddy-data` and `caddy-config` volumes.
|
||||||
|
The application containers receive no ACME account or TLS private keys.
|
||||||
|
|
||||||
|
Example existing-proxy configuration:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python govoplan-deploy.py configure \
|
||||||
|
--directory /srv/govoplan \
|
||||||
|
--ingress existing-proxy \
|
||||||
|
--trusted-proxy-cidr 172.20.0.7/32
|
||||||
|
```
|
||||||
|
|
||||||
|
Example managed configuration:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python govoplan-deploy.py configure \
|
||||||
|
--directory /srv/govoplan \
|
||||||
|
--ingress managed \
|
||||||
|
--acme-email operator@example.org
|
||||||
|
```
|
||||||
|
|
||||||
|
Before managed ingress starts, public A/AAAA records must resolve to the target
|
||||||
|
and inbound TCP 80/443 must reach it. Existing-proxy mode additionally requires
|
||||||
|
the public proxy and valid certificate to be reachable before apply. After a
|
||||||
|
successful receipt, `doctor` reports DNS resolution, certificate validity and
|
||||||
|
remaining lifetime, public `/health/ready`, and the private HAProxy/WebUI path
|
||||||
|
as separate checks. Reconfiguration retains the certificate volumes; bundle
|
||||||
|
rollback never deletes or exposes their contents. Include both Caddy volumes
|
||||||
|
in coordinated backup and restore evidence.
|
||||||
|
|
||||||
|
This is same-host scaling. Docker Compose uses a bridge network and does not
|
||||||
|
place containers on another machine. See
|
||||||
|
[Scaling And Multi-Host Deployment](SCALING_AND_MULTI_HOST_DEPLOYMENT.md) for
|
||||||
|
the supported topology and promotion path.
|
||||||
|
|
||||||
|
## Reconfiguration Semantics
|
||||||
|
|
||||||
|
`installation.json` is desired state. `receipt.json` is the last successfully
|
||||||
|
applied state. `plan` compares their canonical hashes and service sets.
|
||||||
|
|
||||||
|
- Adding a managed component creates its service and persistent volume.
|
||||||
|
- Removing a component removes its service container on apply.
|
||||||
|
- Volumes are retained by default; deleting data requires a separate,
|
||||||
|
deliberately destructive workflow.
|
||||||
|
- Existing generated credentials are retained unless an explicit future rotate
|
||||||
|
operation is requested.
|
||||||
|
- Private configuration changes are represented by a keyed fingerprint in the
|
||||||
|
plan and receipt; plaintext values are never copied there.
|
||||||
|
- Managed-to-external transitions require the new endpoint in the same
|
||||||
|
operation.
|
||||||
|
- Migrations run as a one-shot service before API/worker replacement.
|
||||||
|
- API, worker, and scheduler start commands wait for exact configured migration
|
||||||
|
heads; only the migration command is permitted to change schema.
|
||||||
|
- API and worker replicas register their software/module composition and
|
||||||
|
heartbeat in PostgreSQL. Ops can request and cancel a node drain.
|
||||||
|
- The first upgrade from a direct WebUI host port stops that legacy WebUI
|
||||||
|
container immediately before HAProxy claims the same endpoint.
|
||||||
|
- Health must recover before a new receipt and applied-state snapshot are
|
||||||
|
committed.
|
||||||
|
|
||||||
|
Every apply operation is journalled before image pulls or runtime mutation. A
|
||||||
|
failure before migration may restore a verified previous bundle. Once migration
|
||||||
|
starts, recovery is forward-only unless an independently verified database
|
||||||
|
backup is restored. See
|
||||||
|
[Recovery And Rollback Guarantees](RECOVERY_AND_ROLLBACK_GUARANTEES.md).
|
||||||
|
|
||||||
|
Production updates still need operator/provider-created coordinated backup and
|
||||||
|
restore evidence, a database compatibility declaration, and a
|
||||||
|
deployment-specific drain policy. The deployer now verifies and enforces the
|
||||||
|
signed evidence before migration, but does not manufacture backups or receive
|
||||||
|
provider administration credentials. See
|
||||||
|
[Backup And Restore Evidence](BACKUP_AND_RESTORE_EVIDENCE.md).
|
||||||
|
|
||||||
|
## Stateless Kubernetes Runtime
|
||||||
|
|
||||||
|
`render-kubernetes` exports the application tier for a standard orchestrator.
|
||||||
|
It requires external PostgreSQL, Redis, and S3 and emits no stateful service or
|
||||||
|
secret value:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python tools/deployment/govoplan-deploy.py render-kubernetes \
|
||||||
|
--directory /srv/govoplan/default \
|
||||||
|
--namespace govoplan \
|
||||||
|
--secret-name govoplan-runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
The output includes a release-specific migration Job, database-head wait init
|
||||||
|
containers, API readiness/liveness probes, rolling Deployments, Services, Pod
|
||||||
|
disruption budgets, a tokenless ServiceAccount, and one fenced scheduler. Apply
|
||||||
|
the named Secret through the cluster's secret manager and review ingress proxy
|
||||||
|
CIDRs before deployment. A release-changing export requires adopted backup
|
||||||
|
evidence and carries only its sanitized digest and identifiers as Job
|
||||||
|
annotations. Detailed rollout and scaling rules live in
|
||||||
|
[Scaling And Multi-Host Deployment](SCALING_AND_MULTI_HOST_DEPLOYMENT.md).
|
||||||
|
|
||||||
|
## Recovery Commands
|
||||||
|
|
||||||
|
List durable deployment operations:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python tools/deployment/govoplan-deploy.py operations \
|
||||||
|
--directory /srv/govoplan/default
|
||||||
|
```
|
||||||
|
|
||||||
|
Recover a selected failed operation after reviewing its stage evidence:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python tools/deployment/govoplan-deploy.py recover \
|
||||||
|
--directory /srv/govoplan/default \
|
||||||
|
--operation-id <operation-id>
|
||||||
|
```
|
||||||
|
|
||||||
|
The command reports whether it restored the pre-migration applied bundle,
|
||||||
|
requires forward recovery, or needs manual intervention. Add `--apply` only
|
||||||
|
after that decision has been reviewed.
|
||||||
|
|
||||||
|
## Web Update Boundary
|
||||||
|
|
||||||
|
The intended update path is:
|
||||||
|
|
||||||
|
1. Ops reads the non-secret installation receipt and reports management mode,
|
||||||
|
current release, component health, and update availability.
|
||||||
|
2. An authorized administrator asks Core to create a typed deployment request,
|
||||||
|
for example `reconcile_release` or `rollback_release`.
|
||||||
|
3. Core persists the reviewed immutable plan, actor, expected current receipt,
|
||||||
|
and idempotency key.
|
||||||
|
4. A separately deployed, narrow deployment agent claims the request.
|
||||||
|
5. The agent verifies signatures/digests, acquires a fenced deployment lock,
|
||||||
|
backs up, pulls, migrates, reconciles, probes health, and writes evidence.
|
||||||
|
6. Ops presents durable progress and the resulting receipt.
|
||||||
|
|
||||||
|
The agent owns container-runtime access. It accepts no command strings from the
|
||||||
|
browser and has no domain-data permissions. Installations managed by Kubernetes,
|
||||||
|
systemd, or another external orchestrator expose read-only status and an export
|
||||||
|
of the reviewed update recipe instead of a non-functional update button.
|
||||||
|
|
||||||
|
## Distribution Workflow
|
||||||
|
|
||||||
|
The downloadable entry point is a reproducible release asset: sorted source
|
||||||
|
paths, fixed ZIP metadata, fixed compression settings, and identical source
|
||||||
|
bytes produce an identical zipapp regardless of checkout timestamps. Obtain the
|
||||||
|
zipapp, detached signature, checksum, and trusted public keyring through
|
||||||
|
independently authenticated paths before execution:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl --proto '=https' --tlsv1.2 --fail --location \
|
||||||
|
https://git.add-ideas.de/GovOPlaN/govoplan/releases/download/vX.Y.Z/govoplan-deploy.pyz \
|
||||||
|
--output govoplan-deploy.pyz
|
||||||
|
sha256sum --check govoplan-deploy.pyz.sha256
|
||||||
|
python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
keyring = json.loads(Path("distribution-keyring.json").read_text())
|
||||||
|
active = [key for key in keyring["keys"] if key["status"] == "active"]
|
||||||
|
if len(active) != 1:
|
||||||
|
raise SystemExit("expected exactly one active runtime release key")
|
||||||
|
Path("runtime-release-public.pem").write_text(active[0]["public_key_pem"])
|
||||||
|
PY
|
||||||
|
openssl pkeyutl -verify -pubin -inkey runtime-release-public.pem -rawin \
|
||||||
|
-in govoplan-deploy.pyz -sigfile govoplan-deploy.pyz.sig
|
||||||
|
python3 govoplan-deploy.pyz init
|
||||||
|
```
|
||||||
|
|
||||||
|
The zipapp has no GovOPlaN package dependency. It accepts a bounded HTTPS
|
||||||
|
manifest or a prefetched file, requires an independently supplied SHA-256
|
||||||
|
digest and explicit trusted keyring, and executes OpenSSL with a fixed argument
|
||||||
|
vector for Ed25519 verification. It never evaluates downloaded shell text or
|
||||||
|
accepts an arbitrary command string.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Run the focused tests:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./.venv/bin/python -m unittest -v tests.test_deployment_installer
|
||||||
|
```
|
||||||
|
|
||||||
|
The tests cover signed release adoption, tamper/expiry/revocation/unknown-key
|
||||||
|
rejection, architecture composition, offline image integrity, profile
|
||||||
|
restrictions, secret persistence, external endpoint
|
||||||
|
requirements, managed Garage bootstrap, S3 policy, replica validation, HAProxy
|
||||||
|
discovery configuration, Compose service selection, secret non-disclosure,
|
||||||
|
service-specific environment isolation, private file modes, external endpoint
|
||||||
|
preflight, first-plan generation, apply ordering, receipt idempotency,
|
||||||
|
hash-chained recovery journals, migration recovery boundaries, and stateless
|
||||||
|
Kubernetes rendering.
|
||||||
@@ -0,0 +1,579 @@
|
|||||||
|
# Institutional Governance Target Architecture
|
||||||
|
|
||||||
|
## Status and sources
|
||||||
|
|
||||||
|
This document is the accepted architectural reconciliation of two product
|
||||||
|
concepts prepared outside the repositories:
|
||||||
|
|
||||||
|
- `govoplan_concept_dev.md`
|
||||||
|
- `software_big_picture.md`
|
||||||
|
|
||||||
|
The source concepts describe GovOPlaN as an operational governance platform for
|
||||||
|
public institutions. This document merges that direction with the implemented
|
||||||
|
platform state as of 2026-08-01. It is the canonical repository version of the
|
||||||
|
direction. Gitea issues remain the source of truth for delivery state.
|
||||||
|
|
||||||
|
Read this together with:
|
||||||
|
|
||||||
|
- [Connected Governance Platform Roadmap](CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md)
|
||||||
|
- [Reference Journey Program](REFERENCE_JOURNEY_PROGRAM.md)
|
||||||
|
- [Module Contracts and Install Boundaries](MODULE_CONTRACTS_AND_INSTALLS.md)
|
||||||
|
- [Datasource and Definition Graph Architecture](DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md)
|
||||||
|
- [Capability and Infrastructure Fit](CAPABILITY_AND_INFRASTRUCTURE_FIT.md)
|
||||||
|
- [Core Module Architecture](../../govoplan-core/docs/MODULE_ARCHITECTURE.md)
|
||||||
|
- [Core External References and Integration Maturity](../../govoplan-core/docs/EXTERNAL_REFERENCES_AND_INTEGRATION_MATURITY.md)
|
||||||
|
- [Core Action, Effect, and Automation Layer](../../govoplan-core/docs/ACTION_EFFECT_AUTOMATION_LAYER.md)
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
GovOPlaN is a configurable **institutional governance and operations layer** for
|
||||||
|
public institutions. It should model the institution, coordinate its work,
|
||||||
|
connect its specialist systems, and preserve why and under whose authority an
|
||||||
|
action occurred.
|
||||||
|
|
||||||
|
GovOPlaN is not intended to become one universal ERP, DMS, groupware suite,
|
||||||
|
workflow editor, or specialist procedure. It should own the governance concepts
|
||||||
|
that must remain understandable across those systems and support native,
|
||||||
|
external, mirrored, synchronized, overlay, and link-only operation explicitly.
|
||||||
|
|
||||||
|
This changes product emphasis, not the modular architecture:
|
||||||
|
|
||||||
|
1. The current kernel and optional-module model remains.
|
||||||
|
2. Existing domain owners keep their data and behavior.
|
||||||
|
3. Cross-module semantics become explicit, versioned contracts.
|
||||||
|
4. Successful compositions become product and sector packages, not forks or
|
||||||
|
monolithic replacement applications.
|
||||||
|
5. Repository creation follows a proof threshold; a noun in the information
|
||||||
|
model does not automatically require a module.
|
||||||
|
|
||||||
|
## What recent work already supersedes
|
||||||
|
|
||||||
|
The source concepts predate several implemented foundations. These items are
|
||||||
|
accepted as the current baseline and must not be reopened as greenfield work.
|
||||||
|
|
||||||
|
| Concept requirement | Reconciled current state |
|
||||||
|
| --- | --- |
|
||||||
|
| Slim kernel plus installable modules | Implemented through entry-point discovery, `ModuleManifest`, migrations, capabilities, interfaces, WebUI contributions, and permutation checks. |
|
||||||
|
| Versioned cross-module contracts | Implemented through named interface ranges, capability protocols, static workspace graph checks, activation validation, and release checks. |
|
||||||
|
| Separate headless workflow runtime and editor | Implemented as `govoplan-workflow-engine` and optional `govoplan-workflow`. Module-owned workflow baselines are versioned and reconciled without replacing local overrides. |
|
||||||
|
| Provider-neutral external references | Implemented in Core with stable external identity and cumulative integration maturity from discovery through replacement. |
|
||||||
|
| Governed asynchronous effects | Implemented foundations include the action/effect contract, transactional platform event outbox, module outboxes, idempotency, outcome-unknown states, reconciliation, and worker health. Coverage still varies by provider. |
|
||||||
|
| Acting identity, function assignment, mandate, and ownership recovery | Implemented foundations span Identity, Organizations, IDM, Access, Mandates, generic ownership transfer/recovery, and audit provenance. Effective competence now resolves through a tenant-bound Mandate capability. |
|
||||||
|
| Governed data foundations | Connectors, Datasources, Dataflow, Reporting, and Search now exist. Datasources already provides live/cached/static modes, staging, immutable materializations, and publication contracts. |
|
||||||
|
| Task-focused projections and configured documentation | Views, view-surface declarations, configurable dashboards, and manifest-driven user/admin documentation exist. Rollout and content depth remain incremental. |
|
||||||
|
| Encryption as an optional capability | Core defines provider-neutral contracts; `govoplan-identity-trust` persists public device keys, key epochs and assurance evidence; and `govoplan-encryption` persists opaque vault/key lifecycle, versioned protection envelopes, quorum recovery authorization, outcome-unknown reconciliation, and disable preflight. A bundled local AES-256-GCM server-envelope provider now stores wrapped key material and supports Files/Postbox protection, rotation, revocation, destruction, rewrap, tamper detection, and fail-closed restore behavior. It is explicitly neither E2EE nor a certified KMS/HSM. |
|
||||||
|
| Search without mandatory OpenSearch | PostgreSQL-backed, permission-aware search and module provider contracts exist; OpenSearch remains an optional adapter. |
|
||||||
|
| Scale-out and recovery architecture | Stateless API/worker, shared database/object storage, event delivery, deployment, and recovery contracts are documented and partly exercised. Production profiles and drills remain active work. |
|
||||||
|
|
||||||
|
The institutional semantics, provider declaration gate, and first product
|
||||||
|
compositions described here are now implemented. Subsequent work is
|
||||||
|
**product depth and stronger maturity evidence**, not another runtime rewrite
|
||||||
|
or an unimplemented architecture boundary.
|
||||||
|
|
||||||
|
## Implementation status (2026-08-01)
|
||||||
|
|
||||||
|
The architecture contract is implemented as a bounded, executable vertical
|
||||||
|
slice. The portfolio declarations and provider governance gates apply to the
|
||||||
|
whole workspace, while the four semantic domains whose repository thresholds
|
||||||
|
were proven now have independent persistent owners:
|
||||||
|
|
||||||
|
| Area | Implemented state | Remaining rollout |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Module portfolio metadata | Core validates versioned architecture layer/kind, maturity evidence, known limits, ownership boundaries, authority modes, reference packages, target-tested providers, and migration/upgrade/recovery/security/operations documentation. | Complete for all 62 source manifests. Focused and release checks enforce `--require-architecture`; a new module cannot enter the workspace without truthful declaration and repository-local evidence. |
|
||||||
|
| External providers | Core validates provider objects/field groups, operations, integration maturity, source authority, bounded reads, freshness/health, idempotency, conflicts, outcome-unknown handling, evidence, correction, reconciliation, outage, classification, purpose, retention, and secret handling. Addresses/CardDAV, Files remote storage, Mail SMTP/IMAP, Calendar CalDAV/ICS/Graph/EWS, and Connectors tabular/sanctions providers declare the contract and tenant-bounded secret-free runtime state. | Registry validation rejects any declared external provider without a sanitized state provider. Future adapters must cross the same gate before activation. |
|
||||||
|
| Institutional context | Core provides versioned temporal, actor/representation, institution/unit/function/task/mandate/jurisdiction/service/case/party/work-item/workflow/approval/decision/record, legal-basis, evidence, information-governance, external-source, presentation, and geographic references. Events, automation actions, audit records, and the transactional Audit outbox preserve the envelope. | Owning modules must progressively require the relevant subset for consequential operations. |
|
||||||
|
| Semantic provider contracts | Provider-neutral DTOs and protocols cover Mandate resolution, versioned Service definitions, procedure Parties/representation, and formal Decisions. `govoplan-mandates`, `govoplan-services`, `govoplan-parties`, and `govoplan-decisions` now persist immutable revisions behind those contracts with tenant isolation, bounded reads, replay safety, OCC, migrations, uninstall guards, permissions, APIs, capability documentation, and recovery documentation. | The owners are deliberately headless. Procedure-specific UI remains with consuming modules. |
|
||||||
|
| Formal-outcome proof | Committee persists bodies, meetings, agenda items, minutes, lifecycle events, and an optional protected local Decision projection. Voting separately owns immutable ballot definitions, frozen electorates, recorded casting/replacement, deterministic tally, certification, challenge, annulment, and provider-backed assurance profiles. Committee consumes `voting.ballots` and retains only deliberation linkage and verified aggregate outcome. A bundled `local_confidential` reference provider encrypts server-readable casts outside native ballot rows and exposes only receipts plus aggregate evidence to Committee. | Native recorded ballots remain reconstructable, and the reference confidential provider is neither secret nor certified: its server can decrypt casts while tallying. Secret/electronic-ballot protocol selection, custody, legal acceptance, independent review, and target evidence remain explicit product decisions. |
|
||||||
|
| Service-to-case proof | Services owns the persistent exact definitions consumed by Portal discovery and Cases intake. Forms owns immutable multi-page/conditional/localized schemas, accessibility assessment and package fragments. Forms Runtime owns definition-aware drafts, validation, submission receipts, status/evidence history, and durable native Case/Workflow handoffs with intent-before-effect and outcome-unknown reconciliation. Portal delegates URL, Case, Form, or Workflow launch to an installed owner while retaining exact Service/Form provenance. | Anonymous intake and concrete attachment/signature providers remain product depth. Portal and Runtime fail closed and explain any absent launcher, target capability, or provider prerequisite. |
|
||||||
|
| Generic approvals and process execution | Approvals persists exact-subject chains, delegation, separation of duties, quorum, signatures-as-evidence, escalation, OCC, and replay-safe decisions. Campaign proves an exact-version delivery gate. Workflow Engine owns immutable definitions/instances plus API, schedule, event and parent triggers, durable timer/event waits, scale-out claims, current-authority rechecks, and idempotent starts independently of the optional editor. | Policy-authored Approval template selection, concrete signature providers, cron adapters, and broader BPMN execution profiles are product/provider depth on explicit contracts. |
|
||||||
|
| Device trust and content protection | Identity Trust separates public device keys, epochs, assurance and key-access decisions from login and Access. Encryption separates resource ownership from opaque provider key custody, versioned envelopes, migration evidence, quorum recovery authorization and uninstall proof. Its local server-envelope provider and Files/Postbox adapters prove ciphertext persistence, integrity, rotation/rewrap and fail-closed key loss without leaking plaintext keys across the capability boundary. | Reviewed KMS/HSM/client providers, more owner adapters, target backup/restore/key-loss drills, and E2EE interoperability/certification remain required before stronger deployment claims. |
|
||||||
|
| Procedure-party proof | Parties persists effective procedure roles, frozen contact snapshots, and representation powers. Existing powers cannot disappear or be silently rewritten; explicit OCC-guarded revocation is required. Cases resolves the provider capability and excludes expired/revoked authority from downstream delivery. | Procedure modules still decide which contextual fields and actions to present. |
|
||||||
|
| Integrated institutional journey | The executable `product.service-to-decision` fixture uses real SQL-backed Services, Cases, Parties, Mandates, Committee, and Decisions providers. It carries one exact Service version through persisted Case intake, representation and frozen delivery authority, effective Mandate resolution, a body/meeting/agendum/vote/minute sequence, a persisted formal Decision, confirmed Postbox effect, Audit/record evidence, remedy/review, and protected reconstruction. A second executable path proves Portal to exact Form revision, persisted submission, and idempotent replay. | This is architecture and composition evidence. Signed, release-bound target accessibility, privacy, security, operator, delivery-provider, and recovery-drill evidence is still required before the package may claim `reference_ready`. |
|
||||||
|
| Governed data catalogue | Datasources stores typed governance metadata, exposes bounded tenant-scoped filters and update APIs/UI, carries governance through staging, and snapshots it into immutable materializations. Reporting now persists immutable dataset, semantic-model, report, quality-plan, saved-view, and schedule revisions; executes typed semantic queries with quality gates, access checks, replay, pivoting, export/import assessment, and provenance; and exposes the governed analytical WebUI. | Rich dependency/impact traversal, additional expression functions, and policy-specific field visibility can grow on the established contracts without moving connector, transformation, or source ownership. |
|
||||||
|
| Portfolio and change governance | Projects now persists tenant-safe, immutable portfolio/project/milestone revisions with OCC, replay, lifecycle rules, restricted memberships, Search ACL indexing, outcomes, benefits, dependencies, capacity assumptions, change impact, and institutional references. Its WebUI exposes the planning catalogue and core planning fields. | Advanced planning structures already accepted by the API can receive deeper specialized editors without creating a second Policy, Reporting, Resources, or Goals owner. |
|
||||||
|
| Product/package governance | Signed configuration packages distinguish reference, product, sector, deployment, and integration classes; preserve parent/evidence provenance; prevent derived packages from loosening constraints; and preflight provider authority, maturity, exact binding, health, freshness, and recovery expectations. Executable product manifests now exist for governed communication and governed data/assurance and are checked in the module matrix. | Both artifacts deliberately remain product-class until target, accessibility, privacy, security, operations, and recovery evidence justifies reference readiness. |
|
||||||
|
| Projection and release | Platform metadata, signed module catalogs, release synthesis, Ops, and role-aware Docs retain and display architecture/provider declarations. Module-owned state providers add bounded configured/active, authority, health, freshness, conflict, recovery, and observation state; ordinary-user Docs omits binding detail. Static checks validate evidence paths, and the WebUI build verifies consuming types. | Runtime-state adoption and broader portfolio presentation follow truthful provider declaration rollout. |
|
||||||
|
|
||||||
|
The implementation deliberately keeps shared reference contracts in Core and
|
||||||
|
domain tables in their owners. It does not claim unsupported release maturity:
|
||||||
|
the four extracted owners and package remain `vertical_slice`/`product` until
|
||||||
|
target evidence supports a stronger claim. Gitea remains authoritative for
|
||||||
|
feature depth beyond this architecture contract.
|
||||||
|
|
||||||
|
## Target capability layers
|
||||||
|
|
||||||
|
The layers describe ownership and dependency direction. They are not navigation
|
||||||
|
groups and do not imply that every installation exposes every module.
|
||||||
|
|
||||||
|
| Layer | Responsibility | Current owners and declared directions |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 0. Runtime and meta | Composition, release, migrations, shared contracts, operations, deployment | Core, meta repository, Admin, Ops |
|
||||||
|
| 1. Institutional foundation | Institution, tenant, identity, organization, function, authority, access, trust | Tenancy, Identity, Organizations, IDM, Access, Identity Trust, Encryption, Mandates |
|
||||||
|
| 2. Governance and accountability | Policy, audit, risk, control, explainability, configured projection | Policy, Audit, Risk Compliance, Docs, Views, Search, Decisions |
|
||||||
|
| 3. Human work and procedure | Intake, cases, tasks, approvals, process execution and editing | Services, Forms, Forms Runtime, Cases, Parties, Tasks, Approvals, Workflow Engine, Workflow, Tickets |
|
||||||
|
| 4. Communication and participation | Delivery, participation, scheduling, channels, consultation | Portal, Postbox, Notifications, Mail, Campaign, Calendar, Scheduling, Poll, Appointments, Booking, Consultation, Committee, Addresses, Distribution Lists |
|
||||||
|
| 5. Content, records, and evidence | Managed content, templates, records, knowledge, disclosure | Files, Templates, DMS, Records, Wiki, Transparency, Certificates |
|
||||||
|
| 6. Data, reporting, and integration | Source access, staging, transformation, search, analytics, protocols | Connectors, Datasources, Dataflow, Reporting, Dashboard, REST, SOAP, XOE/V, XTA/OSCI, FIT-Connect, XRechnung, ERP adapters |
|
||||||
|
| 7. Domain capabilities | Reusable public-sector subject matter | Projects, Procurement, Contracts, Grants, Resources, Assets, Facilities, Learning, Payments, Ledger, Permits, Inspections, Evaluation, Helpdesk |
|
||||||
|
| 8. Product and sector packages | Versioned compositions, terminology, forms, processes, controls, reports, integration profiles | Signed configuration packages and reference packages; not runtime modules by default |
|
||||||
|
|
||||||
|
## Canonical institutional semantics
|
||||||
|
|
||||||
|
The connected model must keep these concepts distinct even where one UI
|
||||||
|
combines them.
|
||||||
|
|
||||||
|
| Concept | Canonical answer | Owner or direction |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Institution and tenant | In which governed installation and tenant does work occur? | Tenancy and Organizations |
|
||||||
|
| Organization and unit | Where is responsibility situated? | Organizations |
|
||||||
|
| Function | Which named organizational responsibility can an incumbent hold? | Organizations |
|
||||||
|
| Identity and account | Who is the person or machine, and through which account do they act? | Identity and Access |
|
||||||
|
| Function assignment | Who holds or represents a function, for which interval and source? | IDM |
|
||||||
|
| Role and permission | What application behavior may the acting principal perform? | Access, constrained by Policy |
|
||||||
|
| Mandate and jurisdiction | Why is an institution, unit, or function competent to act on this subject, territory, population, or interval? | Mandates |
|
||||||
|
| Service | What governed promise can an institution offer, to whom, under which prerequisites, evidence, channel, deadline, and responsibility? | Services; Portal presents it |
|
||||||
|
| Case | Which concrete administrative matter is being handled? | Cases |
|
||||||
|
| Party | In what procedural capacity does a person or organization participate, and who may represent or receive for it? | Parties; Identity/Organizations remain the subject owners |
|
||||||
|
| Work item | What must a responsible actor do next? | Tasks and domain modules |
|
||||||
|
| Workflow | How is work coordinated, including waits, human hand-offs, and governed actions? | Workflow Engine; Workflow is the optional editor |
|
||||||
|
| Approval | Has a proposed action passed a configured review or separation-of-duties gate? | Approvals |
|
||||||
|
| Decision | What formal institutional outcome was reached, by which competent authority, on which facts, rules, evidence, reasoning, and review path? | Decisions |
|
||||||
|
| Evidence and record | What proves the input, state, action, effect, correction, and retained institutional memory? | Domain owner, Files/DMS/Records, and Audit |
|
||||||
|
|
||||||
|
### Extracted semantic modules
|
||||||
|
|
||||||
|
Four horizontal concepts passed the repository proof threshold. Their Core
|
||||||
|
DTOs and provider protocols remain neutral; their persistent data, lifecycle,
|
||||||
|
security, APIs, migrations, and recovery behavior now live in independent
|
||||||
|
repositories.
|
||||||
|
|
||||||
|
#### Mandates
|
||||||
|
|
||||||
|
Mandates should own public or internal tasks, jurisdiction, responsibility,
|
||||||
|
decision/signature authority, legal or organizational basis, and effective
|
||||||
|
history. Organizations continues to own structures and functions; IDM owns
|
||||||
|
incumbency; Access owns permissions; Policy owns constraints.
|
||||||
|
|
||||||
|
`govoplan-mandates` answers: *Was this function competent to act for this case
|
||||||
|
at the relevant time, and on what basis?* Its resolver evaluates effective
|
||||||
|
time, task, authority, unit, function, jurisdiction, subject, conflicts, legal
|
||||||
|
basis, and evidence deterministically. Missing or ambiguous authority fails
|
||||||
|
closed.
|
||||||
|
|
||||||
|
#### Services
|
||||||
|
|
||||||
|
Services should own versioned service definitions: audience, prerequisites,
|
||||||
|
legal basis, evidence, fees, deadlines, channels, responsible unit/function,
|
||||||
|
jurisdiction, forms, case/workflow/result bindings, remedies, service levels,
|
||||||
|
and publication status. Portal presents and starts services but should not own
|
||||||
|
their institutional definition.
|
||||||
|
|
||||||
|
`govoplan-services` now owns those exact versioned definitions. Portal is the
|
||||||
|
first presentation consumer and Cases freezes the selected revision into its
|
||||||
|
intake context. Availability is an independent capability so publication does
|
||||||
|
not imply that all runtime prerequisites are satisfied.
|
||||||
|
|
||||||
|
#### Parties
|
||||||
|
|
||||||
|
Parties should own procedure-local roles and relationships: applicant,
|
||||||
|
respondent, beneficiary, representative, joint applicant, delivery recipient,
|
||||||
|
power or authority to represent, and permitted/preferred channels for the
|
||||||
|
matter. Identity answers who the subject is; Organizations answers which
|
||||||
|
institutional unit it is; Addresses owns contact points; Parties answers how
|
||||||
|
the subject participates here.
|
||||||
|
|
||||||
|
`govoplan-parties` owns the shared effective-dated lifecycle. Cases retains a
|
||||||
|
bounded compatibility projection only when the module is absent; that fallback
|
||||||
|
contains no representation lifecycle and cannot silently become a second
|
||||||
|
authority source.
|
||||||
|
|
||||||
|
#### Decisions
|
||||||
|
|
||||||
|
Decisions should own formal outcomes: subject, type, competent authority,
|
||||||
|
facts, evidence, applicable rule versions, reasoning, operative result,
|
||||||
|
conditions, effect, delivery/publication, remedy/review, correction, revocation,
|
||||||
|
and links to observed effects. Approvals own review gates; Poll owns response
|
||||||
|
collection; Committee owns deliberation, meetings, and votes; Workflow owns
|
||||||
|
coordination.
|
||||||
|
|
||||||
|
`govoplan-decisions` owns the persistent lifecycle and protected reconstruction
|
||||||
|
surface. Committee supplies deliberation context and records through the
|
||||||
|
provider capability. Consumers retain exact Decision references without
|
||||||
|
gaining table access.
|
||||||
|
|
||||||
|
## Source authority and integration maturity
|
||||||
|
|
||||||
|
Two independent dimensions must be recorded. They must not be collapsed into a
|
||||||
|
single `sync` flag.
|
||||||
|
|
||||||
|
### Source-authority mode
|
||||||
|
|
||||||
|
| Mode | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `native_authoritative` | GovOPlaN owns the authoritative object and lifecycle. |
|
||||||
|
| `external_authoritative` | The external system owns the object; GovOPlaN reads or acts through it. |
|
||||||
|
| `external_mirror` | The external system is authoritative and GovOPlaN keeps a governed local projection or immutable snapshots. |
|
||||||
|
| `governed_sync` | Both sides may change supported fields under explicit conflict and reconciliation rules. |
|
||||||
|
| `governance_overlay` | GovOPlaN owns policy, responsibility, evidence, or coordination around an externally executed object. |
|
||||||
|
| `linked_reference` | GovOPlaN keeps only a stable link and minimal display/provenance metadata. |
|
||||||
|
|
||||||
|
Authority may be declared per tenant, organization, service, object type,
|
||||||
|
object, field group, or process step. A broad default must not hide a narrower
|
||||||
|
override.
|
||||||
|
|
||||||
|
### Integration maturity
|
||||||
|
|
||||||
|
The implemented maturity ladder remains `discover`, `link`, `search`, `read`,
|
||||||
|
`publish`, `synchronize`, `migrate`, and `replace`. Maturity says what an
|
||||||
|
adapter can do. Source-authority mode says who owns truth in a particular
|
||||||
|
configuration. For example, a connector may support `synchronize`, while a
|
||||||
|
tenant deliberately configures it as `external_mirror`.
|
||||||
|
|
||||||
|
### Provider declaration
|
||||||
|
|
||||||
|
Every provider that reads or causes external effects must declare:
|
||||||
|
|
||||||
|
- owned object and field groups;
|
||||||
|
- supported source-authority modes and integration maturity;
|
||||||
|
- read, write, delete, search, preview, and dry-run operations;
|
||||||
|
- revision/concurrency tokens, freshness, health, and bounded-read limits;
|
||||||
|
- idempotency, retry, timeout, conflict, and outcome-unknown behavior;
|
||||||
|
- evidence, audit, correction, rollback/compensation, and reconciliation paths;
|
||||||
|
- degraded and outage behavior;
|
||||||
|
- classification, purpose, retention, and secret-handling requirements.
|
||||||
|
|
||||||
|
The common provider declaration composes the external-reference, action/effect,
|
||||||
|
connector-lifecycle, capability, operational-check, and documentation
|
||||||
|
contracts. Core, release tooling, Ops, Docs, and configuration-package
|
||||||
|
preflight validate it; Registry refuses to activate a declared external
|
||||||
|
provider without bounded, sanitized runtime state.
|
||||||
|
|
||||||
|
## Cross-cutting contracts
|
||||||
|
|
||||||
|
The following contracts are mandatory for consequential domain objects. They
|
||||||
|
should be shared reference DTOs and provider protocols, not shared domain
|
||||||
|
tables in Core.
|
||||||
|
|
||||||
|
1. **Time and history:** valid-from/to, recorded-at, superseded-at, revision,
|
||||||
|
change reason, and stable identity.
|
||||||
|
2. **Actor and representation:** real account/identity, system or service
|
||||||
|
account, represented account/function/party, delegation or power, and
|
||||||
|
mandate reference.
|
||||||
|
3. **Institutional context:** tenant, institution, organization unit, function,
|
||||||
|
task/mandate, jurisdiction, service, case, and decision references.
|
||||||
|
4. **Legal and policy basis:** typed, versioned references to rules,
|
||||||
|
obligations, policies, exceptions, and the effective decision source.
|
||||||
|
5. **Requested and observed effect:** intent, approval, dispatch, possible
|
||||||
|
execution, confirmation, reconciliation, correction, and terminal evidence.
|
||||||
|
6. **Evidence and provenance:** source, version, checksum, derivation,
|
||||||
|
responsible actor, timestamps, and inspection links.
|
||||||
|
7. **Information governance:** classification, purpose, legal basis, retention,
|
||||||
|
hold, minimization, and disclosure state.
|
||||||
|
8. **External source:** system/profile/object identity, authority mode,
|
||||||
|
maturity, version, freshness, health, and conflict state.
|
||||||
|
9. **Presentation:** language, accessibility, channel, explanation, and
|
||||||
|
configured availability.
|
||||||
|
|
||||||
|
Existing contracts already cover substantial parts of items 1, 2, 5, 6, 8,
|
||||||
|
and 9. New work should extend those contracts instead of creating parallel DTO
|
||||||
|
families.
|
||||||
|
|
||||||
|
## Existing module direction changes
|
||||||
|
|
||||||
|
### Datasources becomes the governed data and register catalogue
|
||||||
|
|
||||||
|
The implemented live/cached/static, staging, immutable materialization, and
|
||||||
|
publication model includes typed governance metadata for owner/steward,
|
||||||
|
authoritative source and authority mode, legal basis and purpose, semantic
|
||||||
|
definition, quality and freshness policy, classification, transfer agreement,
|
||||||
|
correction process, affected services/processes, and dependent flows,
|
||||||
|
reports, controls, and decisions. Connector credentials and protocol behavior
|
||||||
|
remain outside Datasources.
|
||||||
|
|
||||||
|
### Projects grows into portfolio and change governance
|
||||||
|
|
||||||
|
The Projects boundary already includes portfolios and goals. Extend it through
|
||||||
|
versioned objectives/outcomes, dependencies, capacity, benefits, change impact,
|
||||||
|
and links to mandates, services, risks, contracts, resources, and indicators.
|
||||||
|
Do not create a separate Goals module before more than one domain proves an
|
||||||
|
independent goal lifecycle.
|
||||||
|
|
||||||
|
### Reporting becomes evidence-backed institutional measurement
|
||||||
|
|
||||||
|
Every report, measure, and indicator should explain the institutional question
|
||||||
|
or obligation it serves, owner, source/materialization and flow revision,
|
||||||
|
freshness/quality, calculation version, visibility/purpose limits, publication,
|
||||||
|
and decisions or actions that consumed it. Reporting owns presentation and
|
||||||
|
execution; source and transformation owners retain their domains.
|
||||||
|
|
||||||
|
### Risk Compliance becomes the horizontal assurance model
|
||||||
|
|
||||||
|
Sanctions screening remains a complete vertical slice. The broader reusable
|
||||||
|
model is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Obligation -> governed object -> risk -> control -> evidence -> finding -> measure -> effectiveness review
|
||||||
|
```
|
||||||
|
|
||||||
|
Risk Compliance now persists that effective-dated, immutable-revision assurance
|
||||||
|
graph, exposes bounded tenant-safe traversal/search/editing, and projects each
|
||||||
|
completed sanctions run into it idempotently. Policy
|
||||||
|
owns enforceable rules and decisions; Audit owns immutable event evidence;
|
||||||
|
domain modules own the governed objects and corrective actions.
|
||||||
|
|
||||||
|
### Connectors exposes authority and effect behavior
|
||||||
|
|
||||||
|
Connector direction (`consume`, `publish`, `bidirectional`) remains useful but
|
||||||
|
is not enough. Profiles and bindings need the source-authority mode and
|
||||||
|
provider declaration above. ERP remains an integration family: finance,
|
||||||
|
workforce, procurement, asset, or other domain modules own semantics while
|
||||||
|
connectors own transport and source interaction.
|
||||||
|
|
||||||
|
### Geography starts as a reference contract
|
||||||
|
|
||||||
|
Before adding a `govoplan-geo` module, define a common reference shape for
|
||||||
|
coordinates, geometry, administrative area, address/location, CRS, source,
|
||||||
|
accuracy, validity, and external GIS identity. Create a repository only when
|
||||||
|
GovOPlaN must own spatial datasets, topology, or independent geospatial
|
||||||
|
lifecycles rather than link to an external GIS.
|
||||||
|
|
||||||
|
## Product and sector packages
|
||||||
|
|
||||||
|
A module says what capability can exist. A product package says how capabilities
|
||||||
|
work together for a bounded outcome. A sector package specializes vocabulary,
|
||||||
|
forms, rules, process baselines, controls, reports, and integration profiles
|
||||||
|
without forking the platform.
|
||||||
|
|
||||||
|
The signed configuration-package mechanism distinguishes:
|
||||||
|
|
||||||
|
- **reference package:** tested composition proving a journey and its recovery
|
||||||
|
behavior;
|
||||||
|
- **product package:** reusable operating capability such as governed
|
||||||
|
communication, service-to-decision, procurement/contracts, or governed BI;
|
||||||
|
- **sector package:** institutional specialization such as municipality,
|
||||||
|
university/research, ministry/program, regulator, grants authority, or
|
||||||
|
committee/council;
|
||||||
|
- **deployment profile:** supported infrastructure and operational topology;
|
||||||
|
- **integration profile:** supported set of external systems, authority modes,
|
||||||
|
bindings, and health expectations.
|
||||||
|
|
||||||
|
Packages may require modules and capabilities, but package definitions remain
|
||||||
|
configuration and evidence. They do not gain access to module-owned tables.
|
||||||
|
|
||||||
|
## Module portfolio metadata
|
||||||
|
|
||||||
|
Repository category is not capability maturity. The runtime manifest, release
|
||||||
|
catalog, Docs projection, and meta repository inventory use one
|
||||||
|
machine-readable declaration with at least:
|
||||||
|
|
||||||
|
- architecture layer and module kind;
|
||||||
|
- lifecycle/maturity claim: `concept`, `scaffold`, `vertical_slice`,
|
||||||
|
`reference_ready`, `supported`, or `lts`;
|
||||||
|
- evidence supporting the claim and known limits;
|
||||||
|
- supported source-authority modes;
|
||||||
|
- owned and explicitly non-owned concepts;
|
||||||
|
- provided/required capabilities and interfaces;
|
||||||
|
- reference packages and target-tested providers;
|
||||||
|
- migration, upgrade, recovery, security, and operations documentation.
|
||||||
|
|
||||||
|
Maturity is a release claim and must be checked against evidence. A manifest
|
||||||
|
must not become “supported” merely because a maintainer changes one string.
|
||||||
|
|
||||||
|
Create a repository only when the capability has distinct data ownership,
|
||||||
|
independent installability, technical assets, a security/lifecycle profile, a
|
||||||
|
release reason, more than one consumer or a proven reference process, and tests
|
||||||
|
that justify the boundary. Otherwise use a shared DTO, provider capability,
|
||||||
|
submodule, configuration fragment, package, or profile.
|
||||||
|
|
||||||
|
## Implemented migration sequence
|
||||||
|
|
||||||
|
### 0. Align the portfolio and contracts - complete
|
||||||
|
|
||||||
|
- This reconciliation is canonical in the meta repository and mirrored to the
|
||||||
|
Gitea wiki.
|
||||||
|
- All 62 source manifests carry validated evidence-based architecture metadata.
|
||||||
|
- External-reference, action/effect, operational-health, ownership, policy,
|
||||||
|
audit, and documentation primitives compose into one enforced provider
|
||||||
|
declaration and sanitized runtime-state contract.
|
||||||
|
- Institutional context, legal basis, evidence, presentation, external source,
|
||||||
|
information governance, temporal revision, and geo references are shared
|
||||||
|
Core DTOs rather than shared domain tables.
|
||||||
|
|
||||||
|
### 1. Prove responsibility and formal outcome - complete
|
||||||
|
|
||||||
|
- Mandate and Decision contracts, deterministic resolution, lifecycle
|
||||||
|
transitions, persistence providers, APIs, permissions, migrations, recovery,
|
||||||
|
and tests are implemented.
|
||||||
|
- Committee and the SQL-backed institutional fixture prove effective-time
|
||||||
|
authority, persisted meeting/agendum/vote/minute context, approval context,
|
||||||
|
reasoning, evidence, observed effect, correction/revision rules, protected
|
||||||
|
reconstruction, and review references.
|
||||||
|
- The independent Mandates and Decisions repositories were created only after
|
||||||
|
persistence and reuse passed the repository threshold.
|
||||||
|
|
||||||
|
### 2. Separate service and party semantics - complete
|
||||||
|
|
||||||
|
- Portal remains the presentation surface while Services owns reusable,
|
||||||
|
versioned definitions and explainable availability.
|
||||||
|
- Parties owns procedure roles, contact snapshots, and append-only
|
||||||
|
representation/revocation authority; Cases consumes the common resolver.
|
||||||
|
- `product.service-to-decision` proves both through a portable administrative
|
||||||
|
service composition.
|
||||||
|
- Forms owns immutable, versioned schemas while Forms Runtime owns drafts,
|
||||||
|
server validation, submission receipts, status/evidence history, and exact
|
||||||
|
Service/Form provenance. Portal delegates Form launch through the runtime
|
||||||
|
capability and fails closed when it is unavailable.
|
||||||
|
|
||||||
|
### 3. Complete governed data, portfolio, and assurance - vertical slices complete
|
||||||
|
|
||||||
|
- Datasources carries typed governance through staging and immutable
|
||||||
|
materializations, with bounded catalogue filters and dependency references.
|
||||||
|
- Reporting owns immutable semantic definitions, safe execution, quality gates,
|
||||||
|
provenance, schedules, saved views, pivoting, and export/import assessment
|
||||||
|
without taking source or transformation ownership.
|
||||||
|
- Risk Compliance persists the horizontal obligation/risk/control/evidence/
|
||||||
|
finding/measure graph and projects sanctions runs idempotently.
|
||||||
|
- Projects persists portfolio/outcome/change-governance revisions as a
|
||||||
|
consuming domain without becoming a second policy or reporting engine.
|
||||||
|
|
||||||
|
### 4. Package repeatable public-sector outcomes - complete at product maturity
|
||||||
|
|
||||||
|
- Governed communication, governed data/assurance, and service-to-decision are
|
||||||
|
portable product package manifests with repository-local evidence.
|
||||||
|
- Package preflight enforces module, capability, provider authority, health,
|
||||||
|
freshness, and recovery expectations without cross-module table access.
|
||||||
|
- Sector and `reference_ready` claims remain gated on target-environment,
|
||||||
|
recovery, accessibility, privacy, security, and operator evidence. This is a
|
||||||
|
maturity gate, not missing architecture implementation.
|
||||||
|
|
||||||
|
## What remains after the executable architecture slice
|
||||||
|
|
||||||
|
The remaining work is not another Core or cross-module architecture rewrite.
|
||||||
|
It falls into two explicitly different categories, neither of which can be
|
||||||
|
truthfully completed by adding generic platform code:
|
||||||
|
|
||||||
|
1. **Concrete provider packages:** the Committee ballot adapter contract is
|
||||||
|
complete, but a real secret/electronic ballot provider requires a selected
|
||||||
|
protocol and product decisions for voter eligibility, custody, secrecy,
|
||||||
|
recount, challenge, retention, and operational assurance. Equivalent future
|
||||||
|
adapters must satisfy the declared provider and recovery gates.
|
||||||
|
Provider selection and certification are tracked in
|
||||||
|
[Committee #1](https://git.add-ideas.de/GovOPlaN/govoplan-committee/issues/1).
|
||||||
|
2. **Target-produced maturity evidence:** `reference_ready`, `supported`, and
|
||||||
|
`lts` cannot be generated from source code. An exact release and deployment
|
||||||
|
must produce signed, expiring accessibility, privacy, security, operator,
|
||||||
|
provider, backup/restore, rollback, and recovery-drill evidence. The verifier
|
||||||
|
and schemas are implemented; the actual claims require those real runs.
|
||||||
|
A bounded issuer now hashes retained reports, checks role-scoped signing
|
||||||
|
authority and exact installed-release origin, emits sanitized signed
|
||||||
|
receipts, verifies them immediately, and exposes admission-enforcing CLI
|
||||||
|
gates. The real pinned-release evidence run is tracked in
|
||||||
|
[GovOPlaN #37](https://git.add-ideas.de/GovOPlaN/govoplan/issues/37).
|
||||||
|
|
||||||
|
Forms and Forms Runtime no longer constitute an architecture gap. Conditional
|
||||||
|
multi-page/localized authoring, package-fragment import, and durable native
|
||||||
|
Case/Workflow handoffs are implemented. Remaining depth is limited to
|
||||||
|
anonymous/public identity profiles, concrete file/signature providers, and
|
||||||
|
additional handoff target adapters. Those use the implemented immutable
|
||||||
|
definition, runtime, policy, evidence, service-launch, and domain-owner
|
||||||
|
boundaries rather than requiring another split. Public/provider decisions stay
|
||||||
|
tracked in Forms Runtime
|
||||||
|
[#2](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime/issues/2) and
|
||||||
|
[#3](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime/issues/3).
|
||||||
|
|
||||||
|
Approvals, Voting, Workflow trigger/wait dispatch, Identity Trust, and
|
||||||
|
Encryption now likewise have repository owners, neutral Core contracts,
|
||||||
|
persistence, migrations, recovery/disable semantics, documentation and focused
|
||||||
|
tests. Their remaining tickets concern concrete providers, deeper adapters and
|
||||||
|
target evidence, not an unresolved institutional architecture boundary.
|
||||||
|
|
||||||
|
Everything else described as architecture in this document now has a
|
||||||
|
repository owner, versioned contract, bounded implementation, migration and
|
||||||
|
recovery boundary where state exists, documentation, and executable evidence.
|
||||||
|
Further work in those modules is product breadth, UX depth, provider adoption,
|
||||||
|
and evidence renewal.
|
||||||
|
|
||||||
|
## Delivery tracking
|
||||||
|
|
||||||
|
The completed cross-repository architecture epic is
|
||||||
|
[GovOPlaN #29](https://git.add-ideas.de/GovOPlaN/govoplan/issues/29).
|
||||||
|
Its implementation work packages and resulting owners are:
|
||||||
|
|
||||||
|
- [Core #279](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/279):
|
||||||
|
validated module architecture and provider authority declarations;
|
||||||
|
- [Core #280](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/280):
|
||||||
|
shared institutional-context and governed reference primitives;
|
||||||
|
- [GovOPlaN #30](https://git.add-ideas.de/GovOPlaN/govoplan/issues/30) and
|
||||||
|
[govoplan-mandates](https://git.add-ideas.de/GovOPlaN/govoplan-mandates):
|
||||||
|
Mandates semantics and persistent resolver;
|
||||||
|
- [GovOPlaN #31](https://git.add-ideas.de/GovOPlaN/govoplan/issues/31) and
|
||||||
|
[govoplan-services](https://git.add-ideas.de/GovOPlaN/govoplan-services):
|
||||||
|
Services semantics, catalogue, and availability;
|
||||||
|
- [GovOPlaN #32](https://git.add-ideas.de/GovOPlaN/govoplan/issues/32) and
|
||||||
|
[govoplan-parties](https://git.add-ideas.de/GovOPlaN/govoplan-parties):
|
||||||
|
Parties and representation semantics and resolver;
|
||||||
|
- [GovOPlaN #33](https://git.add-ideas.de/GovOPlaN/govoplan/issues/33) and
|
||||||
|
[govoplan-decisions](https://git.add-ideas.de/GovOPlaN/govoplan-decisions):
|
||||||
|
formal Decisions semantics and registry;
|
||||||
|
- [Datasources #6](https://git.add-ideas.de/GovOPlaN/govoplan-datasources/issues/6):
|
||||||
|
governed data/register catalogue;
|
||||||
|
- [Risk Compliance #7](https://git.add-ideas.de/GovOPlaN/govoplan-risk-compliance/issues/7):
|
||||||
|
horizontal assurance graph;
|
||||||
|
- [GovOPlaN #34](https://git.add-ideas.de/GovOPlaN/govoplan/issues/34):
|
||||||
|
product and sector package classes; and
|
||||||
|
- [Docs #19](https://git.add-ideas.de/GovOPlaN/govoplan-docs/issues/19):
|
||||||
|
configured architecture, maturity, and source-authority explanations;
|
||||||
|
- [Forms #2](https://git.add-ideas.de/GovOPlaN/govoplan-forms/issues/2):
|
||||||
|
immutable reusable definitions and the designer surface; and
|
||||||
|
- [Forms Runtime #1](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime/issues/1):
|
||||||
|
definition-aware submissions and Portal service launch;
|
||||||
|
- [Forms #3](https://git.add-ideas.de/GovOPlaN/govoplan-forms/issues/3) and
|
||||||
|
[Forms Runtime #4](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime/issues/4):
|
||||||
|
conditional/localized definition depth and governed native handoffs;
|
||||||
|
- [Approvals #1](https://git.add-ideas.de/GovOPlaN/govoplan-approvals/issues/1)
|
||||||
|
and [Campaign #22](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/22):
|
||||||
|
generic exact-subject approval chains and one consequential delivery gate;
|
||||||
|
- `govoplan-voting`: governed recorded ballots plus fail-closed provider-backed
|
||||||
|
assurance profiles consumed by Committee; and
|
||||||
|
- Identity Trust #1 and Encryption #1-#3: public device trust, provider-neutral
|
||||||
|
key/protection lifecycle, recovery authorization and disable proof, plus a
|
||||||
|
bounded local server-envelope provider and Files/Postbox fixtures; external
|
||||||
|
KMS/HSM/client-provider conformance remains separately gated.
|
||||||
|
|
||||||
|
Existing Projects #1, Reporting #4, Portal #1, Cases #1, Datasources #1,
|
||||||
|
Risk Compliance #2, GovOPlaN #14, and GovOPlaN #19 carry product-depth and
|
||||||
|
reference-readiness work instead of duplicating the completed architecture
|
||||||
|
contract.
|
||||||
|
|
||||||
|
## Completion evidence
|
||||||
|
|
||||||
|
The architecture direction is established by the following executable and
|
||||||
|
machine-enforced evidence:
|
||||||
|
|
||||||
|
- the `product.service-to-decision` composition and SQL-backed golden fixture
|
||||||
|
retain institutional context from service entry through a persisted case,
|
||||||
|
party, authority/work context, committee deliberation, decision, observed
|
||||||
|
communication effect, minute/record, and review references;
|
||||||
|
- the Portal/Form journey retains the exact published Service and Form
|
||||||
|
revisions through persisted draft state, validates on the server, and returns
|
||||||
|
the same submission on an idempotent launch replay;
|
||||||
|
- the system can answer who acted, for whom, in which function, under which
|
||||||
|
mandate and jurisdiction, using which rule and evidence versions;
|
||||||
|
- every implemented external binding declares authority mode, maturity,
|
||||||
|
operations, health, freshness, conflict, and recovery behavior, and Registry
|
||||||
|
rejects a declaration without sanitized runtime state;
|
||||||
|
- every material report or decision can be reconstructed from governed source
|
||||||
|
and transformation versions;
|
||||||
|
- product/package manifests are portable without cross-module table access or
|
||||||
|
code forks, while future sector packages inherit the same signed-package
|
||||||
|
constraints; and
|
||||||
|
- documentation and Ops explain the configured composition and its limits to
|
||||||
|
users, administrators, operators, and auditors.
|
||||||
|
|
||||||
|
These criteria complete the architecture contract at `vertical_slice` and
|
||||||
|
`product` maturity. They do not waive the separately enforced evidence needed
|
||||||
|
for a module or package to claim `reference_ready`, `supported`, or `lts`.
|
||||||
|
The capability-fit verifier now computes that cumulative readiness gate from
|
||||||
|
independently signed, expiring claims bound to the exact assessed release,
|
||||||
|
installed payload, deployment subject, controls, and artifact hashes. Actual
|
||||||
|
target runs and recovery drills remain operator-produced evidence.
|
||||||
@@ -444,25 +444,25 @@ reason to infer that a pattern is satisfied.
|
|||||||
and does not duplicate a central component.
|
and does not duplicate a central component.
|
||||||
- Behavioral/accessibility evidence is linked from the rollout matrix and issue.
|
- Behavioral/accessibility evidence is linked from the rollout matrix and issue.
|
||||||
- Configured-system help can reach the applicable pattern or reference topic
|
- Configured-system help can reach the applicable pattern or reference topic
|
||||||
when [Docs #15](https://git.add-ideas.de/add-ideas/govoplan-docs/issues/15)
|
when [Docs #15](https://git.add-ideas.de/GovOPlaN/govoplan-docs/issues/15)
|
||||||
supplies that experience.
|
supplies that experience.
|
||||||
|
|
||||||
## First Pilot: Campaign
|
## First Pilot: Campaign
|
||||||
|
|
||||||
[Campaign #74](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/74)
|
[Campaign #74](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/74)
|
||||||
is the first full-domain audit and migration. It should prove patterns before
|
is the first full-domain audit and migration. It should prove patterns before
|
||||||
generic extraction:
|
generic extraction:
|
||||||
|
|
||||||
- [#59](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/59) and
|
- [#59](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/59) and
|
||||||
[#73](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/73): stable,
|
[#73](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/73): stable,
|
||||||
accessible preview and attachment-detail overlays
|
accessible preview and attachment-detail overlays
|
||||||
- [#63](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/63): review
|
- [#63](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/63): review
|
||||||
stages, outcomes, blockers, and intervention vocabulary
|
stages, outcomes, blockers, and intervention vocabulary
|
||||||
- [#62](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/62): explicit
|
- [#62](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/62): explicit
|
||||||
synchronous/asynchronous send mode and durable delivery progress
|
synchronous/asynchronous send mode and durable delivery progress
|
||||||
- [#65](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/65): one
|
- [#65](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/65): one
|
||||||
coherent report filtering and count-affordance model
|
coherent report filtering and count-affordance model
|
||||||
- [#35](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/35): guided
|
- [#35](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/35): guided
|
||||||
first-campaign entry
|
first-campaign entry
|
||||||
|
|
||||||
These slices do not depend on the Workflow runtime. Campaign's current
|
These slices do not depend on the Workflow runtime. Campaign's current
|
||||||
|
|||||||
+244
-111
@@ -9,7 +9,20 @@ The applicable design contract is
|
|||||||
|
|
||||||
## Snapshot And Method
|
## Snapshot And Method
|
||||||
|
|
||||||
Snapshot refreshed: 2026-07-22.
|
The source-derived inventory command is documented in
|
||||||
|
[`PLATFORM_CONTROL_PLANE.md`](PLATFORM_CONTROL_PLANE.md). It produces
|
||||||
|
machine-readable field, label, translation, route, API-reference, and module
|
||||||
|
manifest evidence. This hand-maintained document remains the reviewed product
|
||||||
|
interpretation and rollout ledger; generated evidence does not replace it.
|
||||||
|
|
||||||
|
Snapshot refreshed: 2026-08-03.
|
||||||
|
|
||||||
|
The generated snapshot contains 65 module manifests, 35 WebUI-contributing
|
||||||
|
repositories, 40 statically declared module routes, 1,156 UI fields, and 836
|
||||||
|
backend endpoints. All backend endpoints are classified and no stale endpoint
|
||||||
|
declarations were found. The 234 endpoints without a static WebUI reference are
|
||||||
|
kept visible as review evidence; they may intentionally serve workers, public
|
||||||
|
clients, connectors, or external integrations.
|
||||||
|
|
||||||
Evidence was read from tracked Git `HEAD` in the local GovOPlaN checkouts:
|
Evidence was read from tracked Git `HEAD` in the local GovOPlaN checkouts:
|
||||||
|
|
||||||
@@ -45,37 +58,73 @@ Inventory states:
|
|||||||
|
|
||||||
| Surface | Owner and code evidence | Audience/access evidence | Primary task and target archetype | Audit / rollout |
|
| Surface | Owner and code evidence | Audience/access evidence | Primary task and target archetype | Audit / rollout |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| Public landing and login | `govoplan-core` `PublicLandingPage`; rendered while no authenticated principal exists | Unauthenticated; maintenance and backend-reachability context are shell inputs | Understand the service and authenticate; public entry | Unreviewed; later public-entry audit |
|
| Public landing and login | `govoplan-core` `PublicLandingPage`; rendered while no authenticated principal exists | Unauthenticated; maintenance and backend-reachability context are shell inputs | Understand the service and authenticate; public entry | Core shell contract complete under Core #227: semantic entry/login, uniform reachable/offline/maintenance feedback, keyboard focus, responsive layout and privacy-safe pre-authentication state |
|
||||||
| Session/bootstrap state | `govoplan-core` `App.tsx` and `AppShell` | All browser sessions during bootstrap | Understand that session/platform state is loading; state contract | Unreviewed; core shell |
|
| Session/bootstrap state | `govoplan-core` `App.tsx` and `AppShell` | All browser sessions during bootstrap | Understand that session/platform state is loading; state contract | Core shell contract complete: loading, unreachable, maintenance, authentication-required and module-load failure states use shared status/alert boundaries without erasing the shell |
|
||||||
| `/` authenticated redirect | `govoplan-core` chooses the first visible navigation destination | Authenticated; result depends on visible nav contributions | Enter the actor's first accessible service area; navigation behavior, not a content page | Unreviewed; focused-view/default-route work must preserve this fallback |
|
| `/` authenticated redirect | `govoplan-core` chooses the first visible navigation destination | Authenticated; result depends on visible nav contributions | Enter the actor's first accessible service area; navigation behavior, not a content page | Core route/module-permutation contract complete; permission, module, View and fallback filtering precede navigation and do not execute a domain action |
|
||||||
| `/dashboard` fallback | `govoplan-core` `DashboardPage` only when the Dashboard module is absent | Authenticated; no route-specific scope in core | Cross-module starting point; dashboard | Unreviewed; compare with module dashboard before shared changes |
|
| `/dashboard` fallback | `govoplan-core` `DashboardPage` only when the Dashboard module is absent | Authenticated; no route-specific scope in core | Cross-module starting point; dashboard | Core fallback and Dashboard module permutations complete; fallback remains usable without the optional Dashboard module |
|
||||||
| `/settings` | `govoplan-core` `SettingsPage` | Authenticated; contributed sections and integrations filter internally | Profile, UI/workspace preference, local connection, and user-scoped integration settings; configuration | Unreviewed; Core #225 program |
|
| `/settings` | `govoplan-core` `SettingsPage` | Authenticated; contributed sections and integrations filter internally | Profile, UI/workspace preference, local connection, and user-scoped integration settings; configuration | Core-owned pattern migration complete in [Core #225](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/225), commit `fa32cca` |
|
||||||
| Shell chrome | `AppShell`, `Titlebar`, `IconRail`, `BreadcrumbBar`, `HelpMenu`, language menu, unsaved-change provider | Public/authenticated variants; nav filtered later | Tenant/actor context, global navigation, help, language, session and maintenance state | Unreviewed; platform-owned prerequisite for focused views |
|
| Shell chrome | `AppShell`, `Titlebar`, `IconRail`, `BreadcrumbBar`, `HelpMenu`, language menu, unsaved-change provider | Public/authenticated variants; nav filtered later | Tenant/actor context, global navigation, help, language, session and maintenance state | Core shell contract complete under Core #227/#225 and Views #2: semantic global controls, scroll-safe rail, visible maintenance state, guarded navigation, configured Docs fallback, optional Search, responsive/theme/i18n checks and module permutations |
|
||||||
|
|
||||||
## Direct Module Route Contributions
|
## Direct Module Route Contributions
|
||||||
|
|
||||||
The access guard column reports only the route-level declaration in
|
The access column summarizes only the route-level declaration in `module.ts`.
|
||||||
`module.ts`. Inner APIs and controls may impose additional checks.
|
Inner APIs and controls may impose additional checks. Public and compatibility
|
||||||
|
routes are called out explicitly because they do not have the same manifest
|
||||||
|
semantics as authenticated navigation routes.
|
||||||
|
|
||||||
| Route | Owner / render evidence | Route-level access evidence | Primary task | Target archetype | Status / priority |
|
| Routes | Owner | Route-level access | Primary archetype | Migration issue |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| `/admin` | `govoplan-access` `AdminPage` | Any core `adminReadScopes` | Administer system and tenant concerns assembled from module sections | Administration/configuration | Contributed; unreviewed; P1 under [Core #225](https://git.add-ideas.de/add-ideas/govoplan-core/issues/225) |
|
| `/admin` | Access | Any declared administration/read scope | Administration/configuration host | Access pattern migration complete in [Access #19](https://git.add-ideas.de/GovOPlaN/govoplan-access/issues/19), commit `1409dbf`; shared host contract complete in [Core #225](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/225) |
|
||||||
| `/address-book` | `govoplan-addresses` `AddressBookPage` | `addresses:contact:read` | Browse and manage contacts, address books, and lists | Directory/list-detail | Contributed; unreviewed; P2 after Campaign |
|
| `/address-book` | Addresses | `addresses:contact:read` | Governed source directory, contact/list detail, external-provider operation, governance facts, and reversible correction | Addresses pattern migration complete in [Addresses #23](https://git.add-ideas.de/GovOPlaN/govoplan-addresses/issues/23), commit `f9a7185` |
|
||||||
| `/calendar` | `govoplan-calendar` `CalendarPage` | `calendar:event:read` | Browse calendars/events and act on calendar data | Directory/list-detail | Contributed; metadata gap; unreviewed; P2 after Campaign |
|
| `/approvals` | Approvals | `approvals:workspace:read` | Work queue/guided decision | Approvals pattern migration complete in [Approvals #3](https://git.add-ideas.de/GovOPlaN/govoplan-approvals/issues/3), commit `24e9559` |
|
||||||
| `/campaigns` | `govoplan-campaign` `CampaignListPage` | `campaigns:campaign:read` | Find, compare, create, and open campaigns | List-detail entry | Pilot; P1 [Campaign #74](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/74) |
|
| `/calendar` | Calendar | `calendar:event:read` | Full-height calendar workspace with filterable collection/agenda sidebar, continuous and bounded date views, guarded VEVENT and source editors, synchronized-source status, durable outbox recovery, and destructive remote-move evidence | Calendar pattern migration complete in [Calendar #22](https://git.add-ideas.de/GovOPlaN/govoplan-calendar/issues/22), commit `d7fd944` |
|
||||||
| `/campaigns/:campaignId/*` | `govoplan-campaign` `CampaignResourceRoute` and `CampaignWorkspace` | `campaigns:campaign:read`, plus resource probe | Configure, review, send, and inspect one campaign/version | List-detail workspace containing edit, review, monitoring, and evidence surfaces | Pilot; P1 Campaign #74 |
|
| `/campaigns`, `/campaigns/:campaignId/*`, `/campaigns/queue`, `/campaigns/reports` | Campaign | Campaign read/report/control scopes | List-detail, guided review, monitoring, reporting | Campaign pattern pilot complete in [Campaign #74](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/74); bounded product features such as watched-folder policy remain independently tracked |
|
||||||
| `/operator` | `govoplan-campaign` `OperatorQueuePage` | `campaigns:campaign:read` and any of queue, control, retry, or reconcile | Monitor and intervene in campaign jobs through authority-specific controls | Monitoring/work queue | Pilot; durable queue controls delivered in [Campaign #78](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/78); #74 audit remains |
|
| `/operator` | Campaign | Campaign read plus queue/control scope | Compatibility redirect to `/campaigns/queue` | Campaign #74 complete; redirect remains declared for saved links and is retired under the compatibility policy rather than through the UI migration |
|
||||||
| `/reports` | `govoplan-campaign` `AggregateReportsPage` | `campaigns:report:read` | Compare privacy-protected cross-campaign outcome totals without recipient detail, diagnostics, export, or drill-down | Aggregate reporting | Pilot; aggregate-reader surface delivered in [Campaign #80](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/80); #74 audit remains |
|
| `/cases`, `/cases/:caseId` | Cases | `cases:case:read` | Governed case directory and detail workspace with guarded OCC lifecycle editor, provider-owned references, immutable timeline/history, and confirmed object-access editor | Cases pattern migration complete in [Cases #4](https://git.add-ideas.de/GovOPlaN/govoplan-cases/issues/4), commit `43b4cc8` |
|
||||||
| `/templates` | `govoplan-campaign` `TemplatesPage` | No route guard declared in `module.ts` | Browse/manage campaign templates | Directory/list-detail | Pilot audit; permission intent must be verified; P2 |
|
| `/committee` | Committee | `committee:workspace:read` | Governed workspace | Committee pattern migration complete in [Committee #2](https://git.add-ideas.de/GovOPlaN/govoplan-committee/issues/2), commit `e64af30` |
|
||||||
| `/dashboard` | `govoplan-dashboard` `DashboardPage` | No route-specific scope | Assemble module-provided actionable widgets | Dashboard | Contributed; unreviewed; P2 |
|
| `/dashboard` | Dashboard | No route-specific scope | View-specific personal workspace with module/permission-filtered widget library, guarded four-column composition, nested widget settings, server/browser fallback, and optimistic layout persistence | Dashboard pattern migration complete in [Dashboard #3](https://git.add-ideas.de/GovOPlaN/govoplan-dashboard/issues/3), commit `da3947f` |
|
||||||
| `/docs` | `govoplan-docs` `DocsPage` | Docs read or system/tenant settings read scopes | Read configured, available, and evidence-aware documentation | Documentation directory/reference | Contributed; unreviewed; P1 [Docs #15](https://git.add-ideas.de/add-ideas/govoplan-docs/issues/15) after initial pattern content |
|
| `/dataflow` | Dataflow | Pipeline read/admin | Governed library, guarded graph/constrained-SQL definition editor, typed node inspector, bounded intermediate preview, automation triggers, and durable run/deployment evidence | Dataflow pattern migration complete in [Dataflow #20](https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/20), commit `109ddcd` |
|
||||||
| `/files` | `govoplan-files` `FilesPage` | `files:file:read` | Browse folders/files and perform managed-file work | Directory/explorer | Contributed; metadata gap; unreviewed; P2 after Campaign |
|
| `/datasources` | Datasources | Catalogue read/source admin | Governed catalogue, staging preflight, optional-origin directory, authority editor, and immutable evidence | Datasources pattern migration complete in [Datasources #7](https://git.add-ideas.de/GovOPlaN/govoplan-datasources/issues/7), commit `6406ce7` |
|
||||||
| `/idm` | `govoplan-idm` `IdmPage` | Any IDM assignment/write or organization function-assign scope | Inspect and govern identity/function assignments | List-detail/configuration | Contributed; unreviewed; P2 |
|
| `/distribution-lists` | Distribution Lists | List read/write/admin | Governed directory, immutable-revision editor, expansion preview, and evidence register | Distribution Lists pattern migration complete in [Distribution Lists #8](https://git.add-ideas.de/GovOPlaN/govoplan-dist-lists/issues/8), commit `6cdd804` |
|
||||||
| `/mail` | `govoplan-mail` `MailboxPage` | `mail:mailbox:read` | Browse mailboxes and messages | Directory/list-detail | Contributed; metadata gap; unreviewed; P2 after Campaign |
|
| `/docs` | Docs | Documentation or settings read | Documentation/reference | Configured-system workflow/reference/pattern help complete in [Docs #15](https://git.add-ideas.de/GovOPlaN/govoplan-docs/issues/15), commit `abe2f78` |
|
||||||
| `/notifications` | `govoplan-notifications` `NotificationCenterPage` | `notifications:notification:read` | Inspect and acknowledge notification state | List-detail/inbox | Contributed without a nav item or backend frontend metadata; navigation intent unknown; P2 discovery |
|
| `/files` | Files | `files:file:read` | Directory/explorer | Files pattern migration complete in [Files #42](https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/42), commit `d8ae506` |
|
||||||
| `/ops` | `govoplan-ops` `OpsPage` | Ops read or system/tenant settings read scopes | Inspect runtime health and readiness | Monitoring | Contributed; unreviewed; P2 |
|
| `/forms` | Forms | `forms:definition:read` | Definition library/editor | Forms pattern migration complete in [Forms #4](https://git.add-ideas.de/GovOPlaN/govoplan-forms/issues/4), commit `e505536` |
|
||||||
| `/organizations` | `govoplan-organizations` `OrganizationsPage` | Organization model/unit/function or admin settings read scopes | Model and inspect organizational structures/functions | Directory/list-detail | Contributed; unreviewed; P2 |
|
| `/forms-runtime`, `/forms-runtime/:instanceId` | Forms Runtime | Participate or workspace read | Guided form execution | Forms Runtime pattern migration complete in [Forms Runtime #5](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime/issues/5), commit `07dd35b` |
|
||||||
| `/scheduling` | `govoplan-scheduling` `SchedulingPage` | `scheduling:schedule:read` | Plan and decide scheduling requests and availability | List-detail/guided decision | Contributed; metadata gap; unreviewed; P2 |
|
| `/idm` | IDM | Assignment, function-change, relationship, or organization scopes | Directory/governed change | IDM pattern migration complete in [IDM #12](https://git.add-ideas.de/GovOPlaN/govoplan-idm/issues/12), commit `d864317` |
|
||||||
|
| `/mail`, `/mail/bounces` | Mail | Mailbox or bounce read/manage | Directory/explorer, operational evidence | Mail pattern migration complete in [Mail #20](https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/20), commit `7844d9c` |
|
||||||
|
| `/notifications` | Notifications | `notifications:notification:read` | Inbox/list-detail with guarded recipient state, confirmed local cancellation/dispatch, and sanitized delivery evidence | Notifications pattern migration complete in [Notifications #4](https://git.add-ideas.de/GovOPlaN/govoplan-notifications/issues/4), commit `ad6a31f` |
|
||||||
|
| `/ops` | Ops | Operations or settings read | Monitoring/evidence with contextual run, drain, readiness-blocker, and recovery guidance | Ops pattern migration complete in [Ops #4](https://git.add-ideas.de/GovOPlaN/govoplan-ops/issues/4), commit `2b32643` |
|
||||||
|
| `/organizations` | Organizations | Model/unit/function or settings read | Directory/hierarchy editor | Organizations pattern migration complete in [Organizations #7](https://git.add-ideas.de/GovOPlaN/govoplan-organizations/issues/7), commit `97acfcb` |
|
||||||
|
| `/portal` | Portal | `portal:service:read` | Explained service directory and governed handoff | Portal pattern migration complete in [Portal #2](https://git.add-ideas.de/GovOPlaN/govoplan-portal/issues/2); durable evidence in `govoplan-portal/docs/INTERFACE_PATTERN_MIGRATION.md` |
|
||||||
|
| `/postbox` | Postbox | `postbox:postbox:read` | Inbox/list-detail | Postbox pattern migration complete in [Postbox #26](https://git.add-ideas.de/GovOPlaN/govoplan-postbox/issues/26), commit `a97eb3b` |
|
||||||
|
| `/projects` | Projects | `projects:project:read` | Revisioned list-detail/project workspace | Projects pattern migration complete in [Projects #2](https://git.add-ideas.de/GovOPlaN/govoplan-projects/issues/2); durable evidence in `govoplan-projects/docs/INTERFACE_PATTERN_MIGRATION.md` |
|
||||||
|
| `/reporting`, `/reports` | Reporting | `reporting:definition:read` | Governed report catalogue, analytical workspace and evidence | Reporting pattern migration complete in [Reporting #8](https://git.add-ideas.de/GovOPlaN/govoplan-reporting/issues/8); durable evidence in `govoplan-reporting/docs/INTERFACE_PATTERN_MIGRATION.md` |
|
||||||
|
| `/risk-compliance` | Risk Compliance | Workspace or sanctions read | Immutable source evidence, version-pinned screening, list-detail review, and revisioned assurance graph with explicit blockers and consequences | Risk Compliance pattern migration complete in [Risk Compliance #8](https://git.add-ideas.de/GovOPlaN/govoplan-risk-compliance/issues/8), commit `24d80a6` |
|
||||||
|
| `/scheduling` | Scheduling | `scheduling:schedule:read` | List-detail/guided decision | Scheduling pattern migration complete in [Scheduling #8](https://git.add-ideas.de/GovOPlaN/govoplan-scheduling/issues/8), commit `c17cbda` |
|
||||||
|
| `/scheduling/public/:requestId/:token` | Scheduling | Public signed token | Public participation | Scheduling #8 complete in `c17cbda` |
|
||||||
|
| `/search` | Search | `search:result:read` | Keyboard-first global/context overlay and full results fallback | Search pattern migration complete in [Search #4](https://git.add-ideas.de/GovOPlaN/govoplan-search/issues/4); durable evidence in `govoplan-search/docs/INTERFACE_PATTERN_MIGRATION.md` |
|
||||||
|
| `/templates` | Templates | Template read/write/publish/render/admin | Governed library, immutable-revision editor, compatibility preview, and render evidence | Templates pattern migration complete in [Templates #5](https://git.add-ideas.de/GovOPlaN/govoplan-templates/issues/5), commit `72fafa2` |
|
||||||
|
| `/voting` | Voting | `voting:ballot:read` | Governed ballot workspace | Voting pattern migration complete in [Voting #1](https://git.add-ideas.de/GovOPlaN/govoplan-voting/issues/1), commit `2625990` |
|
||||||
|
| `/workflow` | Workflow | Definition read or instance admin | Native BPMN editor, governed revision actions and execution evidence | Workflow pattern migration complete in [Workflow #15](https://git.add-ideas.de/GovOPlaN/govoplan-workflow/issues/15); durable evidence in `govoplan-workflow/docs/INTERFACE_PATTERN_MIGRATION.md` |
|
||||||
|
|
||||||
|
## Final Module Closure Evidence
|
||||||
|
|
||||||
|
The final five module-owned work packages complete the 2026-08-03 rollout
|
||||||
|
snapshot. Their module documents are the durable detailed inventories; the
|
||||||
|
table below records the cross-product closure evidence.
|
||||||
|
|
||||||
|
| Owner | Dominant archetype and consequential boundary | Focused evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Workflow | Definition list-detail plus specialized native BPMN editor; save/activate/archive/delete/reset and instance transitions remain revisioned, confirmed, and Engine-owned | Shared dialogs/status/alerts/help, dirty-navigation guard, keyboard palette insertion, edge inspector alternative, responsive/reduced-motion contract, TypeScript and focused structure test |
|
||||||
|
| Search | Focus-contained global/context overlay plus URL-stable full results; filters only narrow permission-aware source results | F3/Ctrl/Cmd+K, listbox keyboard navigation, provider-partial diagnostics, shared controls/help, narrow layout and focused overlay/interface tests |
|
||||||
|
| Reporting | Three-region governed analytical workspace; runs, schedules, exports and publications retain purpose, permission, source and policy provenance | Shared grid/dialog/status/help, keyboard-explainable Run blockers, responsive task order, provider/semantic backend tests and focused interface test |
|
||||||
|
| Projects | Revisioned list-detail planning workspace; visibility and saves are ACL/OCC-governed and retain a change reason | Shared dialog/status/help/field labels, save errors attached to the editor, semantic list controls, responsive/focus contract and focused interface test |
|
||||||
|
| Portal | Explained service directory and exact-revision provider handoff; Portal never owns the launched case/form/workflow effect | Shared status/alert/toggle/help/blocker controls, stable disabled Open action with actor/action/destination, guarded navigation, responsive layout and focused interface test |
|
||||||
|
|
||||||
|
Future WebUI modules and newly added routes are not grandfathered by this
|
||||||
|
snapshot. They must meet the same surface definition of done in their owning
|
||||||
|
feature issue and pass the source/runtime inventory gates; they do not reopen
|
||||||
|
this finite migration program unless the pattern contract itself changes.
|
||||||
|
|
||||||
## Manifest And Runtime Route Alignment
|
## Manifest And Runtime Route Alignment
|
||||||
|
|
||||||
@@ -85,28 +134,27 @@ loading reason about the configured interface without executing module UI code.
|
|||||||
is recorded here as an evidence gap; this inventory does not infer whether each
|
is recorded here as an evidence gap; this inventory does not infer whether each
|
||||||
gap is intentional.
|
gap is intentional.
|
||||||
|
|
||||||
| Module | `module.ts` routes | Backend manifest frontend routes | Backend nav alignment | Result |
|
The generated comparison is aligned for all authenticated canonical routes.
|
||||||
| --- | --- | --- | --- | --- |
|
Two deliberate exceptions remain visible:
|
||||||
| Access | `/admin` | `/admin` | Aligned | Described |
|
|
||||||
| Addresses | `/address-book` | `/address-book` | Aligned | Described |
|
|
||||||
| Admin | No direct route; `admin.sections` | None | Not applicable | Composed surface |
|
|
||||||
| Audit | No direct route; `admin.sections` | None | Not applicable | Composed surface |
|
|
||||||
| Calendar | `/calendar` | None | `/calendar` nav exists | Metadata gap |
|
|
||||||
| Campaign | Five routes | None | Four top-level nav items exist | Metadata gap; wildcard resource route is also undescribed |
|
|
||||||
| Dashboard | `/dashboard` | `/dashboard` | Aligned | Described |
|
|
||||||
| Docs | `/docs` | `/docs` | Aligned | Described |
|
|
||||||
| Files | `/files` | None | `/files` nav exists | Metadata gap |
|
|
||||||
| IDM | `/idm` | `/idm` | Aligned | Described |
|
|
||||||
| Mail | `/mail` | None | `/mail` nav exists | Metadata gap |
|
|
||||||
| Notifications | `/notifications` | No frontend metadata | No nav item | Metadata and discovery gap |
|
|
||||||
| Ops | `/ops` | `/ops` | Aligned | Described |
|
|
||||||
| Organizations | `/organizations` | `/organizations` | Aligned | Described |
|
|
||||||
| Policy | No direct route; `admin.sections` | None | Not applicable | Composed surface |
|
|
||||||
| Scheduling | `/scheduling` | None | `/scheduling` nav exists | Metadata gap |
|
|
||||||
|
|
||||||
Before a release claims a complete configured-system route inventory, add a
|
- Campaign contributes `/operator` as a compatibility redirect for saved View
|
||||||
contract check or explicit exceptions so executable routes and manifest
|
projections; its canonical and manifest-declared destination is
|
||||||
metadata cannot silently diverge.
|
`/campaigns/queue`.
|
||||||
|
- Scheduling contributes `/scheduling/public/:requestId/:token` through the
|
||||||
|
separate `publicRoutes` contract. Authenticated manifest routes intentionally
|
||||||
|
do not describe public signed-token entry points yet.
|
||||||
|
|
||||||
|
Admin, Audit, Policy, Tenancy, and Views contribute composed administration or
|
||||||
|
settings surfaces rather than direct routes. Their migration issues are
|
||||||
|
[Admin #8](https://git.add-ideas.de/GovOPlaN/govoplan-admin/issues/8),
|
||||||
|
[Audit #8](https://git.add-ideas.de/GovOPlaN/govoplan-audit/issues/8),
|
||||||
|
[Policy #11](https://git.add-ideas.de/GovOPlaN/govoplan-policy/issues/11),
|
||||||
|
[Tenancy #6](https://git.add-ideas.de/GovOPlaN/govoplan-tenancy/issues/6), and
|
||||||
|
[Views #2](https://git.add-ideas.de/GovOPlaN/govoplan-views/issues/2).
|
||||||
|
|
||||||
|
Release evidence must continue to run the generated inventory and manifest
|
||||||
|
shape checks so new executable routes, public routes, aliases, and composed
|
||||||
|
surfaces cannot silently diverge from their declared metadata.
|
||||||
|
|
||||||
## Composed Surfaces And Extension Points
|
## Composed Surfaces And Extension Points
|
||||||
|
|
||||||
@@ -115,30 +163,113 @@ enabled and the actor passes the declared filters.
|
|||||||
|
|
||||||
| Host surface | Contributor and evidence | Contributed regions/actions | Pattern implication | Audit |
|
| Host surface | Contributor and evidence | Contributed regions/actions | Pattern implication | Audit |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| `/admin` | Access host (`AdminPage`) | System tenants/users/roles, tenant users/groups/roles/API keys/settings, function-role mappings, user/group mail and file connector scopes | One stable admin information architecture must contain both host-owned and contributed sections | Unreviewed; P1 Core #225 |
|
| `/admin` | Access host (`AdminPage`) | System tenants/users/roles, tenant users/groups/roles/API keys/settings, function-role mappings, user/group mail and file connector scopes | One stable admin information architecture must contain both host-owned and contributed sections | Pattern migration, contextual help, explained permission/protection states, optional-module blockers, localization, and focused evidence complete in Access #19 (`1409dbf`); Core #225 shared host contract complete |
|
||||||
| `/admin` | `govoplan-admin` `admin.sections` | Overview; system settings; configuration changes; configuration packages; role/group templates; module management | Configuration, guided operations, review/preflight, consequence | In progress under Core #225; surface-level evidence still needed |
|
| `/admin` | `govoplan-admin` `admin.sections` | Overview; system settings; configuration changes; configuration packages; role/group templates; module management | Configuration, guided operations, review/preflight, consequence | Pattern migration, contextual help, explained permission/protection/applicability states, guarded consequential actions, localization, and focused evidence complete in Admin #8 (`d428f33`) |
|
||||||
| `/admin` | `govoplan-audit` `admin.sections` | System audit; tenant audit | Evidence/provenance and reporting | Unreviewed |
|
| `/admin` | `govoplan-tenancy` `admin.sections` | System tenant registry and active-tenant settings | Administration directory, effective configuration, lifecycle consequence | Pattern migration, contextual help, explained permission/lifecycle/system-policy states, dirty-state guards, localization, and focused evidence complete in Tenancy #6 (`e76fe16`) |
|
||||||
| `/admin` | `govoplan-files` `admin.sections` and `files.connectors` | System and tenant file connections plus scoped connector managers used by Access | Adaptive configuration, discovery/test, policy and credentials | First migration family in Core #225; verification incomplete in this inventory |
|
| `/admin` | `govoplan-audit` `admin.sections` | System audit; tenant audit | Evidence/provenance and reporting | Pattern migration, localized evidence projection, contextual help, and focused tests complete in Audit #8 (`6d3fcc1`) |
|
||||||
| `/admin` | `govoplan-organizations` `admin.sections` | Tenant organization settings | Configuration/list-detail | Unreviewed |
|
| `/admin` | `govoplan-files` `admin.sections` and `files.connectors` | System and tenant file connections plus scoped connector managers used by Access | Adaptive configuration, discovery/test, policy and credentials | Pattern migration, contextual help, blocker explanations and focused evidence complete in Files #42 (`d8ae506`) |
|
||||||
| `/admin` | `govoplan-policy` `admin.sections` | System, tenant, group, and user retention | Effective value, source/provenance, consequential configuration | Unreviewed; Core #225 phase 4 |
|
| `/admin` | `govoplan-organizations` `admin.sections` | Tenant organization settings | Configuration/list-detail | Pattern migration, tenant-owned provenance, contextual help, guarded settings/editor drafts, explained permission states, localization and focused evidence complete in Organizations #7 (`97acfcb`) |
|
||||||
| `/admin` and `/settings` | `govoplan-mail` `mail.profiles` | System/tenant/group/user mail profile and policy managers | Same server/credential/policy grammar as file connectors | Unreviewed; Core #225 mail migration |
|
| `/admin` | `govoplan-policy` `admin.sections` | System, tenant, group, and user retention | Effective value, source/provenance, consequential configuration | Pattern migration complete in Policy #11 (`f964ed7`) with Core editor contract `fa32cca` |
|
||||||
| `/settings` | Core host | Profile; interface; workspace; local connection | Personal configuration with adaptive forms and immediate feedback | Unreviewed |
|
| `/admin` and `/settings` | `govoplan-mail` `mail.profiles` | System/tenant/group/user mail profile and policy managers | Same server/credential/policy grammar as file connectors | Pattern migration, contextual help, policy/target/permission blockers and focused evidence complete in Mail #20 (`7844d9c`; shared test-reason contract Core `2d0551a`) |
|
||||||
| `/settings` | Files and Mail named capabilities | User-scoped file connections and mail profiles/policy | Optional integration regions disappear cleanly when capability absent | Unreviewed |
|
| `/settings` | Core host | Profile; interface; workspace; local connection | Personal configuration with adaptive forms and immediate feedback | Pattern migration complete in Core #225 (`fa32cca`) |
|
||||||
| `/settings` | `govoplan-notifications` `settings.sections` | Notification preferences | Personal configuration | Unreviewed |
|
| `/settings` | Files and Mail named capabilities | User-scoped file connections and mail profiles/policy | Optional integration regions disappear cleanly when capability absent | Files #42, Mail #20 and Core #225 complete |
|
||||||
| `/dashboard` | Dashboard host and `dashboard.widgets` | Installed-modules widget; Ops health widget when Ops contributes it | Widget ordering, staleness, permissions, destination behavior | Unreviewed |
|
| `/admin` and `/settings` | `govoplan-views` `admin.sections`, `settings.sections`, and `views.runtime` | System/tenant definition and assignment editors, personal/group editors, global selector | Versioned presentation projection with inheritance, lockout safeguards, optional directory targets, and no authorization effect | Pattern migration, contextual help, localized selector/editor, guarded drafts, explained inherited/permission/capability states, and focused evidence complete in Views #2 (`c125f33`) |
|
||||||
| `/organizations` | IDM `organizations.functionActions` | Action leading to assignment view filtered by IDM scopes | Cross-module context action through explicit capability | Unreviewed |
|
| `/settings` | `govoplan-notifications` `settings.sections` | Notification preferences | Personal configuration | Pattern migration, contextual help, permission/target explanation, typed toggles and focused evidence complete in Notifications #4 (`ad6a31f`) |
|
||||||
| Campaign attachments/import | Files `files.fileExplorer` | Folder tree, managed chooser, file listing/pattern resolution/sharing | Optional domain composition without sibling-private imports | Pilot audit under Campaign #74 |
|
| `/dashboard` | Dashboard host and `dashboard.widgets` | Installed-modules widget; Ops health widget when Ops contributes it | Widget ordering, staleness, permissions, destination behavior | Pattern migration, view-aware composition, keyboard/drag alternatives, responsive packing, module filtering and focused evidence complete in Dashboard #3 (`da3947f`) |
|
||||||
| Campaign review/send | Mail runtime `mail.devMailbox` | Mock-mail verification when backend advertises runtime capability | Optional review stage with unavailable/optional states | Pilot audit under Campaign #63/#62 |
|
| `/organizations` | IDM `organizations.functionActions` | Action leading to assignment view filtered by IDM scopes | Cross-module context action through explicit capability | IDM pattern migration complete in IDM #12 (`d864317`) |
|
||||||
|
| Campaign attachments/import | Files `files.fileExplorer` | Folder tree, managed chooser, file listing/pattern resolution/sharing | Optional domain composition without sibling-private imports | Campaign #74 pilot complete; watched-folder and duplicate-attachment product policy remain independent Campaign #60/#61 features |
|
||||||
|
| Campaign review/send | Mail runtime `mail.devMailbox` | Mock-mail verification when backend advertises runtime capability | Optional review stage with unavailable/optional states | Explicit intervention and review-progress vocabulary delivered in Campaign #63; send modes/progress delivered in #62/#79 |
|
||||||
|
|
||||||
Other named capability exports (`files.connectors`, `organizations.functionPicker`,
|
Other named capability exports (`files.connectors`, `organizations.functionPicker`,
|
||||||
and mail profile validation) are contracts consumed inside the composed surfaces
|
and mail profile validation) are contracts consumed inside the composed surfaces
|
||||||
above; they are not independent routes.
|
above; they are not independent routes.
|
||||||
|
|
||||||
|
## Core Configuration Surface Map
|
||||||
|
|
||||||
|
Core #225 now supplies and verifies the platform-owned configuration contract.
|
||||||
|
The durable Core inventory is
|
||||||
|
`govoplan-core/docs/INTERFACE_PATTERN_MIGRATION.md`.
|
||||||
|
|
||||||
|
| Surface / code evidence | Primary task | Target pattern | Material consequence/state | Completion evidence |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `/settings` (`SettingsPage`) | Change personal profile, interface/workspace preferences, or local development connection | Two-zone typed settings workspace | Changes are user-scoped; save and test actions distinguish clean, busy, and active states | Contextual help, unsaved guard, typed controls and keyboard-explainable disabled actions in Core `fa32cca` |
|
||||||
|
| Reusable credentials (`CredentialEnvelopeManager`) | Compare and configure scoped reusable authentication material | Repeated administration plus adaptive create/edit | Secret values are write-only; permission and missing-owner states block mutation explicitly; deletion can break dependent connections | Actionable blocker, stable row actions, typed references, unsaved guard and shared destructive confirmation |
|
||||||
|
| Retention (`RetentionPolicyManagement`) | Inspect effective retention and narrow permitted local values | Effective-policy editor | Parent locks, source paths and write authority control whether sensitive evidence can be retained | Typed narrowing controls, source-path help, lock/target/permission blockers and clean/loading/save reasons |
|
||||||
|
| Shared configuration primitives | Compose module-owned settings without sibling-private imports | Platform behavior contract | Consequence, focus, help, async, confirmation and permission semantics remain consistent | Core component suites, 121 module-system tests and full-product type/build/bundle gates |
|
||||||
|
|
||||||
|
No primary Core configuration flow requires raw JSON. Expert JSON remains
|
||||||
|
limited to diagnostics, interchange, conflict evidence, or read-only inspection.
|
||||||
|
|
||||||
|
## Policy Surface Map
|
||||||
|
|
||||||
|
Policy #11 verifies the four composed retention sections. The durable
|
||||||
|
module-level inventory is
|
||||||
|
`govoplan-policy/docs/INTERFACE_PATTERN_MIGRATION.md`.
|
||||||
|
|
||||||
|
| Surface / code evidence | Primary task | Target pattern | Material consequence/state | Completion evidence |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| System retention | Set the instance ceiling and run retention | Effective-policy editor plus destructive operation | An applied run can irreversibly redact/delete retained content; dry-run and applied evidence remain distinct | Core source-path/lock contract, permission and busy reasons, shared confirmation, typed/filterable outcome grid and audit-oriented wording |
|
||||||
|
| Tenant retention | Narrow the inherited system ceiling | Effective-policy editor | Tenant policy cannot silently loosen its parent | Core typed controls, effective path and parent-lock explanation |
|
||||||
|
| Group and user retention | Select an authorized target and narrow inherited policy | Targeted effective-policy editor | Selection exposes only bounded account/group labels; no retained content is returned | Delta-backed target loading, retry, missing-target blocker and responsive shared admin composition |
|
||||||
|
|
||||||
|
Automated evidence for Policy `f964ed7` comprises 50 backend/manifest tests,
|
||||||
|
the Policy interface structural gate, 65 manifest-shape checks, and the
|
||||||
|
full-product TypeScript/Vite build with structural localization, theme and
|
||||||
|
bundle-budget gates. Policy uses no sibling-private imports.
|
||||||
|
|
||||||
|
## Files Surface Map
|
||||||
|
|
||||||
|
Files #42 classifies and verifies the complete Files-owned route and composition
|
||||||
|
boundary. The durable module-level inventory is
|
||||||
|
`govoplan-files/docs/INTERFACE_PATTERN_MIGRATION.md`.
|
||||||
|
|
||||||
|
| Surface / code evidence | Primary task | Target pattern | Material consequence/state | Completion evidence |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `/files` (`FilesPage`) | Browse spaces/folders and repeatedly act on current content | Full-height directory/explorer | Navigation is low consequence; upload, synchronize, move, copy and share are medium; delete is high | Stable two-pane composition, contextual help, selection/permission/state-specific disabled reasons, shared confirmation and responsive collapse |
|
||||||
|
| Upload/archive, transfer, rename and connector-import dialogs | Supply, validate and review one bounded change | Adaptive create/edit or guided import | Writes managed content and may resolve conflicts or import untrusted bytes | Shared dialogs/drop zone, bounded archive preflight, conflict review, explicit confirmation and no browser-native confirmation |
|
||||||
|
| Share/access explanation | Inspect or change who can use a resource | Review/decision | Grants can disclose content; delete/revoke changes access | Shared access explanation, action components and destructive confirmation; backend redaction remains authoritative |
|
||||||
|
| File connector tree and connection/credential dialogs | Compare and configure external endpoints and reusable credentials | Administration plus adaptive create/edit | Endpoint, secret and capability changes can enable remote access | Shared connection tree/forms/advanced panel, endpoint discovery and login test, unsaved-change guard, read-only deployment provenance and actionable disabled reasons |
|
||||||
|
| Connector policy card | Narrow effective connector use | Effective-policy editor | Inherited deny/allow rules affect lower scopes | Typed selectors, deny-precedence warning, effective sources, contextual admin help and permission blocker |
|
||||||
|
| `files.widget.spaces` | See available spaces and enter Files | Dashboard widget | Space/provider names remain permission-filtered | Shared loading, alert and status components; bounded configuration and refresh |
|
||||||
|
| `files.fileExplorer` capability | Select a governed managed snapshot for another module | Directory chooser | Exact file/version becomes another module's governed input | Capability-only composition, no sibling-private import, stable chooser/confirmation and exact snapshot evidence |
|
||||||
|
|
||||||
|
Automated evidence for commit `d8ae506` comprises 104 Files backend tests,
|
||||||
|
three focused Files WebUI structure tests, the full-product TypeScript/Vite
|
||||||
|
build, structural localization audit, theme contract and bundle budget. Shared
|
||||||
|
Dialog and disabled-tooltip behavior provide focus entry/return and
|
||||||
|
keyboard-reachable explanations; responsive source order is guarded at 1050 px
|
||||||
|
and 760 px. Secrets are not returned to the WebUI, and JSON remains only an
|
||||||
|
advanced provider-compatibility escape hatch rather than the primary editor.
|
||||||
|
|
||||||
|
## Mail Surface Map
|
||||||
|
|
||||||
|
Mail #20 classifies and verifies the complete Mail-owned route and composition
|
||||||
|
boundary. The durable module-level inventory is
|
||||||
|
`govoplan-mail/docs/INTERFACE_PATTERN_MIGRATION.md`.
|
||||||
|
|
||||||
|
| Surface / code evidence | Primary task | Target pattern | Material consequence/state | Completion evidence |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| `/mail` (`MailboxPage`) | Browse an authorized provider mailbox without changing it | Full-height directory/explorer | Message metadata and content are private; every provider read is bounded and non-mutating | Stable three-pane composition, contextual help, explicit no-profile blocker, refresh reasons, keyboard rows, paging and responsive collapse |
|
||||||
|
| Mail profile tree and profile/server/credential dialogs | Compare and configure reusable transport identities | Administration plus guided/adaptive create/edit | Endpoint and credential changes can enable external effects | Shared connection tree/dialog/stage rail/forms, focused hierarchy editors, unsaved guard, connection tests, permission/target blockers and disabled-save reasons |
|
||||||
|
| Mail policy card | Narrow profile visibility, lower-scope definitions and transport/address patterns | Effective-policy editor | Inherited allow/deny rules affect delivery and lower scopes | Typed selectors and controls, effective source path, lock/read-only blocker, dirty-save state and contextual admin help |
|
||||||
|
| `/mail/bounces` watcher table | Configure and explicitly scan bounded IMAP evidence sources | Operational administration | Provider access changes durable source cursors and evidence | Shared grid/status/loading/alerts, actionable no-profile and busy states, field help and stable row actions |
|
||||||
|
| `/mail/bounces` observations and watcher removal | Review sanitized delivery outcomes or stop future scans | Evidence/reporting plus destructive confirmation | Recipient diagnostics are sensitive; watcher removal retains existing evidence | Bounded sanitized rows and shared confirmation with retained-evidence consequence |
|
||||||
|
| `mail.profiles` and reference-selector capabilities | Select/validate Mail-owned transport from another module | Governed capability composition | A selected identity can perform external effects | Stable references, Mail-owned authorization/secret resolution, no sibling-private imports and clean optional absence |
|
||||||
|
|
||||||
|
Automated evidence for Mail commit `7844d9c` and Core commit `2d0551a`
|
||||||
|
comprises 114 Mail backend tests, Mail's focused UI/model/structure suite, the
|
||||||
|
Core shared mail-component suite, 65 manifest-shape checks and the full-product
|
||||||
|
TypeScript/Vite build with structural localization, theme and bundle-budget
|
||||||
|
gates. Shared Dialog and disabled-tooltip behavior provides focus containment,
|
||||||
|
return and keyboard-reachable explanations. Responsive source order is guarded
|
||||||
|
at 1250 px, 900 px and 760 px. Passwords remain write-only, mailbox responses
|
||||||
|
are bounded, and bounce evidence excludes raw provider messages.
|
||||||
|
|
||||||
## Campaign Pilot Surface Map
|
## Campaign Pilot Surface Map
|
||||||
|
|
||||||
Campaign is detailed first because it exercises almost every archetype. The
|
Campaign is detailed first because it exercises almost every archetype. The
|
||||||
recipient-data editor is now consolidated into the `recipients` section on
|
recipient-data editor is now consolidated into the `recipients` section on
|
||||||
remote `main`; [Campaign #67](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/67)
|
remote `main`; [Campaign #67](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/67)
|
||||||
records the accepted and verified integration boundary.
|
records the accepted and verified integration boundary.
|
||||||
|
|
||||||
Campaign already consumes core primitives including `ModuleSubnav`, `Card`,
|
Campaign already consumes core primitives including `ModuleSubnav`, `Card`,
|
||||||
@@ -150,75 +281,77 @@ prove that the composition or states satisfy the pattern.
|
|||||||
|
|
||||||
| Surface / code evidence | Primary task | Target pattern | Material consequence/state | Known issue / rollout |
|
| Surface / code evidence | Primary task | Target pattern | Material consequence/state | Known issue / rollout |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| Campaign list (`CampaignListPage`) | Find, compare, create, open | List-detail entry | Campaign lifecycle/status and creation | Audit in [#74](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/74); guided entry [#35](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/35) |
|
| Campaign list (`CampaignListPage`) | Find, compare, create, open | List-detail entry | Campaign lifecycle/status and creation | #74 and guided entry [#35](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/35) complete |
|
||||||
| Overview (`CampaignOverviewPage`) | Understand/edit campaign identity, version, access, lifecycle | Object overview plus adaptive edit | Lock/archive/delete/access changes need real consequence and reversibility wording | #74 remaining audit |
|
| Overview (`CampaignOverviewPage`) | Understand/edit campaign identity, version, access, lifecycle | Object overview plus adaptive edit | Lock/archive/delete/access changes expose consequence, reversibility, owner/access and lifecycle evidence | #74 complete; lifecycle policy is independently extended in Campaign #26 |
|
||||||
| Fields (`CampaignFieldsPage`) | Define recipient/template field schema | Structured editor | Schema changes can invalidate recipient/template data | #74 audit |
|
| Fields (`CampaignFieldsPage`) | Define recipient/template field schema | Structured editor | Schema changes can invalidate recipient/template data | #74 complete |
|
||||||
| Attachments/files (`AttachmentsDataPage`, `AttachmentRulesOverlay`) | Select sources and attachment/ZIP rules | Directory chooser plus adaptive rule editor | Missing or mismatched files affect built messages | #74; attachment-detail [#59](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/59) |
|
| Attachments/files (`AttachmentsDataPage`, `AttachmentRulesOverlay`) | Select sources and attachment/ZIP rules | Directory chooser plus adaptive rule editor | Missing or mismatched files affect built messages | #74 and attachment-detail [#59](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/59) complete |
|
||||||
| Recipients (`RecipientDataPage`) | Select/import/map/edit recipients, address fields and per-recipient values/files | Import/mapping plus list-detail editor | Personal data, validation, bulk activation, file links | Consolidated editor delivered in #67; #74 remaining audit and guided entry #35 |
|
| Recipients (`RecipientDataPage`) | Select/import/map/edit recipients, address fields and per-recipient values/files | Import/mapping plus list-detail editor | Personal data, validation, bulk activation, file links | Consolidated editor #67, guided entry #35 and #74 audit complete; independent bulk action #68 remains product scope |
|
||||||
| Template (`TemplateDataPage`, placeholder/expression dialogs) | Author subject/body and preview substitutions | Adaptive editor plus stable preview | Generated communication content and unresolved expressions | #74; stable overlay [#73](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/73) |
|
| Template (`TemplateDataPage`, placeholder/expression dialogs) | Author subject/body and preview substitutions | Adaptive editor plus stable preview | Generated communication content and unresolved expressions | #74 and stable overlay [#73](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/73) complete |
|
||||||
| Mail settings (`MailSettingsPage` settings view) | Select/configure campaign mail transport | Adaptive configuration | Credentials, SMTP/IMAP destinations, test outcomes | #74; align with Core #225 mail pattern |
|
| Mail settings (`MailSettingsPage` settings view) | Select/configure campaign mail transport | Adaptive configuration | Credentials, SMTP/IMAP destinations, test outcomes | #74 and Core #225 shared mail pattern complete; final credential hierarchy remains Mail #10 |
|
||||||
| Campaign settings (`GlobalSettingsPage` settings view) | Configure campaign behavior | Adaptive configuration | Can alter validation/build/send behavior | #74 audit |
|
| Campaign settings (`GlobalSettingsPage` settings view) | Configure campaign behavior | Adaptive configuration | Can alter validation/build/send behavior | #74 complete |
|
||||||
| Mail policy (`MailSettingsPage` policy view) | Inspect/override effective mail policy | Effective policy/provenance editor | Inheritance and locks affect allowed delivery | #74; Core #225 policy pattern |
|
| Mail policy (`MailSettingsPage` policy view) | Inspect/override effective mail policy | Effective policy/provenance editor | Inheritance and locks affect allowed delivery | #74 and Core #225 effective-policy pattern complete |
|
||||||
| Campaign policy (`GlobalSettingsPage` policy view) | Inspect/override campaign policy | Effective policy/provenance editor | Inheritance, actor authority, and blocked edits | #74; Core #225 policy pattern |
|
| Campaign policy (`GlobalSettingsPage` policy view) | Inspect/override campaign policy | Effective policy/provenance editor | Inheritance, actor authority, and blocked edits | #74 and Core #225 effective-policy pattern complete |
|
||||||
| Review/send (`ReviewSendPage`) | Validate, build, mock-test, confirm/send, inspect results | Guided review/decision plus durable progress | External communication, bounded synchronous execution, persisted queue mode, partial effects, retries, evidence | Bounded synchronous and explicit/persisted queued modes delivered in [#62](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/62) and [#79](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/79); [#63](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/63) wording and #74 audit remain |
|
| Review/send (`ReviewSendPage`) | Validate, build, mock-test, confirm/send, inspect results | Guided review/decision plus durable progress | External communication, bounded synchronous execution, persisted queue mode, partial effects, retries, evidence | Interventions #63, send/progress #62/#79 and #74 wording/accessibility audit complete |
|
||||||
| Message and attachment detail overlays | Inspect one built/mock message and its attachment links | Stable detail/review dialog | Personal data, exact outbound content, reviewed state | Delivered and verified in [#59](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/59) and [#73](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/73) |
|
| Message and attachment detail overlays | Inspect one built/mock message and its attachment links | Stable detail/review dialog | Personal data, exact outbound content, reviewed state | Delivered and verified in [#59](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/59) and [#73](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/73) |
|
||||||
| Campaign report (`CampaignReportPage`) | Filter and inspect delivery outcomes | Reporting/list-detail | Partial, failed, explicitly excluded/skipped, SMTP/IMAP outcomes and retries | Server-owned filtering and counts delivered in [#65](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/65) with the full-result DataGrid contract from [Core #263](https://git.add-ideas.de/add-ideas/govoplan-core/issues/263); excluded semantics in [#66](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/66) |
|
| Campaign report (`CampaignReportPage`) | Filter and inspect delivery outcomes | Reporting/list-detail | Partial, failed, explicitly excluded/skipped, SMTP/IMAP outcomes and retries | Server-owned filtering and counts delivered in [#65](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/65) with the full-result DataGrid contract from [Core #263](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/263); excluded semantics in [#66](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/66) |
|
||||||
| Audit (`CampaignAuditPage`) | Inspect campaign evidence/history | Provenance timeline/report | Actor/action/effect trace | #74 audit |
|
| Audit (`CampaignAuditPage`) | Reach campaign evidence/history | Explained provenance handoff | Campaign emits platform evidence; Audit owns reading, retention and bundles | #74 complete as an explicit Audit handoff; object-scoped projection may follow Audit #3 without a sibling-private import |
|
||||||
| JSON (`CampaignJsonView`) | Inspect expert representation | Advanced diagnostics/reference | Raw data may contain personal/configuration values; not a primary editor | #74 privacy/redaction audit |
|
| JSON (`CampaignJsonView`) | Inspect/download expert representation | Advanced diagnostics/reference | Full authorized configuration may contain personal data but no inline transport secrets | #74 privacy audit complete with explicit sensitivity warning and campaign-read boundary |
|
||||||
| Create wizard (`CreateWizard`) | Seed a campaign through basics, sender, fields, recipients, template, attachments, review, send | Guided setup | Current steps mix creation and later consequential delivery; completion semantics need audit | Guided first campaign [#35](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/35) |
|
| Create wizard (`CreateWizard`) | Seed a campaign through basics, sender, fields, recipients, template, attachments, review, send | Guided setup | Current steps mix creation and later consequential delivery; completion semantics need audit | Guided first campaign [#35](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/35) |
|
||||||
| Review/send wizard routes | Alternate guided review/send shells | Guided review | Tracked routes exist; implementation relationship to `ReviewSendPage` must be established, not guessed | #74 inventory decision |
|
| Review/send wizard routes | Focus the canonical review or send stage | Guided review | Thin wrappers render the same `ReviewSendPage` with a stable initial stage; no parallel workflow state exists | #74 inventory decision complete |
|
||||||
| Operator queue (`OperatorQueuePage`) | Monitor jobs and intervene | Monitoring/work queue | Campaign/version/job identity, historical active-version discovery, fixed action positions, authority-aware disabled states, exact non-overlapping queue counts, server-paged jobs, bounded refresh, retry/queue/reconcile per version, campaign-wide pause/resume/cancel, and leave/return progress | Durable operator controls delivered in [#78](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/78); #74 wording/accessibility audit remains |
|
| Operator queue (`OperatorQueuePage`) | Monitor jobs and intervene | Monitoring/work queue | Campaign/version/job identity, historical active-version discovery, fixed action positions, authority-aware disabled states, exact non-overlapping queue counts, server-paged jobs, bounded refresh, retry/queue/reconcile per version, campaign-wide pause/resume/cancel, and leave/return progress | Durable controls #78 and #74 wording/accessibility audit complete |
|
||||||
| Aggregate reports (`AggregateReportsPage`) | Compare cross-campaign delivery outcomes | Privacy-preserving aggregate reporting | Tenant/campaign ACL, deployment/tenant small-cell policy, complementary and overlapping-cell suppression, explicit denominator, and no recipient detail/diagnostics/export/drill-down | Separate aggregate-reader surface delivered in [#80](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/80); not parity with the permission-gated per-campaign detail report |
|
| Aggregate reports (`AggregateReportsPage`) | Compare cross-campaign delivery outcomes | Privacy-preserving aggregate reporting | Tenant/campaign ACL, deployment/tenant small-cell policy, complementary and overlapping-cell suppression, explicit denominator, and no recipient detail/diagnostics/export/drill-down | Separate aggregate-reader surface delivered in [#80](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/80); not parity with the permission-gated per-campaign detail report |
|
||||||
| Templates route (`TemplatesPage`) | Browse template records | Directory/list-detail | Template availability and later generated outputs | #74 audit; verify missing route guard intent |
|
|
||||||
|
|
||||||
The five review stages currently named in code are `Validate and inspect`,
|
The five review stages currently named in code are `Validate and inspect`,
|
||||||
`Build and review`, `Mock send and verify`, `Confirm and send`, and `Delivery
|
`Build and review`, `Mock send and verify`, `Confirm and send`, and `Delivery
|
||||||
results`. Campaign #63 owns the intervention and status vocabulary; Workflow is
|
results`. Campaign #63 owns the intervention and status vocabulary; Workflow is
|
||||||
not required to define or implement it.
|
not required to define or implement it.
|
||||||
|
|
||||||
## Repositories Without A WebUI Route Contribution
|
## Repositories Without A WebUI Package
|
||||||
|
|
||||||
The following local repositories contain a backend manifest but no
|
The generated manifest snapshot reports no WebUI package for:
|
||||||
`webui/src/module.ts` at this snapshot:
|
|
||||||
|
|
||||||
`govoplan-approvals`, `govoplan-assets`, `govoplan-booking`,
|
`govoplan-assets`, `govoplan-booking`, `govoplan-certificates`,
|
||||||
`govoplan-certificates`, `govoplan-committee`, `govoplan-consultation`,
|
`govoplan-connectors`, `govoplan-consultation`, `govoplan-contracts`,
|
||||||
`govoplan-contracts`, `govoplan-dist-lists`, `govoplan-evaluation`,
|
`govoplan-decisions`, `govoplan-encryption`, `govoplan-evaluation`,
|
||||||
`govoplan-facilities`, `govoplan-forms-runtime`, `govoplan-grants`,
|
`govoplan-facilities`, `govoplan-grants`, `govoplan-helpdesk`,
|
||||||
`govoplan-helpdesk`, `govoplan-identity`, `govoplan-inspections`,
|
`govoplan-identity`, `govoplan-identity-trust`, `govoplan-inspections`,
|
||||||
`govoplan-issue-reporting`, `govoplan-learning`, `govoplan-permits`,
|
`govoplan-learning`, `govoplan-mandates`, `govoplan-parties`,
|
||||||
`govoplan-poll`, `govoplan-procurement`, `govoplan-records`,
|
`govoplan-permits`, `govoplan-poll`, `govoplan-procurement`,
|
||||||
`govoplan-resources`, `govoplan-rest`, `govoplan-risk-compliance`,
|
`govoplan-records`, `govoplan-resources`, `govoplan-rest`,
|
||||||
`govoplan-soap`, `govoplan-tenancy`, and `govoplan-transparency`.
|
`govoplan-services`, `govoplan-soap`, `govoplan-tickets`,
|
||||||
|
`govoplan-transparency`, `govoplan-wiki`, and `govoplan-workflow-engine`.
|
||||||
|
|
||||||
This is only negative route evidence. It does not classify the backend module's
|
Tenancy does provide composed administration surfaces despite having no direct
|
||||||
maturity or decide that it needs a WebUI. Connector-only, capability-only, or
|
route. This section is only negative package evidence; connector-only,
|
||||||
backend-only modules may remain intentionally headless.
|
capability-only, runtime-only, and backend-only modules may intentionally remain
|
||||||
|
headless. A new WebUI should be created only for a concrete user task, not to
|
||||||
|
make every module symmetrical.
|
||||||
|
|
||||||
## Rollout Matrix
|
## Rollout Matrix
|
||||||
|
|
||||||
| Order | Scope | Current evidence | Target | Owner / issue | Verification gate | Status |
|
| Order | Scope | Current evidence | Target | Owner / issue | Verification gate | Status |
|
||||||
| --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
| 0 | Product grammar and route inventory | Doctrine, ledger, layout rules, module contract, current route sources | One reconciled pattern language and evidence inventory | Meta [#11](https://git.add-ideas.de/add-ideas/govoplan/issues/11) | Docs links/diff checks; issue/wiki sync after integration | Initial slice in this document |
|
| 0 | Product grammar and route inventory | Doctrine, ledger, layout rules, module contract, current route sources | One reconciled pattern language and evidence inventory | Meta [#11](https://git.add-ideas.de/GovOPlaN/govoplan/issues/11) | Reviewed route/component inventory, module documents, manifest shapes and focused contracts | Complete 2026-08-03 |
|
||||||
| 1 | Campaign baseline integration | Recipient-editor WIP and tracker state have been reconciled with remote `main` | Integrated, testable baseline before migration claims | Campaign #67 and tracker cleanup | Backend and focused WebUI suites; issue evidence | Complete 2026-07-22 |
|
| 1 | Campaign baseline integration | Recipient-editor WIP and tracker state have been reconciled with remote `main` | Integrated, testable baseline before migration claims | Campaign #67 and tracker cleanup | Backend and focused WebUI suites; issue evidence | Complete 2026-07-22 |
|
||||||
| 2 | Campaign previews/details | Stable shared dialog with bounded scrolling and fixed responsive preview workspace | Stable header/body/footer, accessible long-content detail | Campaign #59 and #73 | Review-preview and overlay structure tests | Complete 2026-07-22 |
|
| 2 | Campaign previews/details | Stable shared dialog with bounded scrolling and fixed responsive preview workspace | Stable header/body/footer, accessible long-content detail | Campaign #59 and #73 | Review-preview and overlay structure tests | Complete 2026-07-22 |
|
||||||
| 3 | Campaign review/interventions | Five domain-owned stages with unresolved intervention language | Clear stages, outcomes, blockers, next actor/action, reviewed evidence | Campaign #63 | State matrix behavior/accessibility tests and agreed vocabulary | P1 needs product wording decision |
|
| 3 | Campaign review/interventions | Five domain-owned stages use central blocker and guided-review primitives; validation/build warnings name action, actor, and destination; hard blockers, individual review, and group review remain distinct; reviewed/remaining counts survive reload through build-bound review evidence | Clear stages, outcomes, blockers, next actor/action, reviewed evidence | Campaign #63 | `reviewProgress` state tests, shared-component structure contract, TypeScript build, configured-system help topic, and Campaign documentation tests | Complete 2026-08-03 (`d635f3a`; Core primitives and contextual help `b823a22`) |
|
||||||
| 4 | Campaign send/progress | A hard deployment ceiling bounds synchronous delivery; the selected synchronous, worker-queue, or database-queue mode is explicit and persisted; progress and recovery survive navigation; immediate-send response and audit evidence are allowlisted | Pre-send mode/consequence plus durable leave/return progress, retry and reconciliation without recipient/provider leakage | Campaign #62 and #79 | Boundary/concurrency/preflight, async selection, persisted mode, sanitized response/audit, partial/failure/retry and reload/return tests | Complete 2026-07-22 (`7e16603`, `60efd1c`, `62a6879`, `b0282eb`, `f095a3e`) |
|
| 4 | Campaign send/progress | A hard deployment ceiling bounds synchronous delivery; the selected synchronous, worker-queue, or database-queue mode is explicit and persisted; progress and recovery survive navigation; immediate-send response and audit evidence are allowlisted | Pre-send mode/consequence plus durable leave/return progress, retry and reconciliation without recipient/provider leakage | Campaign #62 and #79 | Boundary/concurrency/preflight, async selection, persisted mode, sanitized response/audit, partial/failure/retry and reload/return tests | Complete 2026-07-22 (`7e16603`, `60efd1c`, `62a6879`, `b0282eb`, `f095a3e`) |
|
||||||
| 5 | Campaign report filtering | Core DataGrid distinguishes client/full-result from server-owned queries; Campaign applies filter/sort/count before pagination and synchronizes count shortcuts with the grid query | One shared server-owned status/list/filter/count model | Campaign #65 and Core #263 | DataGrid contract/build tests plus exact shortcut/query/filter/count and large-result behavior | Complete 2026-07-22 (`e6062fe`, `cece71d`, `aa4ec66`, `4eb651c`) |
|
| 5 | Campaign report filtering | Core DataGrid distinguishes client/full-result from server-owned queries; Campaign applies filter/sort/count before pagination and synchronizes count shortcuts with the grid query | One shared server-owned status/list/filter/count model | Campaign #65 and Core #263 | DataGrid contract/build tests plus exact shortcut/query/filter/count and large-result behavior | Complete 2026-07-22 (`e6062fe`, `cece71d`, `aa4ec66`, `4eb651c`) |
|
||||||
| 6 | Campaign operator recovery | A durable campaign/version queue page exposes historical work, exact non-overlapping state counts, persisted mode, permission-safe controls, server-paged job evidence, bounded refresh and active-state recovery | Fixed-position actions, disabled explanations, leave/return state, version-scoped retry/queue/reconcile and explicit campaign-wide pause/resume/cancel | Campaign #78 | Queue model/structure, historical-version, permission, paging, recovery-control, stale-response and delta tests | Complete 2026-07-22 (`21f3014`, `99d44ee`, `735e874`) |
|
| 6 | Campaign operator recovery | A durable campaign/version queue page exposes historical work, exact non-overlapping state counts, persisted mode, permission-safe controls, server-paged job evidence, bounded refresh and active-state recovery | Fixed-position actions, disabled explanations, leave/return state, version-scoped retry/queue/reconcile and explicit campaign-wide pause/resume/cancel | Campaign #78 | Queue model/structure, historical-version, permission, paging, recovery-control, stale-response and delta tests | Complete 2026-07-22 (`21f3014`, `99d44ee`, `735e874`) |
|
||||||
| 7 | Campaign aggregate reports | A separate aggregate-reader projection and UI expose only policy-suppressed business totals with a stable status domain | Explicit denominator and exclusions, deployment floor plus tenant-strengthened small-cell threshold, complementary and overlapping-cell suppression, no detail/export/diagnostics | Campaign #80 | Aggregate query, cross-metric suppression, route/role/ACL, stable filter and UI structure tests | Complete 2026-07-22 (`06125cc`, `fc36aee`, `8ee87b7`, `ac3329c`, `1225802`) |
|
| 7 | Campaign aggregate reports | A separate aggregate-reader projection and UI expose only policy-suppressed business totals with a stable status domain | Explicit denominator and exclusions, deployment floor plus tenant-strengthened small-cell threshold, complementary and overlapping-cell suppression, no detail/export/diagnostics | Campaign #80 | Aggregate query, cross-metric suppression, route/role/ACL, stable filter and UI structure tests | Complete 2026-07-22 (`06125cc`, `fc36aee`, `8ee87b7`, `ac3329c`, `1225802`) |
|
||||||
| 8 | Campaign excluded outcomes | Excluded build rows become explicit skipped transport outcomes and remain protected from queue/cancel/retry ambiguity | One durable source-to-job-to-report meaning with guarded historical normalization | Campaign #66 | Builder/persistence, migration, query/count, queue-control and report-explanation tests | Complete 2026-07-22 (`7229fb8`) |
|
| 8 | Campaign excluded outcomes | Excluded build rows become explicit skipped transport outcomes and remain protected from queue/cancel/retry ambiguity | One durable source-to-job-to-report meaning with guarded historical normalization | Campaign #66 | Builder/persistence, migration, query/count, queue-control and report-explanation tests | Complete 2026-07-22 (`7229fb8`) |
|
||||||
| 9 | Guided first campaign | Existing wizard routes and ordinary workspace overlap | Task-oriented entry that hands off clearly to normal editing/review | Campaign #35 | First-run flow, resume/back, validation, optional modules, no implicit send | P1 after core pilot patterns stabilize |
|
| 9 | Guided first campaign | Eight-stage creation flow persists current step/draft and hands off to ordinary review/delivery preparation | Task-oriented entry that hands off clearly to normal editing/review | Campaign #35 | First-run flow, resume/back, partial validation, immutable-history and optional-module behavior, no implicit send | Complete 2026-07-30 |
|
||||||
| 10 | Prove/extract generic primitives | Core already exports many primitives; Campaign composition still unreviewed | Extract only contracts with a second consumer or clear platform ownership | Core #225 plus bounded follow-ups | Core behavior/accessibility tests and module-permutation tests | After Campaign proof |
|
| 10 | Prove/extract generic primitives | Shared consequence, focus, help, blocker, unsaved-change, confirmation, connection-tree and effective-policy contracts now have Core and multiple module consumers | Keep Core behavior-only and leave domain composition in owning modules | Core #225 plus bounded follow-ups | Core behavior/accessibility tests and module-permutation tests | Complete 2026-08-03 (`fa32cca`; Files `d8ae506`; Mail `7844d9c`) |
|
||||||
| 11 | Configured-system pattern help | Docs route and classification exist | Role/config-aware pattern and route/field/blocker help | Docs #15 | Topic grouping, audience filtering, stable links/anchors | P1 after initial pattern IDs stabilize |
|
| 11 | Configured-system pattern help | Role/config-aware workflow, reference, pattern, and system topics are projected by Docs; shared route, field, blocker, and action links resolve to configured Docs or the hosted fallback | Stable configured-system guidance without feature-to-Docs imports | Docs #15 | Docs suite, shared component tests, Campaign review tests, 46 module permutations, full-product bundle budget | Complete 2026-08-03 (Docs `abe2f78`; Core `b823a22`; Campaign `d635f3a`) |
|
||||||
| 12 | Admin/configuration family | Phase inventory and connector primitives exist in the ledger | Apply the pattern to files, mail, policy, retention, packages, modules, API keys, settings | Core #225 and module children | Per-surface state/accessibility/consequence evidence | Parallel where independent of Campaign shared decisions |
|
| 12 | Admin/configuration family | Core host/settings/credential/retention contracts, shared primitives, module lifecycle, Files, Mail, Policy, Access, Admin, Tenancy, Views, and Organizations are integrated and verified | Continue the same consequence/provenance grammar only through bounded module-owned migrations | Core #225 and module children | Per-surface state/accessibility/consequence evidence | Core #225 complete `fa32cca`; Access `1409dbf`; Files `d8ae506`; Mail `7844d9c`; Policy `f964ed7`; Admin `d428f33`; Tenancy `e76fe16`; Views `c125f33`; Organizations `97acfcb` |
|
||||||
| 13 | Remaining direct routes | Routes are contributed; most are unreviewed | Per-module bounded audit and migration plan | New module issues derived from this inventory | Applicable definition-of-done gates | P2 after Campaign, not a bulk rewrite |
|
| 13 | Remaining module surfaces | 33 bounded module-owned issues cover every WebUI contributor not already tracked by Campaign #74 or completed Docs #15 | Per-module audit and migration, ordered by user task and consequence rather than a bulk rewrite | Issues linked in the direct-route and composed-surface sections | Module-focused tests, manifest shapes, contextual Docs, and applicable definition-of-done gates | Complete: prior 28 recorded commits plus Workflow #15, Search #4, Reporting #8, Projects #2 and Portal #2 verified 2026-08-03 |
|
||||||
| 14 | Manifest/runtime alignment | Several executable routes are absent from manifest metadata | Declared alignment or explicit validated exception | Core contract issue to create | Automated manifest/module route check and configured Docs verification | Discovery follow-up |
|
| 14 | Manifest/runtime alignment | Authenticated canonical routes align; public signed-token and compatibility routes are explicit exceptions | Stable declarations reconcile with source and any effective runtime module combination | [Meta #25](https://git.add-ideas.de/GovOPlaN/govoplan/issues/25) | Strict duplicate/stale/undeclared declaration CI, per-module digests, and authorized read-only runtime inventory | Complete 2026-08-04 |
|
||||||
|
|
||||||
Workflow/user-story implementation is postponed. It is not on the critical path
|
Workflow remains outside this rollout matrix because it has its own runtime and
|
||||||
for this rollout matrix. Focused views can be specified, manually selected, and
|
editor workstream, not because it is postponed. Focused views can be specified,
|
||||||
tested through core composition contracts; a later workflow step may become one
|
manually selected, and tested through core composition contracts today.
|
||||||
activation source without changing the proven surface patterns.
|
Workflow steps may activate those views through the same contract without
|
||||||
|
changing the proven surface patterns.
|
||||||
|
|
||||||
## Inventory Maintenance
|
## Inventory Maintenance
|
||||||
|
|
||||||
|
|||||||
@@ -114,7 +114,10 @@ For release validation:
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
./.venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-release.txt --python ./.venv/bin/python
|
./.venv/bin/python tools/repo/sync-python-environment.py --requirements requirements-release.txt --python ./.venv/bin/python
|
||||||
|
./.venv/bin/python -m pip install -r requirements-release-tests.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
That install is necessary because the release environment intentionally resolves
|
That install is necessary because the release environment intentionally resolves
|
||||||
tagged package refs, not local editable source trees.
|
tagged package refs, not local editable source trees. The second requirements
|
||||||
|
file contains only the harness needed to execute tests from those immutable
|
||||||
|
source tags; it is not part of the deployable release dependency set.
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
# 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` translates the reviewed Git
|
||||||
|
source refs in `requirements-release.txt` into an exact registry package set.
|
||||||
|
It resolves each version tag to its commit and verifies the package metadata in
|
||||||
|
that tag.
|
||||||
|
|
||||||
|
`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 SHA-256 values and npm integrity
|
||||||
|
values. 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 wheelhouse directly and
|
||||||
|
installs module WebUI tarballs only after matching them to the lock. It publishes
|
||||||
|
the package set, package lock, and hash-locked requirements as release assets.
|
||||||
|
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.
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|
||||||
|
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.
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
# Platform Control Plane And Self-Description
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
GovOPlaN should be able to describe its installed structure without becoming a
|
||||||
|
self-modifying application. The platform model is a declarative control plane:
|
||||||
|
module manifests, UI contributions, schemas, policy provenance, runtime
|
||||||
|
capabilities, and generated source evidence describe what can be configured.
|
||||||
|
Ordinary administrators edit validated data through those contracts; they do
|
||||||
|
not edit Python, TypeScript, routes, or database code from the product UI.
|
||||||
|
|
||||||
|
This distinction provides the requested overview while preserving reviewable
|
||||||
|
releases, module boundaries, migrations, and security controls.
|
||||||
|
|
||||||
|
## Canonical Sources
|
||||||
|
|
||||||
|
| Concern | Canonical source |
|
||||||
|
| --- | --- |
|
||||||
|
| Installed modules and dependency graph | Runtime `ModuleManifest` registry |
|
||||||
|
| Backend routes | Registered FastAPI application; Python AST is build-time evidence |
|
||||||
|
| Frontend routes and navigation | `PlatformWebModule` contributions |
|
||||||
|
| View-filterable regions | Versioned `viewSurfaces` declarations |
|
||||||
|
| Admin sections and module settings | `admin.sections`, including `moduleId`, `kind`, scope group, permission guards, and surface ID |
|
||||||
|
| User settings | `settings.sections` and core settings schemas |
|
||||||
|
| Labels and translations | Generated translation catalogs plus source usage |
|
||||||
|
| Fields and help coverage | Shared form components plus generated TypeScript AST inventory |
|
||||||
|
| API use by the WebUI | Typed API clients plus generated static reference inventory |
|
||||||
|
| Stable platform interface IDs | Typed manifest/WebUI declarations plus line-independent source anchors for low-level controls |
|
||||||
|
| Effective configuration | Owning module data plus Policy provenance |
|
||||||
|
|
||||||
|
Runtime introspection is authoritative for an installed system. Static source
|
||||||
|
inventory is authoritative evidence for a checkout or release candidate. The
|
||||||
|
two should be compared in CI and by Ops, not conflated.
|
||||||
|
|
||||||
|
## Generated Inventory
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mnt/DATA/git/govoplan
|
||||||
|
./.venv/bin/python tools/inventory/platform-interface-inventory.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The command writes:
|
||||||
|
|
||||||
|
- `audit-reports/platform-inventory/platform-interface-inventory.json`
|
||||||
|
- `audit-reports/platform-inventory/platform-interface-inventory.md`
|
||||||
|
|
||||||
|
Use `--strict` for the combined translation, endpoint, and declaration audit.
|
||||||
|
Use `--strict-declarations` for duplicate/stale/undeclared interface checks
|
||||||
|
without making existing translation coverage a release blocker. Use
|
||||||
|
`--strict-endpoints` in the endpoint-surface CI gate so unrelated translation
|
||||||
|
catalog work cannot disable route classification enforcement. Both strict modes
|
||||||
|
require every backend endpoint without a statically visible WebUI path to have
|
||||||
|
an exact entry in
|
||||||
|
`tools/inventory/endpoint-surface-declarations.json`. The registry is keyed by
|
||||||
|
repository, HTTP method, and canonical version-independent path. It accepts:
|
||||||
|
|
||||||
|
- `ui_reachable`: a mounted router, generic action, or provider path hides the
|
||||||
|
reference from static extraction;
|
||||||
|
- `intentionally_headless`: a capability/API is deliberately consumed without
|
||||||
|
its own UI;
|
||||||
|
- `public_integration`: a documented public or interoperability endpoint;
|
||||||
|
- `worker_internal`: a worker, scheduler, reconciliation, or monitoring path;
|
||||||
|
- `compatibility`: a retained transition endpoint with a current replacement;
|
||||||
|
- `missing_ui`: a real UI gap, which must include a Gitea tracking issue;
|
||||||
|
- `removable`: a reviewed dead endpoint pending removal.
|
||||||
|
|
||||||
|
Strict mode also rejects declarations that no longer match source. When an
|
||||||
|
endpoint is added, changed, or removed, update its declaration in the same
|
||||||
|
change. Do not classify an endpoint from a string mismatch alone: first check
|
||||||
|
mounted prefixes, dynamic action paths, public clients, worker use, and
|
||||||
|
capability consumers.
|
||||||
|
|
||||||
|
It combines:
|
||||||
|
|
||||||
|
1. loaded module manifests
|
||||||
|
2. TypeScript AST extraction of fields, label attributes, visible text,
|
||||||
|
translations, frontend routes, navigation, capabilities, and API references
|
||||||
|
3. Python AST extraction of FastAPI route decorators and router prefixes
|
||||||
|
4. normalized runtime declarations from every loaded `ModuleManifest`
|
||||||
|
|
||||||
|
The declaration set covers routes, navigation, View surfaces, fields, actions,
|
||||||
|
help references, translations, admin/settings sections, widgets, search
|
||||||
|
objects, permissions, provided interfaces, and backend capabilities. Typed
|
||||||
|
module contributions keep their declared IDs. Shared controls may declare
|
||||||
|
`interfaceId` and `helpTopicId`; otherwise the extractor assigns a deterministic
|
||||||
|
source anchor based on repository, file, component context, control type, and
|
||||||
|
semantic label rather than a line number. The JSON records which identity
|
||||||
|
source was used.
|
||||||
|
|
||||||
|
The JSON includes exact repository, file, and line evidence. A missing-help
|
||||||
|
entry is a review candidate because dynamic parent components may supply help.
|
||||||
|
A backend route without a static frontend reference is also a review candidate:
|
||||||
|
public APIs, workers, callbacks, health checks, connectors, and dynamic URL
|
||||||
|
assembly are valid explanations.
|
||||||
|
|
||||||
|
The module matrix enforces endpoint and interface declarations with
|
||||||
|
`--strict-endpoints --strict-declarations`.
|
||||||
|
Combined `--strict` additionally fails when used translation keys are absent
|
||||||
|
from generated locale catalogs. Help-text findings remain review candidates
|
||||||
|
rather than a release gate because dynamic parent components can supply help.
|
||||||
|
|
||||||
|
## Runtime Comparison
|
||||||
|
|
||||||
|
Core exposes a sanitized read-only catalog at
|
||||||
|
`GET /api/v1/platform/interface-catalog`. Access requires
|
||||||
|
`admin:module:read` or `system:settings:read`. Tenant module entitlements are
|
||||||
|
applied before serialization, so the response describes only the effective
|
||||||
|
installed combination. It contains IDs, paths, authorization metadata,
|
||||||
|
versions, counts, and canonical digests; it excludes factories, callbacks,
|
||||||
|
credentials, and mutable runtime state.
|
||||||
|
|
||||||
|
Capture and compare a running installation:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --fail --silent \
|
||||||
|
-H "Authorization: Bearer $GOVOPLAN_ACCESS_TOKEN" \
|
||||||
|
"$GOVOPLAN_URL/api/v1/platform/interface-catalog" \
|
||||||
|
> /tmp/govoplan-runtime-interface.json
|
||||||
|
|
||||||
|
./.venv/bin/python tools/inventory/platform-interface-inventory.py \
|
||||||
|
--runtime-snapshot /tmp/govoplan-runtime-interface.json \
|
||||||
|
--strict-declarations \
|
||||||
|
--strict-endpoints
|
||||||
|
```
|
||||||
|
|
||||||
|
The comparison accepts any installed subset. Every module present in the
|
||||||
|
runtime response must have the same contract version, module version, and
|
||||||
|
declaration digest as the static release inventory. Unknown, duplicate, or
|
||||||
|
mismatched runtime modules fail strict declaration mode.
|
||||||
|
|
||||||
|
## Admin Information Architecture
|
||||||
|
|
||||||
|
The Admin host uses a tree because system, tenant, group, user, and module
|
||||||
|
settings form a hierarchy rather than one flat list. Every contributed section
|
||||||
|
can identify:
|
||||||
|
|
||||||
|
- its owning `moduleId`
|
||||||
|
- whether it is `management` or `settings`
|
||||||
|
- its system/tenant/group/user scope group
|
||||||
|
- an optional future `parentId`
|
||||||
|
- permission and View visibility requirements
|
||||||
|
|
||||||
|
Existing panels remain their own render owners. The tree only changes discovery
|
||||||
|
and grouping. A later embedded-settings contract may add named slots inside an
|
||||||
|
owning page; it must not allow one module to import another module's private
|
||||||
|
component.
|
||||||
|
|
||||||
|
## Navigation And Workflow
|
||||||
|
|
||||||
|
The intended maximum visible navigation stack is:
|
||||||
|
|
||||||
|
1. global shell context
|
||||||
|
2. one task/object navigation surface
|
||||||
|
3. one workflow stage surface when a workflow is active
|
||||||
|
|
||||||
|
Workflow instance pages should reuse the Campaign stage language: clear stage
|
||||||
|
state, optional/skipped/blocked semantics, partial progress, and a stable current
|
||||||
|
step. Workflow definition pages remain graph editors. Views may activate a
|
||||||
|
focused workflow view that suppresses unrelated shell and module surfaces while
|
||||||
|
retaining an explicit way out.
|
||||||
|
|
||||||
|
Nested module submenus should not be added merely because a data hierarchy
|
||||||
|
exists. Prefer a tree inside configuration/directory surfaces, tabs for sibling
|
||||||
|
views, and the workflow stage rail for ordered work.
|
||||||
|
|
||||||
|
## Safe Meta-Configuration
|
||||||
|
|
||||||
|
The platform can eventually render many configuration editors from versioned
|
||||||
|
JSON Schema and UI Schema supplied by modules. Generated editors remain bounded
|
||||||
|
by:
|
||||||
|
|
||||||
|
- explicit typed schemas and migrations
|
||||||
|
- module-owned validation and preview
|
||||||
|
- Policy locks and provenance
|
||||||
|
- permission and View filtering
|
||||||
|
- preflight, consequence, and rollback information
|
||||||
|
- auditable apply operations
|
||||||
|
|
||||||
|
Custom code, new routes, arbitrary SQL, and executable workflow nodes remain
|
||||||
|
release artifacts. Modeling them as ordinary configuration would create an
|
||||||
|
unreviewed code-execution and migration channel.
|
||||||
|
|
||||||
|
## Enforced Contract
|
||||||
|
|
||||||
|
1. Public WebUI routes and View surfaces must reconcile with runtime manifest
|
||||||
|
metadata; stale runtime routes and source-only public surfaces fail CI.
|
||||||
|
2. Duplicate stable IDs fail CI. Shared controls support explicit field/action
|
||||||
|
and help-topic identities; fallback anchors remain visible review evidence.
|
||||||
|
3. Every statically unreferenced backend endpoint has an exact reviewed
|
||||||
|
consumer classification, and stale classifications fail CI.
|
||||||
|
4. Runtime module combinations can be compared exactly with static release
|
||||||
|
evidence through versioned per-module digests.
|
||||||
|
5. Runtime introspection is authorized, tenant-filtered, and read-only. It is
|
||||||
|
safe for Ops/Docs projection but is not a generic configuration or code
|
||||||
|
mutation channel.
|
||||||
|
|
||||||
|
Generated JSON and Markdown remain build/audit artifacts. Do not hand-edit or
|
||||||
|
use them as a backlog; change the owning manifest, typed WebUI contribution,
|
||||||
|
translation/help declaration, or exact endpoint classification instead.
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
# Production Target And Independent Evidence Handoff
|
||||||
|
|
||||||
|
This runbook identifies the external inputs needed to finish
|
||||||
|
[GovOPlaN #27](https://git.add-ideas.de/GovOPlaN/govoplan/issues/27) and
|
||||||
|
[GovOPlaN #37](https://git.add-ideas.de/GovOPlaN/govoplan/issues/37). The
|
||||||
|
repository can render, inspect and sign evidence for a target, but it cannot
|
||||||
|
manufacture an independent failure domain or an independent approval authority.
|
||||||
|
|
||||||
|
## GovOPlaN #27: real two-node target
|
||||||
|
|
||||||
|
The bounded acceptance target is two independently schedulable worker nodes.
|
||||||
|
The API and WebUI must each have ready replicas on both nodes, all Deployments
|
||||||
|
must be available, the active module composition and software versions must be
|
||||||
|
consistent, every configured queue must have a worker, and the database
|
||||||
|
connection budget must pass. The validation then deletes one ready API pod and
|
||||||
|
requires replacement without an observed readiness outage.
|
||||||
|
|
||||||
|
Two virtual machines on different physical hosts or availability zones meet the
|
||||||
|
failure-domain intent. Two containers, VMs or Kubernetes nodes on one physical
|
||||||
|
host are useful development targets but do not close #27. A two-worker cluster
|
||||||
|
also does not prove control-plane high availability. For a self-managed
|
||||||
|
production cluster, use three control-plane nodes plus at least two workers; a
|
||||||
|
managed control plane plus two workers is the shorter path.
|
||||||
|
|
||||||
|
### What the target owner must provide
|
||||||
|
|
||||||
|
Provide these through a secure handoff, not an issue, chat message or Git:
|
||||||
|
|
||||||
|
1. A kubeconfig path with access to the target, for example
|
||||||
|
`~/.config/govoplan/targets/<target>.kubeconfig`, mode `0600`.
|
||||||
|
2. A stable installation ID, public HTTPS hostname, namespace, ingress class and
|
||||||
|
TLS-secret or certificate-manager arrangement.
|
||||||
|
3. Two independently schedulable workers and permission to place API and WebUI
|
||||||
|
replicas on both.
|
||||||
|
4. External, logically shared PostgreSQL, Redis and S3 endpoints with trusted
|
||||||
|
CA material and network reachability from every worker. Do not co-locate the
|
||||||
|
only copies of these services on the two workers used for the failure drill.
|
||||||
|
5. The six runtime secret values required by the generated manifest:
|
||||||
|
`MASTER_KEY_B64`, `DATABASE_URL`, `GOVOPLAN_DATABASE_URL_PGTOOLS`,
|
||||||
|
`REDIS_URL`, `FILE_STORAGE_S3_ACCESS_KEY_ID` and
|
||||||
|
`FILE_STORAGE_S3_SECRET_ACCESS_KEY`.
|
||||||
|
6. A short-lived GovOPlaN API key limited to `ops:operations:read`, supplied in
|
||||||
|
`GOVOPLAN_OPS_API_KEY` only for evidence collection.
|
||||||
|
7. An approved drill window and permission to delete one API pod.
|
||||||
|
|
||||||
|
If no Kubernetes target exists, provide hostnames/IP addresses for the machines,
|
||||||
|
an SSH user and key path, the internal/external DNS plan, and the permitted
|
||||||
|
firewall ports. Those inputs are sufficient to provision a k3s target. They are
|
||||||
|
not sufficient to claim control-plane HA unless three control-plane failure
|
||||||
|
domains are present.
|
||||||
|
|
||||||
|
### Separate deployment and evidence authorities
|
||||||
|
|
||||||
|
The deployment identity may create and update the namespace, Secret,
|
||||||
|
ConfigMap, Deployments, Services, Jobs, PodDisruptionBudgets and Ingress. The
|
||||||
|
evidence collector only needs:
|
||||||
|
|
||||||
|
- cluster scope: `get` and `list` for `nodes`;
|
||||||
|
- target namespace: `get` and `list` for `pods` and `deployments`;
|
||||||
|
- target namespace during the approved drill: `delete` for `pods`.
|
||||||
|
|
||||||
|
Use separate kubeconfig contexts or service accounts when the same person does
|
||||||
|
not hold both roles.
|
||||||
|
|
||||||
|
### Render, apply and verify
|
||||||
|
|
||||||
|
Use the signed, digest-pinned installation bundle selected for the target:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export KUBECONFIG="$HOME/.config/govoplan/targets/<target>.kubeconfig"
|
||||||
|
|
||||||
|
python tools/deployment/govoplan-deploy.py render-kubernetes \
|
||||||
|
--directory /srv/govoplan/<installation-id> \
|
||||||
|
--namespace govoplan \
|
||||||
|
--secret-name govoplan-runtime \
|
||||||
|
--tls-secret-name govoplan-tls \
|
||||||
|
--ingress-class-name nginx \
|
||||||
|
--output /srv/govoplan/<installation-id>/kubernetes.json
|
||||||
|
|
||||||
|
kubectl apply -f /srv/govoplan/<installation-id>/kubernetes.json
|
||||||
|
kubectl -n govoplan wait --for=condition=available deployment --all --timeout=10m
|
||||||
|
|
||||||
|
export GOVOPLAN_OPS_API_KEY="$(cat /run/secrets/govoplan-ops-evidence-key)"
|
||||||
|
python tools/deployment/govoplan-deploy.py verify-kubernetes \
|
||||||
|
--directory /srv/govoplan/<installation-id> \
|
||||||
|
--namespace govoplan \
|
||||||
|
--exercise-api-pod-loss \
|
||||||
|
--output /srv/govoplan/<installation-id>/evidence/kubernetes-multi-host.json
|
||||||
|
unset GOVOPLAN_OPS_API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
The verifier emits sanitized JSON and exits nonzero if the topology, runtime,
|
||||||
|
queue, connection-budget or pod-loss checks fail. Preserve the private cluster
|
||||||
|
logs and manifest alongside the sanitized result in the controlled evidence
|
||||||
|
store.
|
||||||
|
|
||||||
|
## GovOPlaN #37: controlled signed target evidence
|
||||||
|
|
||||||
|
Yes, collection, review and signing can run in containers. A container provides
|
||||||
|
repeatability and process isolation; it does not create independent authority.
|
||||||
|
The production approver must control a different private key from the target
|
||||||
|
operator/assessor and must review the evidence before signing the
|
||||||
|
`production_approval` scope.
|
||||||
|
|
||||||
|
Use at least these three key boundaries:
|
||||||
|
|
||||||
|
1. **Installer authority:** signs installed-release-origin receipts only.
|
||||||
|
2. **Target assessment authority:** signs the permitted target, accessibility,
|
||||||
|
privacy, security, operations and recovery scopes.
|
||||||
|
3. **Production approval authority:** independently signs only
|
||||||
|
`production_approval` after reviewing the other evidence.
|
||||||
|
|
||||||
|
Do not reuse release-catalog keys for any of these roles. Keep private Ed25519
|
||||||
|
keys outside Git, Gitea, GovOPlaN application storage and chat. Publish only the
|
||||||
|
public keyrings. The proof issuer already rejects key reuse across release,
|
||||||
|
installer and proof trust domains.
|
||||||
|
|
||||||
|
### Generate independently held keys
|
||||||
|
|
||||||
|
Each authority runs this command in its own `0700` directory. The generator
|
||||||
|
refuses existing output paths and writes both files as `0600`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
install -d -m 0700 "$HOME/.config/govoplan/authority-keys"
|
||||||
|
|
||||||
|
python tools/assessments/generate-authority-keypair.py \
|
||||||
|
--purpose proof \
|
||||||
|
--key-id authority:target-2026 \
|
||||||
|
--scope target_environment \
|
||||||
|
--scope accessibility \
|
||||||
|
--scope privacy \
|
||||||
|
--scope security \
|
||||||
|
--scope operations \
|
||||||
|
--scope recovery \
|
||||||
|
--private-key "$HOME/.config/govoplan/authority-keys/target-2026.pem" \
|
||||||
|
--keyring "$HOME/.config/govoplan/authority-keys/target-2026-public.json"
|
||||||
|
```
|
||||||
|
|
||||||
|
The independent production approver generates another key with only
|
||||||
|
`--scope production_approval`. An installer authority uses `--purpose installer`
|
||||||
|
and no `--scope`. Merge public key entries into the separately controlled
|
||||||
|
keyrings only after the responsible authorities verify fingerprints out of
|
||||||
|
band.
|
||||||
|
|
||||||
|
### Container boundary
|
||||||
|
|
||||||
|
Use two one-shot jobs or containers:
|
||||||
|
|
||||||
|
- **Collector/assessor:** network access, read-only source and trust mounts,
|
||||||
|
read/write private evidence output, and the narrowly scoped kubeconfig. It
|
||||||
|
must not receive the production-approval private key.
|
||||||
|
- **Production approver:** `--network none`, read-only assessment/evidence/trust
|
||||||
|
mounts, a read-only secret mount containing only the approval key, and a
|
||||||
|
separate output mount. It must not receive deployment credentials.
|
||||||
|
|
||||||
|
Build or select the assessment image by digest and record that digest in the
|
||||||
|
evidence log. A representative runtime shape is:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --network none --read-only --tmpfs /tmp \
|
||||||
|
--user "$(id -u):$(id -g)" \
|
||||||
|
--mount type=bind,src="$PWD/evidence",dst=/evidence,readonly \
|
||||||
|
--mount type=bind,src="$PWD/trust",dst=/trust,readonly \
|
||||||
|
--mount type=bind,src="$HOME/.config/govoplan/authority-keys",dst=/run/keys,readonly \
|
||||||
|
--mount type=bind,src="$PWD/approved",dst=/output \
|
||||||
|
<assessment-image>@sha256:<digest> \
|
||||||
|
<assessment command>
|
||||||
|
```
|
||||||
|
|
||||||
|
The current evidence commands and required scopes are documented in
|
||||||
|
[`TARGET_MATURITY_EVIDENCE_RUNBOOK.md`](TARGET_MATURITY_EVIDENCE_RUNBOOK.md).
|
||||||
|
The final proof must cover `target_environment`, `accessibility`, `privacy`,
|
||||||
|
`security`, `operations`, `recovery` and independent `production_approval`, and
|
||||||
|
must bind to the verified installed composition and installer receipt.
|
||||||
|
|
||||||
|
## Completion boundary
|
||||||
|
|
||||||
|
#27 can close after the real target produces a passing pod-loss result. #37 can
|
||||||
|
close after an independently approved, schema-valid proof is generated for that
|
||||||
|
same installed composition and the public authority keyrings, proof and private
|
||||||
|
evidence custody references are recorded. Neither issue should close from a
|
||||||
|
single-host simulation or a self-approved signature.
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# Recovery And Rollback Guarantees
|
||||||
|
|
||||||
|
## Principle
|
||||||
|
|
||||||
|
GovOPlaN must prove recovery claims with durable state recorded before and
|
||||||
|
after side effects. A failed operation is not automatically rolled back merely
|
||||||
|
because the previous application image still exists. Database schema and
|
||||||
|
external effects may make release rollback unsafe.
|
||||||
|
|
||||||
|
Core therefore distinguishes five recovery modes:
|
||||||
|
|
||||||
|
| Mode | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `atomic` | One database transaction either commits or rolls back. No external effect is claimed. |
|
||||||
|
| `compensation` | Durable evidence identifies explicit inverse actions for completed effects. |
|
||||||
|
| `snapshot_restore` | A separately verified backup reference and restore procedure exist. |
|
||||||
|
| `forward_recovery` | Repair or resume the current version; reverting code/configuration is not claimed safe. |
|
||||||
|
| `irreversible` | No automated recovery is claimed and an approval reference is mandatory. |
|
||||||
|
|
||||||
|
An operation plan must include verification steps. Compensation requires named
|
||||||
|
compensation steps, snapshot restore requires a verified backup reference,
|
||||||
|
forward recovery requires repair steps, and irreversible work requires explicit
|
||||||
|
approval.
|
||||||
|
|
||||||
|
## Core Recovery Ledger
|
||||||
|
|
||||||
|
Core stores recovery operations and append-only, hash-chained checkpoints in
|
||||||
|
PostgreSQL. The contract provides:
|
||||||
|
|
||||||
|
- installation/module/resource identity;
|
||||||
|
- an idempotency key bound to a canonical request hash;
|
||||||
|
- recovery mode, preconditions, verification steps, and references;
|
||||||
|
- optional runtime lease holder and fencing token;
|
||||||
|
- explicit planned, prepared, running, recovery-required, recovering,
|
||||||
|
succeeded, recovered, failed, outcome-unknown, and manual-intervention states;
|
||||||
|
- an evidence-chain head and sequence count;
|
||||||
|
- rejection of plaintext secrets in metadata or evidence.
|
||||||
|
|
||||||
|
Preparation cannot succeed without durable precondition evidence. A non-atomic
|
||||||
|
operation cannot hide a partial effect by transitioning directly from running
|
||||||
|
to failed. Success and recovery require explicit verification evidence with at
|
||||||
|
least one check. The ledger verifies its hash chain before evidence is trusted.
|
||||||
|
|
||||||
|
This is a platform contract, not an assertion that every existing module
|
||||||
|
operation has adopted it. Module operations with external or multi-resource
|
||||||
|
effects must be migrated to the ledger before claiming these guarantees.
|
||||||
|
The owning-module inventory and adoption state are maintained in
|
||||||
|
[Recovery Ledger Adoption](RECOVERY_LEDGER_ADOPTION.md); CI validates the
|
||||||
|
machine-readable inventory so newly identified boundaries cannot disappear from
|
||||||
|
the backlog silently.
|
||||||
|
|
||||||
|
## Deployment Journal
|
||||||
|
|
||||||
|
Every `govoplan-deploy apply` begins an operation journal before it pulls images
|
||||||
|
or mutates runtime state. The private installation directory records:
|
||||||
|
|
||||||
|
```text
|
||||||
|
operations/<operation-id>/operation.json
|
||||||
|
operations/<operation-id>/before/
|
||||||
|
applied-state/
|
||||||
|
```
|
||||||
|
|
||||||
|
Each stage is hash-chained. The previous applied bundle is copied with per-file
|
||||||
|
SHA-256 evidence. Applied state is replaced atomically after health verification;
|
||||||
|
an interrupted replacement restores its previous directory.
|
||||||
|
New journals also bind the complete desired deployment plan, snapshot
|
||||||
|
availability, failure summary, recovery mode, and terminal status to the
|
||||||
|
evidence chain. Recovery verifies every snapshot entry and checksum before it
|
||||||
|
changes any live bundle file, then replaces each live file atomically. A crash
|
||||||
|
between file replacements is recoverable by rerunning the same idempotent
|
||||||
|
recovery command under the deployment lock.
|
||||||
|
|
||||||
|
Inspect operations:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python tools/deployment/govoplan-deploy.py operations \
|
||||||
|
--directory /srv/govoplan/default
|
||||||
|
```
|
||||||
|
|
||||||
|
Recover the latest failed operation, or provide its identifier:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python tools/deployment/govoplan-deploy.py recover \
|
||||||
|
--directory /srv/govoplan/default \
|
||||||
|
--operation-id 20260801T120000Z-1234abcd
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `--apply` only after reviewing the reported action.
|
||||||
|
|
||||||
|
## Migration Boundary
|
||||||
|
|
||||||
|
Before database migration starts, a failed deployment with a verified prior
|
||||||
|
applied snapshot may restore its prior release/configuration bundle and
|
||||||
|
reconcile that desired state.
|
||||||
|
|
||||||
|
As soon as migration starts, the journal permanently changes to
|
||||||
|
`forward_recovery`. It will not restore old application configuration because
|
||||||
|
old code may not understand the new schema. Recovery then means one of:
|
||||||
|
|
||||||
|
1. fix and re-run the current release;
|
||||||
|
2. deploy a newer compatible repair release;
|
||||||
|
3. restore a separately verified, coordinated database/object/key backup and
|
||||||
|
then deploy the matching release.
|
||||||
|
|
||||||
|
The deployment tool does not create that backup. It does verify an externally
|
||||||
|
produced, signed evidence contract covering PostgreSQL, objects, protected
|
||||||
|
configuration, and key custody at one recovery point plus an isolated restore
|
||||||
|
drill. A self-hosted release change cannot reach the migration command or be
|
||||||
|
exported as a Kubernetes migration Job until fresh evidence bound to the
|
||||||
|
previous immutable release has been adopted. Compose verifies it again after
|
||||||
|
runtime quiescing. See
|
||||||
|
[Backup And Restore Evidence](BACKUP_AND_RESTORE_EVIDENCE.md) for the contract,
|
||||||
|
provider runbooks, RPO/RTO ownership, retention, and disposal rules.
|
||||||
|
|
||||||
|
## Scaled Nodes
|
||||||
|
|
||||||
|
Recovery actions must be safe across replicas:
|
||||||
|
|
||||||
|
- drain affected API and worker nodes before incompatible changes;
|
||||||
|
- use the deployment-wide PostgreSQL advisory lock for schema migration;
|
||||||
|
- use distributed leases and fencing tokens for singleton or externally
|
||||||
|
visible effects;
|
||||||
|
- use idempotency keys for retried commands and jobs;
|
||||||
|
- retain shared object keys and database references until deletion succeeds;
|
||||||
|
- classify uncertain external outcomes instead of retrying blindly;
|
||||||
|
- verify the exact software/module composition after replacement.
|
||||||
|
|
||||||
|
Campaign generated-message objects now follow this model: object writes are
|
||||||
|
compensated when a build fails before database commit, workers verify stored
|
||||||
|
size and digest before delivery, and retention keeps the database reference
|
||||||
|
when storage deletion fails. A hard process loss between object creation and
|
||||||
|
database commit can still leave an orphan object; an inventory reconciler is a
|
||||||
|
separate operational slice and must use the build-specific object prefix.
|
||||||
|
|
||||||
|
## Required Drills
|
||||||
|
|
||||||
|
Record evidence for at least these scenarios before production acceptance:
|
||||||
|
|
||||||
|
1. Kill an API replica and verify traffic continues without session loss.
|
||||||
|
2. Drain and replace a worker while work is queued and while one job is active.
|
||||||
|
3. Start two migration jobs and verify only one mutates schema.
|
||||||
|
4. Kill the fenced scheduler and verify one replacement acquires a higher
|
||||||
|
fencing token.
|
||||||
|
5. Fail deployment before migration and restore the prior applied bundle.
|
||||||
|
6. Fail deployment after migration and verify old configuration is not
|
||||||
|
restored.
|
||||||
|
7. Restore PostgreSQL, object storage, and encryption keys to one coordinated
|
||||||
|
recovery point and verify representative object hashes.
|
||||||
|
8. Interrupt object storage during Campaign build and retention and verify
|
||||||
|
compensation/reference-preservation behavior.
|
||||||
|
9. Tamper with a deployment or Core recovery checkpoint and verify chain
|
||||||
|
validation rejects it.
|
||||||
|
|
||||||
|
No runbook, status badge, or green health endpoint substitutes for a dated,
|
||||||
|
repeatable restore drill against the actual deployment topology.
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Recovery Ledger Adoption
|
||||||
|
|
||||||
|
The Core recovery ledger is a platform primitive, not automatic protection for
|
||||||
|
module-owned effects. The canonical, machine-checked inventory is
|
||||||
|
[`recovery-operation-inventory.json`](recovery-operation-inventory.json).
|
||||||
|
|
||||||
|
## Classification Rules
|
||||||
|
|
||||||
|
- Use `atomic` only when every mutation commits in one database transaction and
|
||||||
|
no external effect occurs.
|
||||||
|
- Use `compensation` when every completed effect has a bounded, verifiable
|
||||||
|
inverse action. A best-effort delete is not proof of compensation.
|
||||||
|
- Use `snapshot_restore` only with fresh, signed backup evidence that covers all
|
||||||
|
affected state services at one recovery point.
|
||||||
|
- Use `forward_recovery` for provider acceptance, queue publication, cursor
|
||||||
|
advancement, and other effects that may be resumable but cannot safely be
|
||||||
|
undone.
|
||||||
|
- Use `irreversible` for approved purge or destruction where no automated
|
||||||
|
recovery is claimed.
|
||||||
|
|
||||||
|
One feature may cross more than one boundary. Module installation is
|
||||||
|
compensatable before schema migration, forward-only after migration starts, and
|
||||||
|
snapshot-restorable for an approved destructive retirement. Mail submission is
|
||||||
|
forward recovery because losing the response after provider acceptance must not
|
||||||
|
cause an automatic resend.
|
||||||
|
|
||||||
|
## Adoption Order
|
||||||
|
|
||||||
|
1. Campaign build is the reference implementation for a database plus object
|
||||||
|
storage operation. Its operation reserves a build-specific object prefix,
|
||||||
|
persists request and precondition evidence before writes, records the final
|
||||||
|
object manifest, and verifies database/object state before success.
|
||||||
|
2. Campaign delivery and Mail provider effects adopt outcome-unknown semantics
|
||||||
|
without weakening their existing provider-specific idempotency records.
|
||||||
|
3. Files applies the same contract to uploads, purge, integrity reconciliation,
|
||||||
|
and writable connector synchronization.
|
||||||
|
4. Connectors, Dataflow, and Workflow Engine consume the contract at their
|
||||||
|
registry/capability boundaries so optional providers remain optional.
|
||||||
|
5. Core module lifecycle uses the ledger in addition to, not instead of, signed
|
||||||
|
deployment and backup evidence.
|
||||||
|
|
||||||
|
Every fenced operation uses a process incarnation and distributed lease. A
|
||||||
|
stale process cannot append a checkpoint or report success. An expired operation
|
||||||
|
is claimed for recovery through an explicit takeover that preserves the prior
|
||||||
|
fence in the checkpoint chain; it is never resumed as a normal retry.
|
||||||
|
|
||||||
|
Connectors read-only sanctions and feed acquisitions are adopted: source
|
||||||
|
revision/cursor and dry-run evidence are recorded before provider I/O, while
|
||||||
|
the immutable snapshot and terminal checkpoint commit atomically. The generic
|
||||||
|
external-mutation contract is conformance-tested but remains `planned` until a
|
||||||
|
production connector actually publishes, updates, or deletes provider state.
|
||||||
|
|
||||||
|
Dataflow runs are adopted. Database-only execution uses one atomic terminal
|
||||||
|
commit for the run projection and recovery checkpoint. Output publication uses
|
||||||
|
forward recovery: source and output digests are checkpointed before dispatch,
|
||||||
|
a conclusive provider result commits with the run projection, and an expired
|
||||||
|
or failed attempt after dispatch becomes `outcome_unknown`. A stale attempt may
|
||||||
|
be retried only when its durable boundary proves dispatch had not started.
|
||||||
|
|
||||||
|
Workflow Engine is adopted at both declared boundaries. Instance workers,
|
||||||
|
trigger deliveries, and timer resumptions use process-bound distributed fences.
|
||||||
|
Every module-action invocation records the pinned definition, input, preview,
|
||||||
|
authority, provider-idempotency, and action-contract hashes before dispatch.
|
||||||
|
Conclusive results commit with the Workflow projection. A lost acknowledgement,
|
||||||
|
invalid result, or unannounced non-atomic effect becomes `outcome_unknown` and
|
||||||
|
cannot be retried until evidence confirms either that the effect occurred or is
|
||||||
|
absent. Linked Dataflow uncertainty blocks the Workflow without duplicating
|
||||||
|
Dataflow's recovery authority.
|
||||||
|
|
||||||
|
Core module lifecycle is adopted at four boundaries. Installer recovery is
|
||||||
|
prepared before snapshots so a full database restore preserves the attempted
|
||||||
|
operation. Pre-migration package changes use compensation, migrated changes use
|
||||||
|
forward recovery, destructive retirement requires a hashed and restore-checked
|
||||||
|
snapshot, and live graph changes restore the prior registry when no migration
|
||||||
|
ran. A deployment-wide database fence serializes these effects; any unresolved
|
||||||
|
predecessor blocks a differently keyed retry until explicit reconciliation.
|
||||||
|
Supervised installs become successful only after restart and health evidence is
|
||||||
|
recorded.
|
||||||
|
|
||||||
|
## Operator Contract
|
||||||
|
|
||||||
|
Ops lists non-terminal and manual-intervention operations. Operators must verify
|
||||||
|
the checkpoint chain before trusting evidence, distinguish `outcome_unknown`
|
||||||
|
from rejection, and use the owning module's documented reconciliation action.
|
||||||
|
No evidence payload may contain credentials or resolved secrets.
|
||||||
|
|
||||||
|
The parent adoption issue remains open until all inventory rows are adopted and
|
||||||
|
the module matrix proves crash, retry, stale-fence, tamper, and optional-module
|
||||||
|
behavior for each consequential path.
|
||||||
@@ -2,9 +2,16 @@
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
This is the selected product-development sequence as of 2026-07-21. It turns
|
This is the selected product-development sequence, originally chosen on
|
||||||
the long-term connected-platform roadmap into five demonstrable journeys while
|
2026-07-21 and reconciled with the implemented platform on 2026-07-31. It turns
|
||||||
the Workflow program remains deliberately deferred.
|
the long-term connected-platform roadmap into five demonstrable journeys.
|
||||||
|
Workflow Engine and the optional Workflow editor now exist, but they are used
|
||||||
|
by a stage only when its package explicitly composes and proves them; Workflow
|
||||||
|
is not an automatic dependency of every journey.
|
||||||
|
|
||||||
|
The institutional semantics and source-authority model applied to these stages
|
||||||
|
are defined in the
|
||||||
|
[Institutional Governance Target Architecture](INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md).
|
||||||
|
|
||||||
The stages are ordered, but they are not monolithic releases. Each stage is
|
The stages are ordered, but they are not monolithic releases. Each stage is
|
||||||
delivered as small, reviewable, green increments and is complete only when its
|
delivered as small, reviewable, green increments and is complete only when its
|
||||||
@@ -445,8 +452,9 @@ that first needs them:
|
|||||||
- exact HIS/CampusOnline source endpoints, student-statistics keys and accepted
|
- exact HIS/CampusOnline source endpoints, student-statistics keys and accepted
|
||||||
calculation, freeze/correction policy, privacy profile, and permitted
|
calculation, freeze/correction policy, privacy profile, and permitted
|
||||||
drill-down level;
|
drill-down level;
|
||||||
- whether repeated data-source and dataflow contracts justify separate
|
- datasource provider selection and quality/promotion policy; the architecture
|
||||||
`govoplan-datasources` and `govoplan-dataflow` modules after the Stage 3 proof;
|
now separates `govoplan-datasources` lifecycle from `govoplan-connectors`
|
||||||
|
acquisition and `govoplan-dataflow` transformation;
|
||||||
- first collaborative editor/provider and whether the first UX is concurrent
|
- first collaborative editor/provider and whether the first UX is concurrent
|
||||||
editing, controlled check-out, or both; and
|
editing, controlled check-out, or both; and
|
||||||
- first Records/archive target and approval/signature assurance level.
|
- first Records/archive target and approval/signature assurance level.
|
||||||
|
|||||||
+62
-25
@@ -22,10 +22,17 @@ effects can be verified safely:
|
|||||||
- freeze a selective plan as a durable, resumable local release run
|
- freeze a selective plan as a durable, resumable local release run
|
||||||
- durably preflight a creation-time-bound repository, create its annotated tag,
|
- durably preflight a creation-time-bound repository, create its annotated tag,
|
||||||
and publish its branch/tag pair atomically
|
and publish its branch/tag pair atomically
|
||||||
|
- deterministically update recognized package/manifest version declarations and
|
||||||
|
commit only the receipt-bound metadata paths
|
||||||
|
- order selected module providers before consumers, create module tags before
|
||||||
|
Core, regenerate Core's selected WebUI release lock, and re-run alignment
|
||||||
|
before any remote push
|
||||||
- build selected Python wheels and generate a private, signed, receipt-bound
|
- build selected Python wheels and generate a private, signed, receipt-bound
|
||||||
catalog candidate
|
catalog candidate
|
||||||
- publish that exact candidate through a verified website commit and immutable
|
- publish that exact candidate through a verified website commit and immutable
|
||||||
tag after explicit confirmation
|
tag after explicit confirmation
|
||||||
|
- install selected candidate wheels into a private no-network/no-dependency
|
||||||
|
target and verify their installed metadata against the frozen plan
|
||||||
|
|
||||||
Start it from the meta repository:
|
Start it from the meta repository:
|
||||||
|
|
||||||
@@ -67,15 +74,35 @@ shows the dry-run commands for the selected rows, and `Generate Candidate`
|
|||||||
creates a signed catalog candidate that advances only selected repositories that
|
creates a signed catalog candidate that advances only selected repositories that
|
||||||
already have a catalog entry.
|
already have a catalog entry.
|
||||||
|
|
||||||
|
The full-width **Release Workflow** guide projects the server state into seven
|
||||||
|
operator phases: Inspect, Targets, Validate, Source, Package, Publish, and
|
||||||
|
Verify. It does not maintain a second workflow state. Completed, current,
|
||||||
|
blocked, locked, and unavailable phases are derived from the dashboard,
|
||||||
|
selective plan, and durable run record. The next-action panel opens the exact
|
||||||
|
section or durable step that needs attention. Changing a channel, target
|
||||||
|
version, repository selection, or release gate detaches the browser from the
|
||||||
|
current run and invalidates the draft plan; the persisted run remains available
|
||||||
|
from the saved-run selector. Problems in unselected repositories remain visible
|
||||||
|
as workspace notices but do not lock an unrelated release; the selective plan
|
||||||
|
is the authority for blockers in the selected repository set.
|
||||||
|
|
||||||
|
Installation verification is an explicit durable step after catalog
|
||||||
|
publication. It verifies the exact candidate receipt, installs every selected
|
||||||
|
Python wheel into a temporary target with network and dependency resolution
|
||||||
|
disabled, and compares installed names and versions with the frozen plan.
|
||||||
|
Deployment startup, database upgrades, and module-combination smoke tests remain
|
||||||
|
release-integration CI gates; the console does not represent its local wheel
|
||||||
|
check as a production deployment.
|
||||||
|
|
||||||
`Build Plan` also returns structured release-gate findings for each selected
|
`Build Plan` also returns structured release-gate findings for each selected
|
||||||
repository. The plan names the recommended next action and gives an explicit
|
repository. The plan names the recommended next action and gives an explicit
|
||||||
remediation for source-version, lockfile, Core WebUI composition, Git state, and
|
remediation for source-version, lockfile, Core WebUI composition, Git state, and
|
||||||
worktree findings. A target version that has not yet been applied consistently
|
worktree findings. A target version that differs from internally consistent
|
||||||
to the selected source tree is therefore explained before the source-tag
|
source metadata becomes a bounded `UPDATE` step. Unsupported, missing, or
|
||||||
preflight rather than appearing only as an error after the operator tries to
|
internally inconsistent declarations remain a blocker with exact remediation.
|
||||||
tag. `source_preflight_ready` means that the plan-visible source gates pass; the
|
`source_preflight_ready` means that plan-visible source gates pass or have a
|
||||||
non-mutating `Preview Tag + Publish` remains mandatory for remote, manifest, and
|
bounded deterministic mutation; the non-mutating `Preview Tag + Publish`
|
||||||
immutable-tag checks.
|
remains mandatory for remote, manifest, and immutable-tag checks.
|
||||||
|
|
||||||
## Durable release runs
|
## Durable release runs
|
||||||
|
|
||||||
@@ -85,11 +112,12 @@ requires the plan to resolve exactly the requested repositories and target
|
|||||||
versions; the browser cannot submit or replace the plan snapshot. The input and
|
versions; the browser cannot submit or replace the plan snapshot. The input and
|
||||||
plan are then immutable and covered by a canonical SHA-256 integrity digest.
|
plan are then immutable and covered by a canonical SHA-256 integrity digest.
|
||||||
Every executable repository step also carries its creation-time full HEAD,
|
Every executable repository step also carries its creation-time full HEAD,
|
||||||
branch, worktree state, target tag, and a SHA-256 over both the fetch and push
|
branch, target tag, a SHA-256 over both the fetch and push URLs of `origin`, and
|
||||||
URLs of `origin`. Both URLs must exactly equal the remote registered in
|
a SHA-256 over bounded dirty-path names and bytes. Both URLs must exactly equal
|
||||||
`repositories.json`; a changed HEAD, branch, worktree, remote, or push URL
|
the remote registered in `repositories.json`; a changed HEAD, branch, worktree,
|
||||||
requires a new run rather than silently retargeting the frozen compatibility
|
remote, push URL, or metadata byte requires a new run or explicit
|
||||||
decision.
|
interrupted-step reconciliation rather than silently retargeting the frozen
|
||||||
|
compatibility decision.
|
||||||
The complete record also has a checksum so a valid-looking manual edit to its
|
The complete record also has a checksum so a valid-looking manual edit to its
|
||||||
mutable state fails closed. File permissions remain the authority boundary;
|
mutable state fails closed. File permissions remain the authority boundary;
|
||||||
these digests detect accidental or manual corruption, not an attacker who can
|
these digests detect accidental or manual corruption, not an attacker who can
|
||||||
@@ -177,24 +205,32 @@ retry. The UI keeps unavailable controls visible and disabled.
|
|||||||
|
|
||||||
Supported executors durably claim the step before invoking an effect. Exact
|
Supported executors durably claim the step before invoking an effect. Exact
|
||||||
attempt replays return the recorded outcome and never invoke the executor a
|
attempt replays return the recorded outcome and never invoke the executor a
|
||||||
second time. Successful repository preflight, tag, and push steps persist a
|
second time. Successful repository preflight, version, commit, Core-bundle,
|
||||||
bounded repository-state receipt. Tag reconciliation independently requires an
|
tag, and push steps persist a bounded repository-state receipt. Version
|
||||||
|
reconciliation requires aligned declarations in the same commit; commit
|
||||||
|
reconciliation requires the expected single-parent release commit and only
|
||||||
|
recognized metadata paths. Tag reconciliation independently requires an
|
||||||
annotated local tag at the frozen HEAD; push reconciliation additionally
|
annotated local tag at the frozen HEAD; push reconciliation additionally
|
||||||
requires both the remote annotated tag object and remote branch to match.
|
requires both the remote annotated tag object and remote branch to match.
|
||||||
Catalog generation persists
|
Catalog generation persists
|
||||||
only its server-issued opaque candidate ID and canonical catalog SHA-256, then
|
only its server-issued opaque candidate ID and canonical catalog SHA-256, then
|
||||||
re-resolves and re-hashes that private candidate before publication.
|
re-resolves and re-hashes that private candidate before publication.
|
||||||
|
|
||||||
The safe baseline is intentionally narrower than the complete dry-run plan.
|
Repository capabilities are frozen into each plan unit (`python-package`,
|
||||||
Dirty worktrees and version changes still show commit/version steps, but those
|
`webui-package`, `module-manifest`, `database-migrations`, `documentation`,
|
||||||
steps have no durable executor because the frozen run does not bind the exact
|
`core-release-bundle`, and the universal `git-source`) and determine which
|
||||||
proposed file content. A run selecting Core together with one or more modules
|
steps appear. Internally aligned version changes are rendered deterministically
|
||||||
begins with a disabled dependency-ordering barrier: local module tags, Core
|
from recognized TOML, JSON, lockfile, manifest, and package declarations.
|
||||||
release-lock regeneration/commit, Core tagging, alignment verification, and
|
Pre-existing dirty worktrees remain visible but have no commit executor; the
|
||||||
pushes need an explicit DAG executor before that composition can mutate.
|
console never absorbs unrelated operator changes.
|
||||||
Operators must prepare and review those changes outside the console and create
|
|
||||||
a new run from the resulting clean HEAD. The console never skips these steps or
|
For mixed releases, module interface providers are ordered before consumers
|
||||||
claims an end-to-end release succeeded.
|
and Core is tagged last. The durable sequence creates and commits module
|
||||||
|
metadata, creates local module tags, updates Core's selected WebUI references,
|
||||||
|
regenerates the release lock against those local tags, commits/tags Core, and
|
||||||
|
runs a receipt-bound alignment gate before exposing any atomic branch/tag push.
|
||||||
|
A failed step stops later steps while preserving prior receipts for explicit
|
||||||
|
retry or reconciliation.
|
||||||
|
|
||||||
The browser likewise retains the request identifier for an uncertain
|
The browser likewise retains the request identifier for an uncertain
|
||||||
resume/retry/reconciliation response and replays it after reload. A successful
|
resume/retry/reconciliation response and replays it after reload. A successful
|
||||||
@@ -226,8 +262,9 @@ other `/api/` route:
|
|||||||
- `POST /api/release-runs/{run_id}/steps/{step_id}/preview` provides the
|
- `POST /api/release-runs/{run_id}/steps/{step_id}/preview` provides the
|
||||||
non-mutating preview for receipt-bound catalog publication.
|
non-mutating preview for receipt-bound catalog publication.
|
||||||
|
|
||||||
Run-storage errors are confined to the Durable Run State card; dashboard and
|
Run-storage errors are confined to the Durable Release Run section; dashboard
|
||||||
release-preview collection continue and show the bounded storage remediation.
|
and release-preview collection continue and the workflow guide points to the
|
||||||
|
bounded storage remediation.
|
||||||
|
|
||||||
The run record is execution evidence only for a supported step whose durable
|
The run record is execution evidence only for a supported step whose durable
|
||||||
claim and bounded result receipt were persisted. The console never infers
|
claim and bounded result receipt were persisted. The console never infers
|
||||||
|
|||||||
+77
-65
@@ -6,81 +6,93 @@ Generated from `repositories.json`. Use that JSON file as the machine-readable s
|
|||||||
|
|
||||||
| Repository | Subtype | Local path | Gitea |
|
| Repository | Subtype | Local path | Gitea |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `govoplan` | `meta` | `../govoplan` | [govoplan](https://git.add-ideas.de/add-ideas/govoplan) |
|
| `govoplan` | `meta` | `../govoplan` | [govoplan](https://git.add-ideas.de/GovOPlaN/govoplan) |
|
||||||
| `govoplan-core` | `kernel` | `../govoplan-core` | [govoplan-core](https://git.add-ideas.de/add-ideas/govoplan-core) |
|
| `govoplan-core` | `kernel` | `../govoplan-core` | [govoplan-core](https://git.add-ideas.de/GovOPlaN/govoplan-core) |
|
||||||
|
|
||||||
## Module
|
## Module
|
||||||
|
|
||||||
| Repository | Subtype | Local path | Gitea |
|
| Repository | Subtype | Local path | Gitea |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `govoplan-access` | `platform` | `../govoplan-access` | [govoplan-access](https://git.add-ideas.de/add-ideas/govoplan-access) |
|
| `govoplan-access` | `platform` | `../govoplan-access` | [govoplan-access](https://git.add-ideas.de/GovOPlaN/govoplan-access) |
|
||||||
| `govoplan-addresses` | `domain` | `../govoplan-addresses` | [govoplan-addresses](https://git.add-ideas.de/add-ideas/govoplan-addresses) |
|
| `govoplan-addresses` | `domain` | `../govoplan-addresses` | [govoplan-addresses](https://git.add-ideas.de/GovOPlaN/govoplan-addresses) |
|
||||||
| `govoplan-admin` | `platform` | `../govoplan-admin` | [govoplan-admin](https://git.add-ideas.de/add-ideas/govoplan-admin) |
|
| `govoplan-admin` | `platform` | `../govoplan-admin` | [govoplan-admin](https://git.add-ideas.de/GovOPlaN/govoplan-admin) |
|
||||||
| `govoplan-appointments` | `domain` | `../govoplan-appointments` | [govoplan-appointments](https://git.add-ideas.de/add-ideas/govoplan-appointments) |
|
| `govoplan-appointments` | `domain` | `../govoplan-appointments` | [govoplan-appointments](https://git.add-ideas.de/GovOPlaN/govoplan-appointments) |
|
||||||
| `govoplan-approvals` | `domain` | `../govoplan-approvals` | [govoplan-approvals](https://git.add-ideas.de/add-ideas/govoplan-approvals) |
|
| `govoplan-approvals` | `domain` | `../govoplan-approvals` | [govoplan-approvals](https://git.add-ideas.de/GovOPlaN/govoplan-approvals) |
|
||||||
| `govoplan-assets` | `domain` | `../govoplan-assets` | [govoplan-assets](https://git.add-ideas.de/add-ideas/govoplan-assets) |
|
| `govoplan-assets` | `domain` | `../govoplan-assets` | [govoplan-assets](https://git.add-ideas.de/GovOPlaN/govoplan-assets) |
|
||||||
| `govoplan-audit` | `platform` | `../govoplan-audit` | [govoplan-audit](https://git.add-ideas.de/add-ideas/govoplan-audit) |
|
| `govoplan-audit` | `platform` | `../govoplan-audit` | [govoplan-audit](https://git.add-ideas.de/GovOPlaN/govoplan-audit) |
|
||||||
| `govoplan-booking` | `domain` | `../govoplan-booking` | [govoplan-booking](https://git.add-ideas.de/add-ideas/govoplan-booking) |
|
| `govoplan-booking` | `domain` | `../govoplan-booking` | [govoplan-booking](https://git.add-ideas.de/GovOPlaN/govoplan-booking) |
|
||||||
| `govoplan-calendar` | `domain` | `../govoplan-calendar` | [govoplan-calendar](https://git.add-ideas.de/add-ideas/govoplan-calendar) |
|
| `govoplan-calendar` | `domain` | `../govoplan-calendar` | [govoplan-calendar](https://git.add-ideas.de/GovOPlaN/govoplan-calendar) |
|
||||||
| `govoplan-campaign` | `domain` | `../govoplan-campaign` | [govoplan-campaign](https://git.add-ideas.de/add-ideas/govoplan-campaign) |
|
| `govoplan-campaign` | `domain` | `../govoplan-campaign` | [govoplan-campaign](https://git.add-ideas.de/GovOPlaN/govoplan-campaign) |
|
||||||
| `govoplan-cases` | `domain` | `../govoplan-cases` | [govoplan-cases](https://git.add-ideas.de/add-ideas/govoplan-cases) |
|
| `govoplan-cases` | `domain` | `../govoplan-cases` | [govoplan-cases](https://git.add-ideas.de/GovOPlaN/govoplan-cases) |
|
||||||
| `govoplan-certificates` | `domain` | `../govoplan-certificates` | [govoplan-certificates](https://git.add-ideas.de/add-ideas/govoplan-certificates) |
|
| `govoplan-certificates` | `domain` | `../govoplan-certificates` | [govoplan-certificates](https://git.add-ideas.de/GovOPlaN/govoplan-certificates) |
|
||||||
| `govoplan-committee` | `domain` | `../govoplan-committee` | [govoplan-committee](https://git.add-ideas.de/add-ideas/govoplan-committee) |
|
| `govoplan-committee` | `domain` | `../govoplan-committee` | [govoplan-committee](https://git.add-ideas.de/GovOPlaN/govoplan-committee) |
|
||||||
| `govoplan-consultation` | `domain` | `../govoplan-consultation` | [govoplan-consultation](https://git.add-ideas.de/add-ideas/govoplan-consultation) |
|
| `govoplan-consultation` | `domain` | `../govoplan-consultation` | [govoplan-consultation](https://git.add-ideas.de/GovOPlaN/govoplan-consultation) |
|
||||||
| `govoplan-contracts` | `domain` | `../govoplan-contracts` | [govoplan-contracts](https://git.add-ideas.de/add-ideas/govoplan-contracts) |
|
| `govoplan-contracts` | `domain` | `../govoplan-contracts` | [govoplan-contracts](https://git.add-ideas.de/GovOPlaN/govoplan-contracts) |
|
||||||
| `govoplan-dashboard` | `platform` | `../govoplan-dashboard` | [govoplan-dashboard](https://git.add-ideas.de/add-ideas/govoplan-dashboard) |
|
| `govoplan-dashboard` | `platform` | `../govoplan-dashboard` | [govoplan-dashboard](https://git.add-ideas.de/GovOPlaN/govoplan-dashboard) |
|
||||||
| `govoplan-dms` | `domain` | `../govoplan-dms` | [govoplan-dms](https://git.add-ideas.de/add-ideas/govoplan-dms) |
|
| `govoplan-dataflow` | `platform` | `../govoplan-dataflow` | [govoplan-dataflow](https://git.add-ideas.de/GovOPlaN/govoplan-dataflow) |
|
||||||
| `govoplan-dist-lists` | `domain` | `../govoplan-dist-lists` | [govoplan-dist-lists](https://git.add-ideas.de/add-ideas/govoplan-dist-lists) |
|
| `govoplan-datasources` | `platform` | `../govoplan-datasources` | [govoplan-datasources](https://git.add-ideas.de/GovOPlaN/govoplan-datasources) |
|
||||||
| `govoplan-docs` | `platform` | `../govoplan-docs` | [govoplan-docs](https://git.add-ideas.de/add-ideas/govoplan-docs) |
|
| `govoplan-decisions` | `domain` | `../govoplan-decisions` | [govoplan-decisions](https://git.add-ideas.de/GovOPlaN/govoplan-decisions) |
|
||||||
| `govoplan-erp` | `domain` | `../govoplan-erp` | [govoplan-erp](https://git.add-ideas.de/add-ideas/govoplan-erp) |
|
| `govoplan-dms` | `domain` | `../govoplan-dms` | [govoplan-dms](https://git.add-ideas.de/GovOPlaN/govoplan-dms) |
|
||||||
| `govoplan-evaluation` | `domain` | `../govoplan-evaluation` | [govoplan-evaluation](https://git.add-ideas.de/add-ideas/govoplan-evaluation) |
|
| `govoplan-dist-lists` | `domain` | `../govoplan-dist-lists` | [govoplan-dist-lists](https://git.add-ideas.de/GovOPlaN/govoplan-dist-lists) |
|
||||||
| `govoplan-facilities` | `domain` | `../govoplan-facilities` | [govoplan-facilities](https://git.add-ideas.de/add-ideas/govoplan-facilities) |
|
| `govoplan-docs` | `platform` | `../govoplan-docs` | [govoplan-docs](https://git.add-ideas.de/GovOPlaN/govoplan-docs) |
|
||||||
| `govoplan-files` | `domain` | `../govoplan-files` | [govoplan-files](https://git.add-ideas.de/add-ideas/govoplan-files) |
|
| `govoplan-encryption` | `platform` | `../govoplan-encryption` | [govoplan-encryption](https://git.add-ideas.de/GovOPlaN/govoplan-encryption) |
|
||||||
| `govoplan-forms` | `domain` | `../govoplan-forms` | [govoplan-forms](https://git.add-ideas.de/add-ideas/govoplan-forms) |
|
| `govoplan-erp` | `domain` | `../govoplan-erp` | [govoplan-erp](https://git.add-ideas.de/GovOPlaN/govoplan-erp) |
|
||||||
| `govoplan-forms-runtime` | `platform` | `../govoplan-forms-runtime` | [govoplan-forms-runtime](https://git.add-ideas.de/add-ideas/govoplan-forms-runtime) |
|
| `govoplan-evaluation` | `domain` | `../govoplan-evaluation` | [govoplan-evaluation](https://git.add-ideas.de/GovOPlaN/govoplan-evaluation) |
|
||||||
| `govoplan-grants` | `domain` | `../govoplan-grants` | [govoplan-grants](https://git.add-ideas.de/add-ideas/govoplan-grants) |
|
| `govoplan-facilities` | `domain` | `../govoplan-facilities` | [govoplan-facilities](https://git.add-ideas.de/GovOPlaN/govoplan-facilities) |
|
||||||
| `govoplan-helpdesk` | `domain` | `../govoplan-helpdesk` | [govoplan-helpdesk](https://git.add-ideas.de/add-ideas/govoplan-helpdesk) |
|
| `govoplan-files` | `domain` | `../govoplan-files` | [govoplan-files](https://git.add-ideas.de/GovOPlaN/govoplan-files) |
|
||||||
| `govoplan-identity` | `platform` | `../govoplan-identity` | [govoplan-identity](https://git.add-ideas.de/add-ideas/govoplan-identity) |
|
| `govoplan-forms` | `domain` | `../govoplan-forms` | [govoplan-forms](https://git.add-ideas.de/GovOPlaN/govoplan-forms) |
|
||||||
| `govoplan-identity-trust` | `platform` | `../govoplan-identity-trust` | [govoplan-identity-trust](https://git.add-ideas.de/add-ideas/govoplan-identity-trust) |
|
| `govoplan-forms-runtime` | `platform` | `../govoplan-forms-runtime` | [govoplan-forms-runtime](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime) |
|
||||||
| `govoplan-idm` | `platform` | `../govoplan-idm` | [govoplan-idm](https://git.add-ideas.de/add-ideas/govoplan-idm) |
|
| `govoplan-grants` | `domain` | `../govoplan-grants` | [govoplan-grants](https://git.add-ideas.de/GovOPlaN/govoplan-grants) |
|
||||||
| `govoplan-inspections` | `domain` | `../govoplan-inspections` | [govoplan-inspections](https://git.add-ideas.de/add-ideas/govoplan-inspections) |
|
| `govoplan-helpdesk` | `domain` | `../govoplan-helpdesk` | [govoplan-helpdesk](https://git.add-ideas.de/GovOPlaN/govoplan-helpdesk) |
|
||||||
| `govoplan-issue-reporting` | `domain` | `../govoplan-issue-reporting` | [govoplan-issue-reporting](https://git.add-ideas.de/add-ideas/govoplan-issue-reporting) |
|
| `govoplan-identity` | `platform` | `../govoplan-identity` | [govoplan-identity](https://git.add-ideas.de/GovOPlaN/govoplan-identity) |
|
||||||
| `govoplan-learning` | `domain` | `../govoplan-learning` | [govoplan-learning](https://git.add-ideas.de/add-ideas/govoplan-learning) |
|
| `govoplan-identity-trust` | `platform` | `../govoplan-identity-trust` | [govoplan-identity-trust](https://git.add-ideas.de/GovOPlaN/govoplan-identity-trust) |
|
||||||
| `govoplan-ledger` | `domain` | `../govoplan-ledger` | [govoplan-ledger](https://git.add-ideas.de/add-ideas/govoplan-ledger) |
|
| `govoplan-idm` | `platform` | `../govoplan-idm` | [govoplan-idm](https://git.add-ideas.de/GovOPlaN/govoplan-idm) |
|
||||||
| `govoplan-mail` | `domain` | `../govoplan-mail` | [govoplan-mail](https://git.add-ideas.de/add-ideas/govoplan-mail) |
|
| `govoplan-inspections` | `domain` | `../govoplan-inspections` | [govoplan-inspections](https://git.add-ideas.de/GovOPlaN/govoplan-inspections) |
|
||||||
| `govoplan-notifications` | `platform` | `../govoplan-notifications` | [govoplan-notifications](https://git.add-ideas.de/add-ideas/govoplan-notifications) |
|
| `govoplan-learning` | `domain` | `../govoplan-learning` | [govoplan-learning](https://git.add-ideas.de/GovOPlaN/govoplan-learning) |
|
||||||
| `govoplan-ops` | `platform` | `../govoplan-ops` | [govoplan-ops](https://git.add-ideas.de/add-ideas/govoplan-ops) |
|
| `govoplan-ledger` | `domain` | `../govoplan-ledger` | [govoplan-ledger](https://git.add-ideas.de/GovOPlaN/govoplan-ledger) |
|
||||||
| `govoplan-organizations` | `platform` | `../govoplan-organizations` | [govoplan-organizations](https://git.add-ideas.de/add-ideas/govoplan-organizations) |
|
| `govoplan-mail` | `domain` | `../govoplan-mail` | [govoplan-mail](https://git.add-ideas.de/GovOPlaN/govoplan-mail) |
|
||||||
| `govoplan-payments` | `domain` | `../govoplan-payments` | [govoplan-payments](https://git.add-ideas.de/add-ideas/govoplan-payments) |
|
| `govoplan-mandates` | `domain` | `../govoplan-mandates` | [govoplan-mandates](https://git.add-ideas.de/GovOPlaN/govoplan-mandates) |
|
||||||
| `govoplan-permits` | `domain` | `../govoplan-permits` | [govoplan-permits](https://git.add-ideas.de/add-ideas/govoplan-permits) |
|
| `govoplan-notifications` | `platform` | `../govoplan-notifications` | [govoplan-notifications](https://git.add-ideas.de/GovOPlaN/govoplan-notifications) |
|
||||||
| `govoplan-policy` | `platform` | `../govoplan-policy` | [govoplan-policy](https://git.add-ideas.de/add-ideas/govoplan-policy) |
|
| `govoplan-ops` | `platform` | `../govoplan-ops` | [govoplan-ops](https://git.add-ideas.de/GovOPlaN/govoplan-ops) |
|
||||||
| `govoplan-poll` | `domain` | `../govoplan-poll` | [govoplan-poll](https://git.add-ideas.de/add-ideas/govoplan-poll) |
|
| `govoplan-organizations` | `platform` | `../govoplan-organizations` | [govoplan-organizations](https://git.add-ideas.de/GovOPlaN/govoplan-organizations) |
|
||||||
| `govoplan-portal` | `domain` | `../govoplan-portal` | [govoplan-portal](https://git.add-ideas.de/add-ideas/govoplan-portal) |
|
| `govoplan-payments` | `domain` | `../govoplan-payments` | [govoplan-payments](https://git.add-ideas.de/GovOPlaN/govoplan-payments) |
|
||||||
| `govoplan-postbox` | `domain` | `../govoplan-postbox` | [govoplan-postbox](https://git.add-ideas.de/add-ideas/govoplan-postbox) |
|
| `govoplan-parties` | `domain` | `../govoplan-parties` | [govoplan-parties](https://git.add-ideas.de/GovOPlaN/govoplan-parties) |
|
||||||
| `govoplan-procurement` | `domain` | `../govoplan-procurement` | [govoplan-procurement](https://git.add-ideas.de/add-ideas/govoplan-procurement) |
|
| `govoplan-permits` | `domain` | `../govoplan-permits` | [govoplan-permits](https://git.add-ideas.de/GovOPlaN/govoplan-permits) |
|
||||||
| `govoplan-records` | `domain` | `../govoplan-records` | [govoplan-records](https://git.add-ideas.de/add-ideas/govoplan-records) |
|
| `govoplan-policy` | `platform` | `../govoplan-policy` | [govoplan-policy](https://git.add-ideas.de/GovOPlaN/govoplan-policy) |
|
||||||
| `govoplan-reporting` | `domain` | `../govoplan-reporting` | [govoplan-reporting](https://git.add-ideas.de/add-ideas/govoplan-reporting) |
|
| `govoplan-poll` | `domain` | `../govoplan-poll` | [govoplan-poll](https://git.add-ideas.de/GovOPlaN/govoplan-poll) |
|
||||||
| `govoplan-resources` | `domain` | `../govoplan-resources` | [govoplan-resources](https://git.add-ideas.de/add-ideas/govoplan-resources) |
|
| `govoplan-portal` | `domain` | `../govoplan-portal` | [govoplan-portal](https://git.add-ideas.de/GovOPlaN/govoplan-portal) |
|
||||||
| `govoplan-risk-compliance` | `domain` | `../govoplan-risk-compliance` | [govoplan-risk-compliance](https://git.add-ideas.de/add-ideas/govoplan-risk-compliance) |
|
| `govoplan-postbox` | `domain` | `../govoplan-postbox` | [govoplan-postbox](https://git.add-ideas.de/GovOPlaN/govoplan-postbox) |
|
||||||
| `govoplan-scheduling` | `domain` | `../govoplan-scheduling` | [govoplan-scheduling](https://git.add-ideas.de/add-ideas/govoplan-scheduling) |
|
| `govoplan-procurement` | `domain` | `../govoplan-procurement` | [govoplan-procurement](https://git.add-ideas.de/GovOPlaN/govoplan-procurement) |
|
||||||
| `govoplan-search` | `platform` | `../govoplan-search` | [govoplan-search](https://git.add-ideas.de/add-ideas/govoplan-search) |
|
| `govoplan-projects` | `domain` | `../govoplan-projects` | [govoplan-projects](https://git.add-ideas.de/GovOPlaN/govoplan-projects) |
|
||||||
| `govoplan-tasks` | `domain` | `../govoplan-tasks` | [govoplan-tasks](https://git.add-ideas.de/add-ideas/govoplan-tasks) |
|
| `govoplan-records` | `domain` | `../govoplan-records` | [govoplan-records](https://git.add-ideas.de/GovOPlaN/govoplan-records) |
|
||||||
| `govoplan-templates` | `domain` | `../govoplan-templates` | [govoplan-templates](https://git.add-ideas.de/add-ideas/govoplan-templates) |
|
| `govoplan-reporting` | `domain` | `../govoplan-reporting` | [govoplan-reporting](https://git.add-ideas.de/GovOPlaN/govoplan-reporting) |
|
||||||
| `govoplan-tenancy` | `platform` | `../govoplan-tenancy` | [govoplan-tenancy](https://git.add-ideas.de/add-ideas/govoplan-tenancy) |
|
| `govoplan-resources` | `domain` | `../govoplan-resources` | [govoplan-resources](https://git.add-ideas.de/GovOPlaN/govoplan-resources) |
|
||||||
| `govoplan-transparency` | `domain` | `../govoplan-transparency` | [govoplan-transparency](https://git.add-ideas.de/add-ideas/govoplan-transparency) |
|
| `govoplan-risk-compliance` | `domain` | `../govoplan-risk-compliance` | [govoplan-risk-compliance](https://git.add-ideas.de/GovOPlaN/govoplan-risk-compliance) |
|
||||||
| `govoplan-workflow` | `platform` | `../govoplan-workflow` | [govoplan-workflow](https://git.add-ideas.de/add-ideas/govoplan-workflow) |
|
| `govoplan-scheduling` | `domain` | `../govoplan-scheduling` | [govoplan-scheduling](https://git.add-ideas.de/GovOPlaN/govoplan-scheduling) |
|
||||||
|
| `govoplan-search` | `platform` | `../govoplan-search` | [govoplan-search](https://git.add-ideas.de/GovOPlaN/govoplan-search) |
|
||||||
|
| `govoplan-services` | `domain` | `../govoplan-services` | [govoplan-services](https://git.add-ideas.de/GovOPlaN/govoplan-services) |
|
||||||
|
| `govoplan-tasks` | `domain` | `../govoplan-tasks` | [govoplan-tasks](https://git.add-ideas.de/GovOPlaN/govoplan-tasks) |
|
||||||
|
| `govoplan-templates` | `domain` | `../govoplan-templates` | [govoplan-templates](https://git.add-ideas.de/GovOPlaN/govoplan-templates) |
|
||||||
|
| `govoplan-tenancy` | `platform` | `../govoplan-tenancy` | [govoplan-tenancy](https://git.add-ideas.de/GovOPlaN/govoplan-tenancy) |
|
||||||
|
| `govoplan-tickets` | `domain` | `../govoplan-tickets` | [govoplan-tickets](https://git.add-ideas.de/GovOPlaN/govoplan-tickets) |
|
||||||
|
| `govoplan-transparency` | `domain` | `../govoplan-transparency` | [govoplan-transparency](https://git.add-ideas.de/GovOPlaN/govoplan-transparency) |
|
||||||
|
| `govoplan-views` | `platform` | `../govoplan-views` | [govoplan-views](https://git.add-ideas.de/GovOPlaN/govoplan-views) |
|
||||||
|
| `govoplan-voting` | `domain` | `../govoplan-voting` | [govoplan-voting](https://git.add-ideas.de/GovOPlaN/govoplan-voting) |
|
||||||
|
| `govoplan-wiki` | `domain` | `../govoplan-wiki` | [govoplan-wiki](https://git.add-ideas.de/GovOPlaN/govoplan-wiki) |
|
||||||
|
| `govoplan-workflow` | `platform` | `../govoplan-workflow` | [govoplan-workflow](https://git.add-ideas.de/GovOPlaN/govoplan-workflow) |
|
||||||
|
| `govoplan-workflow-engine` | `platform` | `../govoplan-workflow-engine` | [govoplan-workflow-engine](https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine) |
|
||||||
|
|
||||||
## Connector
|
## Connector
|
||||||
|
|
||||||
| Repository | Subtype | Local path | Gitea |
|
| Repository | Subtype | Local path | Gitea |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `govoplan-connectors` | `connector-hub` | `../govoplan-connectors` | [govoplan-connectors](https://git.add-ideas.de/add-ideas/govoplan-connectors) |
|
| `govoplan-connectors` | `connector-hub` | `../govoplan-connectors` | [govoplan-connectors](https://git.add-ideas.de/GovOPlaN/govoplan-connectors) |
|
||||||
| `govoplan-fit-connect` | `standard` | `../govoplan-fit-connect` | [govoplan-fit-connect](https://git.add-ideas.de/add-ideas/govoplan-fit-connect) |
|
| `govoplan-fit-connect` | `standard` | `../govoplan-fit-connect` | [govoplan-fit-connect](https://git.add-ideas.de/GovOPlaN/govoplan-fit-connect) |
|
||||||
| `govoplan-rest` | `protocol` | `../govoplan-rest` | [govoplan-rest](https://git.add-ideas.de/add-ideas/govoplan-rest) |
|
| `govoplan-rest` | `protocol` | `../govoplan-rest` | [govoplan-rest](https://git.add-ideas.de/GovOPlaN/govoplan-rest) |
|
||||||
| `govoplan-soap` | `protocol` | `../govoplan-soap` | [govoplan-soap](https://git.add-ideas.de/add-ideas/govoplan-soap) |
|
| `govoplan-soap` | `protocol` | `../govoplan-soap` | [govoplan-soap](https://git.add-ideas.de/GovOPlaN/govoplan-soap) |
|
||||||
| `govoplan-xoev` | `standard` | `../govoplan-xoev` | [govoplan-xoev](https://git.add-ideas.de/add-ideas/govoplan-xoev) |
|
| `govoplan-xoev` | `standard` | `../govoplan-xoev` | [govoplan-xoev](https://git.add-ideas.de/GovOPlaN/govoplan-xoev) |
|
||||||
| `govoplan-xrechnung` | `standard` | `../govoplan-xrechnung` | [govoplan-xrechnung](https://git.add-ideas.de/add-ideas/govoplan-xrechnung) |
|
| `govoplan-xrechnung` | `standard` | `../govoplan-xrechnung` | [govoplan-xrechnung](https://git.add-ideas.de/GovOPlaN/govoplan-xrechnung) |
|
||||||
| `govoplan-xta-osci` | `standard` | `../govoplan-xta-osci` | [govoplan-xta-osci](https://git.add-ideas.de/add-ideas/govoplan-xta-osci) |
|
| `govoplan-xta-osci` | `standard` | `../govoplan-xta-osci` | [govoplan-xta-osci](https://git.add-ideas.de/GovOPlaN/govoplan-xta-osci) |
|
||||||
|
|
||||||
## Website
|
## Website
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,13 @@ one place. If a deployment profile later needs pinned SHAs for every repository,
|
|||||||
generate that lock as a release artifact instead of making day-to-day
|
generate that lock as a release artifact instead of making day-to-day
|
||||||
development depend on submodule updates.
|
development depend on submodule updates.
|
||||||
|
|
||||||
|
Module release tags also publish wheels and WebUI tarballs to the organization
|
||||||
|
PyPI/npm registries. The meta release resolves exact versions into a hash-bound
|
||||||
|
package lock before producing the signed OCI runtime. See
|
||||||
|
`docs/PACKAGE_REGISTRY_RELEASES.md`. Git tags remain source provenance; package
|
||||||
|
registries are reusable artifact transport; the signed runtime manifest and
|
||||||
|
digest-pinned images remain production authority.
|
||||||
|
|
||||||
## Docker Placement
|
## Docker Placement
|
||||||
|
|
||||||
Whole-product Docker and production-like deployment composition belongs in
|
Whole-product Docker and production-like deployment composition belongs in
|
||||||
|
|||||||
@@ -0,0 +1,259 @@
|
|||||||
|
# Scaling And Multi-Host Deployment
|
||||||
|
|
||||||
|
For the exact external handoff, least-privilege collector permissions and live
|
||||||
|
two-node acceptance procedure, see
|
||||||
|
[`PRODUCTION_TARGET_HANDOFF.md`](PRODUCTION_TARGET_HANDOFF.md).
|
||||||
|
|
||||||
|
## Implemented Contract
|
||||||
|
|
||||||
|
GovOPlaN now supports a stateless application tier backed by logically shared
|
||||||
|
state services. The runtime roles are independently replaceable API, WebUI,
|
||||||
|
worker, and scheduler processes. Every replica in one installation must use the
|
||||||
|
same immutable release composition and the same:
|
||||||
|
|
||||||
|
- `GOVOPLAN_INSTALLATION_ID`;
|
||||||
|
- PostgreSQL database;
|
||||||
|
- Redis broker and coordination service;
|
||||||
|
- `MASTER_KEY_B64` and deployment secret references;
|
||||||
|
- enabled-module graph;
|
||||||
|
- S3-compatible object-storage namespace.
|
||||||
|
|
||||||
|
The application tier must not use node-local durable business data in a
|
||||||
|
multi-host deployment. Files owns managed file metadata while Core provides the
|
||||||
|
storage-backend contract. Campaign build artifacts are stored under opaque
|
||||||
|
object keys and workers read those objects from the shared backend. Temporary
|
||||||
|
build and materialization directories may remain node-local because they are
|
||||||
|
discardable.
|
||||||
|
|
||||||
|
Core validates three explicit state profiles:
|
||||||
|
|
||||||
|
| Profile | Supported shape | Storage rule |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `local` | One API and one worker process for development | Local filesystem permitted. |
|
||||||
|
| `host-shared` | Multiple processes on one Docker host | A shared host volume is permitted; PostgreSQL and Redis are required. |
|
||||||
|
| `shared` | Multiple independent hosts | PostgreSQL, Redis, and S3-compatible object storage are required. |
|
||||||
|
|
||||||
|
`shared` also requires a stable installation identifier. Module package
|
||||||
|
mutation is blocked in this profile: build and verify a new immutable release,
|
||||||
|
then roll the complete cluster to it.
|
||||||
|
|
||||||
|
## Same-Host Compose
|
||||||
|
|
||||||
|
The generated Compose bundle provides:
|
||||||
|
|
||||||
|
```text
|
||||||
|
client -> TLS proxy -> HAProxy -> WebUI replicas -> HAProxy -> API replicas
|
||||||
|
|
||||||
|
API/worker/scheduler -> PostgreSQL
|
||||||
|
-> Redis
|
||||||
|
-> local volume, managed Garage, or external S3
|
||||||
|
```
|
||||||
|
|
||||||
|
HAProxy discovers Compose replicas through Docker DNS and performs health-aware
|
||||||
|
balancing without mounting the Docker socket. This improves concurrency and
|
||||||
|
permits process replacement, but the Docker host and installer-managed stateful
|
||||||
|
services remain single failure domains. Generated Compose therefore declares
|
||||||
|
the `host-shared` state profile even when its shared storage happens to be an
|
||||||
|
external S3 service. Its API backend checks `/health/ready`, so drain or
|
||||||
|
coordination loss removes a replica from rotation. Container, load-balancer,
|
||||||
|
and Kubernetes probes send the configured public host explicitly, keeping
|
||||||
|
readiness compatible with strict trusted-host validation.
|
||||||
|
|
||||||
|
Managed Garage is a convenient single-node S3-compatible service. It is not a
|
||||||
|
multi-host storage cluster. Use an independently operated Garage cluster or
|
||||||
|
another S3-compatible service for the `shared` profile.
|
||||||
|
|
||||||
|
## Kubernetes Export
|
||||||
|
|
||||||
|
The deployment compiler exports a stateless Kubernetes runtime when PostgreSQL,
|
||||||
|
Redis, and S3 are all external:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python tools/deployment/govoplan-deploy.py render-kubernetes \
|
||||||
|
--directory /srv/govoplan/default \
|
||||||
|
--namespace govoplan \
|
||||||
|
--secret-name govoplan-runtime \
|
||||||
|
--tls-secret-name govoplan-tls \
|
||||||
|
--ingress-class-name nginx \
|
||||||
|
--output /srv/govoplan/default/kubernetes.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The export contains a Namespace, tokenless ServiceAccount, non-secret
|
||||||
|
ConfigMap, API/WebUI/worker/scheduler Deployments, Services, Pod disruption
|
||||||
|
budgets, Ingress, and a release-specific migration Job. It deliberately emits
|
||||||
|
no Secret values, persistent volume, PostgreSQL, Redis, or object-store
|
||||||
|
deployment. Export is rejected unless both release images use immutable
|
||||||
|
`image@sha256:...` references.
|
||||||
|
|
||||||
|
Create the named Secret through the cluster's secret-management path. The
|
||||||
|
command prints the exact required key contract. Review the generated
|
||||||
|
`FORWARDED_ALLOW_IPS` value and replace it with the exact ingress-proxy network
|
||||||
|
before production use.
|
||||||
|
|
||||||
|
The generated containers run as non-root with a read-only root filesystem and
|
||||||
|
an ephemeral `/tmp`. Runtime Deployments wait for the exact configured database
|
||||||
|
migration heads before starting. The API exposes `/health/ready`, which fails
|
||||||
|
while that API node is draining or cannot prove its runtime-coordination
|
||||||
|
heartbeat.
|
||||||
|
|
||||||
|
## Runtime Coordination
|
||||||
|
|
||||||
|
Each API and worker incarnation registers in PostgreSQL with its role, software
|
||||||
|
version, module-composition hash, queue set, and heartbeat. Ops shows active,
|
||||||
|
draining, stopped, and stale nodes and compares active counts with configured
|
||||||
|
replica expectations.
|
||||||
|
|
||||||
|
An operator may request or cancel drain from Ops:
|
||||||
|
|
||||||
|
- API readiness becomes unavailable on the next heartbeat so the load balancer
|
||||||
|
stops assigning new requests.
|
||||||
|
- A worker stops consuming its configured queues and may finish work already
|
||||||
|
claimed by that process.
|
||||||
|
- A stale process incarnation cannot overwrite a replacement incarnation's
|
||||||
|
heartbeat.
|
||||||
|
- A coordination outage removes API readiness and cancels worker consumers;
|
||||||
|
the existing incarnation must heartbeat successfully before either resumes.
|
||||||
|
|
||||||
|
Singleton work uses PostgreSQL-backed leases with monotonically increasing
|
||||||
|
fencing tokens. The generated scheduler runs Celery beat through
|
||||||
|
`govoplan_core.commands.fenced_run`; loss of its lease terminates the child and
|
||||||
|
returns a distinct failure code. A fenced business operation must validate the
|
||||||
|
same lease token immediately before committing its effect.
|
||||||
|
|
||||||
|
## Release Ordering
|
||||||
|
|
||||||
|
Use this order for every multi-replica rollout:
|
||||||
|
|
||||||
|
1. Verify immutable image identities, module composition, external state
|
||||||
|
reachability, backup evidence, and the generated plan.
|
||||||
|
2. Drain application replicas when the migration compatibility declaration
|
||||||
|
requires it.
|
||||||
|
3. Run the release-specific migration Job exactly once. PostgreSQL advisory
|
||||||
|
locking serializes all Core and module migration tasks across competing
|
||||||
|
deployment jobs.
|
||||||
|
4. Let runtime init containers run `wait_for_database`. They wait for exact
|
||||||
|
configured Alembic heads and never mutate schema.
|
||||||
|
5. Roll API, workers, scheduler, and WebUI using health-aware replacement.
|
||||||
|
6. Verify runtime composition, expected replica counts, queue consumers,
|
||||||
|
object-storage round trips, and recovery status in Ops.
|
||||||
|
|
||||||
|
Applying the complete generated manifest is fail-closed: runtime pods remain in
|
||||||
|
their init phase until the migration Job reaches the expected heads. A second
|
||||||
|
release may be submitted concurrently, but advisory locking prevents concurrent
|
||||||
|
schema mutation and each release has a distinct migration Job name.
|
||||||
|
|
||||||
|
## Storage Trust Boundary
|
||||||
|
|
||||||
|
Installer-managed Garage uses its exact generated endpoint. An arbitrary
|
||||||
|
external S3 endpoint is accepted only when the deployment explicitly sets
|
||||||
|
`FILE_STORAGE_S3_ENDPOINT_TRUSTED=true`; that endpoint must be a clean HTTPS
|
||||||
|
origin without embedded credentials, query, fragment, or path. This is an
|
||||||
|
operator trust declaration, not a user-controlled connector bypass. Operators
|
||||||
|
remain responsible for DNS, certificate, network-egress, bucket-policy,
|
||||||
|
versioning, and lifecycle controls.
|
||||||
|
|
||||||
|
## Capacity
|
||||||
|
|
||||||
|
- Set `GOVOPLAN_DB_CONNECTION_LIMIT` to the PostgreSQL role's effective
|
||||||
|
connection limit. The Kubernetes export reserves
|
||||||
|
`GOVOPLAN_DB_CONNECTION_RESERVE` connections and rejects a topology whose
|
||||||
|
calculated rolling-update peak would exceed the remainder. The calculation
|
||||||
|
includes API pools, every Celery parent and prefork child, the scheduler,
|
||||||
|
migration, and one surge replica per deployment. Role-specific pool and
|
||||||
|
overflow values are emitted into each workload rather than inherited from one
|
||||||
|
unconstrained global default.
|
||||||
|
- Scale workers by queue, with upper bounds based on external provider limits.
|
||||||
|
`GOVOPLAN_WORKER_POOLS` may contain a JSON list of exact queue owners, for
|
||||||
|
example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{"name":"delivery","queues":["send_email","append_sent"],"replicas":2,"concurrency":2},
|
||||||
|
{"name":"platform","queues":["events","workflow","default"],"replicas":2,"concurrency":2}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Pool replica totals must equal `replicas.worker`, and the pools must cover
|
||||||
|
`CELERY_QUEUES` exactly without duplicate ownership. Each pool receives its
|
||||||
|
own Deployment, disruption budget, topology-spread selector, runtime identity,
|
||||||
|
and declared concurrency.
|
||||||
|
- Keep one fenced scheduler rather than load-balancing schedulers.
|
||||||
|
- Increase WebUI replicas for asset/proxy capacity.
|
||||||
|
- Measure request latency, database query time and locks, active connections,
|
||||||
|
queue age, retry rate, storage latency, and provider throttling before adding
|
||||||
|
replicas.
|
||||||
|
|
||||||
|
Workers compete for Redis-backed work and are not placed behind a load balancer.
|
||||||
|
SMTP, IMAP, directory, connector, workflow, dataflow, and reporting queues often
|
||||||
|
hit external-system limits before host CPU is exhausted.
|
||||||
|
|
||||||
|
## What This Does Not Claim
|
||||||
|
|
||||||
|
The implemented contract provides stateless runtime placement, shared artifact
|
||||||
|
access, node visibility, drain controls, migration serialization, and scheduler
|
||||||
|
fencing. It does not by itself provide:
|
||||||
|
|
||||||
|
- a highly available PostgreSQL, Redis, or object-store deployment;
|
||||||
|
- automatic PostgreSQL/object backup creation or point-in-time recovery;
|
||||||
|
- autoscaling policy;
|
||||||
|
- central logs, metrics, traces, or alert routing;
|
||||||
|
- certificate portability between independently managed ingress providers;
|
||||||
|
- automatic reconciliation of every possible module side effect;
|
||||||
|
- a service-level availability guarantee.
|
||||||
|
|
||||||
|
The deployer verifies and gates migrations on signed coordinated backup and
|
||||||
|
isolated-restore evidence, but backup capture and restoration remain owned by
|
||||||
|
the selected state-service providers. Before claiming high
|
||||||
|
availability, drill replica loss, rolling replacement, session continuity, job
|
||||||
|
redelivery, scheduler failover, migration exclusion, object-store outage, and a
|
||||||
|
coordinated database/object/key restore. Recovery rules and evidence are
|
||||||
|
defined in [Recovery And Rollback Guarantees](RECOVERY_AND_ROLLBACK_GUARANTEES.md).
|
||||||
|
|
||||||
|
## Worker Delivery Evidence
|
||||||
|
|
||||||
|
The module-matrix workflow runs `tools/checks/worker-runtime-drill.py` against a
|
||||||
|
real isolated Redis database. The drill starts supervised Celery worker
|
||||||
|
processes and records four guarantees without accessing tenant data:
|
||||||
|
|
||||||
|
1. a task published through the broker is consumed exactly once;
|
||||||
|
2. an application retry is delivered again and completes;
|
||||||
|
3. warm `SIGTERM` lets an in-flight late-ack task complete before shutdown; and
|
||||||
|
4. loss of a worker after task start causes the unacknowledged task to be
|
||||||
|
redelivered after the configured visibility timeout.
|
||||||
|
|
||||||
|
Run the same drill with the release Python environment and target Redis before
|
||||||
|
promoting a worker composition. Use a dedicated Redis database, retain the JSON
|
||||||
|
evidence, and set `CELERY_VISIBILITY_TIMEOUT_SECONDS` above the longest supported
|
||||||
|
business-task duration. The short visibility timeout used by CI is an isolated
|
||||||
|
test setting, not a production recommendation.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOVOPLAN_WORKER_DRILL_REDIS_URL=redis://redis.example.test:6379/15 \
|
||||||
|
.venv/bin/python tools/checks/worker-runtime-drill.py \
|
||||||
|
--output evidence/worker-runtime.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Live Multi-Host Evidence
|
||||||
|
|
||||||
|
After deploying a pinned release on at least two Kubernetes nodes, create an API
|
||||||
|
key with Ops read scope and run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GOVOPLAN_OPS_API_KEY='...'
|
||||||
|
python tools/deployment/govoplan-deploy.py verify-kubernetes \
|
||||||
|
--directory /srv/govoplan/installation \
|
||||||
|
--namespace govoplan
|
||||||
|
```
|
||||||
|
|
||||||
|
The command fails unless API and WebUI pods are ready on at least two nodes,
|
||||||
|
all rendered deployments are available, Ops reports a consistent release and
|
||||||
|
module composition, every declared worker queue is served, and the calculated
|
||||||
|
database peak remains below its budget. It writes a private, sanitized JSON
|
||||||
|
record under the installation evidence directory and never retains the API key.
|
||||||
|
|
||||||
|
Use `--exercise-api-pod-loss` in an approved drill window to delete one API pod,
|
||||||
|
observe the public readiness path continuously, and record its replacement.
|
||||||
|
This proves the bounded stateless-node-loss slice only. Session continuity,
|
||||||
|
accepted-job redelivery, state-service failover, and coordinated restore remain
|
||||||
|
separate target exercises whose signed evidence is governed by
|
||||||
|
`docs/TARGET_MATURITY_EVIDENCE_RUNBOOK.md` and GovOPlaN #37.
|
||||||
+27
-7
@@ -50,6 +50,10 @@ accept findings, but scanner execution errors and malformed JSON/SARIF reports
|
|||||||
always fail the run. The manifest lists the expected, present, and missing
|
always fail the run. The manifest lists the expected, present, and missing
|
||||||
reports for that invocation. Validation and checksums use that explicit set, so
|
reports for that invocation. Validation and checksums use that explicit set, so
|
||||||
reusing a report directory cannot make stale output look like part of a new run.
|
reusing a report directory cannot make stale output look like part of a new run.
|
||||||
|
It also contains `coverage_status` and structured `scanner_coverage` entries.
|
||||||
|
Every required scanner is recorded as `no-findings`, `findings`,
|
||||||
|
`scanner-failure`, or `skipped`; this makes an incomplete local run visible
|
||||||
|
without treating it as a clean audit.
|
||||||
|
|
||||||
The wrapper tags the toolbox image by a fingerprint of the Dockerfile,
|
The wrapper tags the toolbox image by a fingerprint of the Dockerfile,
|
||||||
`requirements-audit.txt`, and the Semgrep smoke-test inputs. If those inputs have
|
`requirements-audit.txt`, and the Semgrep smoke-test inputs. If those inputs have
|
||||||
@@ -90,6 +94,12 @@ tools/checks/security-audit/run.sh --mode quick --scope current --update --build
|
|||||||
- `ci`: quick plus Semgrep public registry rulesets, Trivy, pip-audit, npm audit.
|
- `ci`: quick plus Semgrep public registry rulesets, Trivy, pip-audit, npm audit.
|
||||||
- `full`: ci plus OSV-Scanner, jscpd, Radon, and Xenon.
|
- `full`: ci plus OSV-Scanner, jscpd, Radon, and Xenon.
|
||||||
|
|
||||||
|
The Gitea workflow uses `full` mode so its coverage contract includes every
|
||||||
|
scanner above. Missing scanners fail strict runs and all Actions runs, even
|
||||||
|
while actual findings remain report-only. Local report-only runs may finish
|
||||||
|
with missing tools for diagnostics, but their manifest is marked
|
||||||
|
`coverage_status: incomplete`.
|
||||||
|
|
||||||
Semgrep and Trivy are invoked with finding-sensitive exit codes. Their exit 1
|
Semgrep and Trivy are invoked with finding-sensitive exit codes. Their exit 1
|
||||||
is therefore a finding under the wrapper contract; higher exit codes, missing
|
is therefore a finding under the wrapper contract; higher exit codes, missing
|
||||||
output, invalid JSON/SARIF, and scanner error payloads are execution failures.
|
output, invalid JSON/SARIF, and scanner error payloads are execution failures.
|
||||||
@@ -103,7 +113,7 @@ baseline.
|
|||||||
|
|
||||||
## Gating
|
## Gating
|
||||||
|
|
||||||
The initial Gitea workflow runs in report-only mode:
|
The Gitea workflow currently runs findings in report-only mode:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
SECURITY_AUDIT_FAIL_ON_FINDINGS=0
|
SECURITY_AUDIT_FAIL_ON_FINDINGS=0
|
||||||
@@ -119,13 +129,13 @@ SECURITY_AUDIT_FAIL_ON_FINDINGS=1
|
|||||||
or run locally with:
|
or run locally with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tools/checks/security-audit/run.sh --mode ci --scope current --strict
|
tools/checks/security-audit/run.sh --mode full --scope govoplan --strict
|
||||||
```
|
```
|
||||||
|
|
||||||
## Audit Burndown Workflow
|
## Audit Burndown Workflow
|
||||||
|
|
||||||
Treat Gitea issues as the active audit state. A full GovOPlaN audit should
|
Treat Gitea issues as the active audit state. A full GovOPlaN audit should
|
||||||
produce one tracker issue in `add-ideas/govoplan` and child issues in the
|
produce one tracker issue in `GovOPlaN/govoplan` and child issues in the
|
||||||
repository that owns each fix.
|
repository that owns each fix.
|
||||||
|
|
||||||
Use the tracker issue for:
|
Use the tracker issue for:
|
||||||
@@ -153,10 +163,20 @@ important scanner counts in the tracker issue.
|
|||||||
|
|
||||||
The jscpd step is intentionally scoped to application and test source. It
|
The jscpd step is intentionally scoped to application and test source. It
|
||||||
excludes documentation snippets, package manifests, generated translations,
|
excludes documentation snippets, package manifests, generated translations,
|
||||||
public SVG assets, workflow YAML, and declarative backend schema JSON because
|
public SVG assets and catalog output, workflow YAML, declarative backend schema
|
||||||
those reports produce metadata or asset repetition rather than actionable source
|
JSON, the generated migration baseline, and mirrored development migration
|
||||||
duplication. Keep exclusions narrow and create child issues for source-code
|
directories because those reports produce metadata or generated-source
|
||||||
clusters that cross module ownership or make behavior harder to change safely.
|
repetition rather than actionable source duplication. Keep exclusions narrow
|
||||||
|
and create child issues for source-code clusters that cross module ownership or
|
||||||
|
make behavior harder to change safely.
|
||||||
|
|
||||||
|
The 2026-08-02 full-workspace baseline covered 64 repositories and reported
|
||||||
|
1.82% duplicated lines before those generated-source exclusions. The reviewed
|
||||||
|
high-value clusters were catalog acceptance persistence, release publication
|
||||||
|
result assembly, and local WebUI JSON mutation wrappers. Similar Dataflow and
|
||||||
|
Workflow graph/governance code remains independently owned until its shared
|
||||||
|
contract is stable enough for Core; a raw similarity score is not grounds for a
|
||||||
|
module-to-module dependency.
|
||||||
|
|
||||||
## Image Freshness
|
## Image Freshness
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ This is a product-level story owned by the GovOPlaN platform rather than by an
|
|||||||
individual domain module. It joins installation, module lifecycle, operations,
|
individual domain module. It joins installation, module lifecycle, operations,
|
||||||
configuration packages, and release provenance into one administrator journey.
|
configuration packages, and release provenance into one administrator journey.
|
||||||
The canonical backlog item is
|
The canonical backlog item is
|
||||||
[GovOPlaN #13](https://git.add-ideas.de/add-ideas/govoplan/issues/13).
|
[GovOPlaN #13](https://git.add-ideas.de/GovOPlaN/govoplan/issues/13).
|
||||||
|
|
||||||
## Terms
|
## Terms
|
||||||
|
|
||||||
@@ -139,6 +139,27 @@ The canonical backlog item is
|
|||||||
|
|
||||||
## Implementation slices
|
## Implementation slices
|
||||||
|
|
||||||
|
Implementation status as of the current source tree:
|
||||||
|
|
||||||
|
- Slice 1 has a published production-artifact baseline. Immutable
|
||||||
|
[`v0.1.14`](https://git.add-ideas.de/GovOPlaN/govoplan/releases/tag/v0.1.14)
|
||||||
|
binds source commit `1f039dd39c1ce2672f4978c8abc6dff862ef1445`, a signed
|
||||||
|
one-file deployer, exact API/Web and managed-dependency image digests,
|
||||||
|
composition, SBOMs, and provenance. Runtime Distribution
|
||||||
|
[run #459](https://git.add-ideas.de/GovOPlaN/govoplan/actions/runs/459)
|
||||||
|
passed migrations, schema checks, non-root API/Web readiness, and worker
|
||||||
|
delivery/shutdown on both amd64 and arm64. Each future release must renew the
|
||||||
|
evidence, and a real installation must still produce topology-specific
|
||||||
|
ingress, failover, backup, and recovery receipts.
|
||||||
|
- Slice 6 has a working application-tier foundation: state profiles, shared
|
||||||
|
object storage, runtime node registration/heartbeats/drain, fenced scheduler,
|
||||||
|
migration serialization, exact-head startup waiting, Ops visibility, and a
|
||||||
|
Kubernetes export. Production acceptance still requires topology-specific
|
||||||
|
failover and restore drills.
|
||||||
|
- The recovery foundation for slices 4 and 5 is implemented as a Core recovery
|
||||||
|
ledger and deployment operation journal. Automatic database backup and broad
|
||||||
|
adoption by module-owned external effects remain open work.
|
||||||
|
|
||||||
1. **Reproducible Core-only distribution.** Publish pinned multi-architecture
|
1. **Reproducible Core-only distribution.** Publish pinned multi-architecture
|
||||||
images, signed distribution manifest, Core-only Compose profile, bootstrap
|
images, signed distribution manifest, Core-only Compose profile, bootstrap
|
||||||
preflight, generated secrets, readiness, and idempotent rerun/repair.
|
preflight, generated secrets, readiness, and idempotent rerun/repair.
|
||||||
@@ -155,9 +176,11 @@ The canonical backlog item is
|
|||||||
5. **Safe module update.** Add drain/maintenance coordination, backup gate,
|
5. **Safe module update.** Add drain/maintenance coordination, backup gate,
|
||||||
migration compatibility window, reconnectable progress, health verification,
|
migration compatibility window, reconnectable progress, health verification,
|
||||||
retry/recovery, and update notification.
|
retry/recovery, and update notification.
|
||||||
6. **Stateless replica profile.** Remove remaining local-runtime assumptions,
|
6. **Stateless replica profile.** Continue module adoption and operational
|
||||||
expose role-specific commands/images, implement worker registration/drain,
|
proof for the implemented role commands, shared-state validation, runtime
|
||||||
and prove multiple API and worker replicas against shared dependencies.
|
registration/drain, fenced scheduler, and Kubernetes application-tier
|
||||||
|
export. Prove multiple API and worker replicas against the target shared
|
||||||
|
dependencies.
|
||||||
7. **Configuration revision model.** Define provider export/import schemas,
|
7. **Configuration revision model.** Define provider export/import schemas,
|
||||||
canonical serialization, secret references, validation/diff, immutable
|
canonical serialization, secret references, validation/diff, immutable
|
||||||
revision storage, audit, apply, and undo-as-new-revision.
|
revision storage, audit, apply, and undo-as-new-revision.
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# Target Maturity Evidence Runbook
|
||||||
|
|
||||||
|
For authority-key generation, container isolation and the concrete inputs that
|
||||||
|
must be supplied by the target owner and independent production approver, see
|
||||||
|
[`PRODUCTION_TARGET_HANDOFF.md`](PRODUCTION_TARGET_HANDOFF.md).
|
||||||
|
|
||||||
|
This runbook turns retained target-environment results into a sanitized,
|
||||||
|
signed GovOPlaN capability-fit proof. It does not make a deployment suitable,
|
||||||
|
certified, supported, or production-approved by itself. The proof records what
|
||||||
|
independent authorities assessed against one exact installed release.
|
||||||
|
|
||||||
|
## Roles and custody
|
||||||
|
|
||||||
|
Use separate trust domains for release signing, installation receipts,
|
||||||
|
boundary assessment, and production approval. A private proof key must be
|
||||||
|
provisioned outside the assessed application and its matching public key must
|
||||||
|
already exist in a separately managed
|
||||||
|
`capability-fit-proof-authority-keyring.schema.json` document. Do not store
|
||||||
|
private keys, raw reports, credentials, personal data, backup material, or
|
||||||
|
target endpoints in Git.
|
||||||
|
|
||||||
|
Each authority key lists only the scopes that role may attest. At least one
|
||||||
|
supplied signing key must cover every claim, and the issuer rejects a key that:
|
||||||
|
|
||||||
|
- is absent, inactive, expired, or revoked in the authority keyring;
|
||||||
|
- expires before the proof;
|
||||||
|
- does not match its independently provisioned public key;
|
||||||
|
- reuses release-catalog or installer-authority key material.
|
||||||
|
|
||||||
|
## Target run
|
||||||
|
|
||||||
|
Install one pinned catalog release and issue its installed-composition receipt
|
||||||
|
with `tools/assessments/installer-receipt.py`. Exercise the actual target
|
||||||
|
topology, including:
|
||||||
|
|
||||||
|
- PostgreSQL and Redis as shared state services;
|
||||||
|
- shared S3-compatible object storage;
|
||||||
|
- at least two stateless API replicas and the intended worker topology;
|
||||||
|
- fenced singleton work, ingress, certificates, proxy headers, and the real
|
||||||
|
network/trust boundary;
|
||||||
|
- provider health and freshness for every provider required by the product;
|
||||||
|
- monitoring, alerting, failure response, accessibility, privacy, and security
|
||||||
|
controls;
|
||||||
|
- backup, isolated restore, failed-deployment rollback, and forward recovery.
|
||||||
|
|
||||||
|
For the recovery claim, retain the observed recovery point, measured RPO and
|
||||||
|
RTO, database/object-store consistency result, and semantic reconstruction of
|
||||||
|
the institutional and Service/Form reference journeys. Measure RPO from the
|
||||||
|
last acknowledged durable effect that survives recovery and RTO until service
|
||||||
|
health plus semantic reconstruction pass. Failed runs are evidence too and
|
||||||
|
must use a negative result.
|
||||||
|
|
||||||
|
The private reports stay in the approved evidence store. Give each report an
|
||||||
|
opaque artifact ID and each evaluated control a versioned opaque control ID.
|
||||||
|
|
||||||
|
## Private claim manifest
|
||||||
|
|
||||||
|
Create a private manifest conforming to
|
||||||
|
`capability-fit-boundary-run.schema.json`. Relative artifact paths resolve from
|
||||||
|
the manifest directory. Paths are read and hashed by the issuer and are never
|
||||||
|
copied into the signed output.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "./capability-fit-boundary-run.schema.json",
|
||||||
|
"schema_version": "0.1.0",
|
||||||
|
"evidence_kind": "govoplan.capability-fit-boundary-run",
|
||||||
|
"proof_id": "target:production:20260802",
|
||||||
|
"expires_at": "2026-09-01T00:00:00Z",
|
||||||
|
"claims": [
|
||||||
|
{
|
||||||
|
"scope": "target_environment",
|
||||||
|
"result": "passed",
|
||||||
|
"control_ids": ["topology:shared-state-v1"],
|
||||||
|
"artifacts": [
|
||||||
|
{"artifact_id": "target:run-20260802", "path": "private/target.json"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "recovery",
|
||||||
|
"result": "passed",
|
||||||
|
"control_ids": ["recovery:restore-rollback-v1"],
|
||||||
|
"artifacts": [
|
||||||
|
{"artifact_id": "recovery:run-20260802", "path": "private/recovery.json"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference readiness needs positive `target_environment`, `accessibility`,
|
||||||
|
`privacy`, `security`, `operations`, and `recovery` claims. Provider acceptance
|
||||||
|
and production approval are separate scopes. A production-approval authority
|
||||||
|
must not approve its own unreviewed target run.
|
||||||
|
|
||||||
|
## Issue and verify
|
||||||
|
|
||||||
|
Issue only while the signed installer observation is current. Repeat
|
||||||
|
`--signing-key` when multiple independent roles are needed. The command first
|
||||||
|
verifies the catalog, independent catalog trust root, exact installed payload,
|
||||||
|
installer receipt, and installer authority. It then hashes artifacts, signs the
|
||||||
|
sanitized proof, verifies it immediately, and writes both proof and review with
|
||||||
|
atomic private-file permissions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.venv/bin/python tools/assessments/boundary-evidence.py \
|
||||||
|
--assessment /srv/govoplan/assessment.json \
|
||||||
|
--catalog /srv/govoplan/catalogs/stable.json \
|
||||||
|
--keyring /srv/govoplan/catalogs/keyring.json \
|
||||||
|
--trusted-keyring /srv/govoplan/trust/catalog-keyring.json \
|
||||||
|
--installed-evidence /srv/govoplan/evidence/installed.json \
|
||||||
|
--installer-receipt /srv/govoplan/evidence/installer-receipt.json \
|
||||||
|
--installer-authority-keyring /srv/govoplan/trust/installer-authorities.json \
|
||||||
|
--claims /srv/govoplan/evidence/private/target-run.json \
|
||||||
|
--authority-keyring /srv/govoplan/trust/proof-authorities.json \
|
||||||
|
--signing-key authority-target=/run/secrets/target-proof-ed25519.pem \
|
||||||
|
--output /srv/govoplan/evidence/target-proof.json \
|
||||||
|
--review-output /srv/govoplan/evidence/target-proof-review.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `--expected-external-provider-subject provider-production` when the claim
|
||||||
|
manifest contains `external_providers`. This value is an opaque deployment ID,
|
||||||
|
not a URL or credential.
|
||||||
|
|
||||||
|
## Promotion gate
|
||||||
|
|
||||||
|
The general verifier can now be made admission-enforcing. These switches return
|
||||||
|
a blocking exit status when a required claim is absent, expired, negative,
|
||||||
|
revoked, or bound to another assessment, release, installation, or subject:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./.venv/bin/python tools/assessments/capability-fit.py \
|
||||||
|
--catalog /srv/govoplan/catalogs/stable.json \
|
||||||
|
--keyring /srv/govoplan/catalogs/keyring.json \
|
||||||
|
--trusted-keyring /srv/govoplan/trust/catalog-keyring.json \
|
||||||
|
--installed-evidence /srv/govoplan/evidence/installed.json \
|
||||||
|
--installer-receipt /srv/govoplan/evidence/installer-receipt.json \
|
||||||
|
--installer-authority-keyring /srv/govoplan/trust/installer-authorities.json \
|
||||||
|
--boundary-evidence /srv/govoplan/evidence/target-proof.json \
|
||||||
|
--boundary-authority-keyring /srv/govoplan/trust/proof-authorities.json \
|
||||||
|
--require-reference-readiness \
|
||||||
|
--require-production-approval \
|
||||||
|
--output /srv/govoplan/evidence/admission-review.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `--require-external-provider-proof` as well when the promoted product
|
||||||
|
requires an external provider. Live admission must not use
|
||||||
|
`--verification-time`; that switch is only for clearly labelled historical
|
||||||
|
review.
|
||||||
|
|
||||||
|
## Renewal and failure
|
||||||
|
|
||||||
|
Renew evidence after release, installed composition, deployment, control, or
|
||||||
|
provider changes and before expiry. Revoke an authority key immediately after
|
||||||
|
custody loss and rerun the affected assessment with a new independent key.
|
||||||
|
Never copy a previous positive claim to a new release. Preserve negative and
|
||||||
|
superseded receipts according to the approved evidence-retention policy.
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# GovOPlaN Views Architecture
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
GovOPlaN Views are governed presentation projections for a task,
|
||||||
|
responsibility, or workflow step. A View can reduce the visible modules,
|
||||||
|
navigation entries, routes, page sections, and commands to the interface
|
||||||
|
needed for the current job.
|
||||||
|
|
||||||
|
Views are optional. If `govoplan-views` is not installed or enabled, the normal
|
||||||
|
permission-derived interface remains unchanged.
|
||||||
|
|
||||||
|
## Security Boundary
|
||||||
|
|
||||||
|
A View is not an authorization mechanism.
|
||||||
|
|
||||||
|
- Access, tenant isolation, resource guards, and backend permission checks
|
||||||
|
remain authoritative.
|
||||||
|
- A View may hide an interface surface that the actor is otherwise allowed to
|
||||||
|
use.
|
||||||
|
- A View can never expose a route, action, tenant, or resource that normal
|
||||||
|
authorization denies.
|
||||||
|
- An authorized deep link outside the current View should offer an explicit
|
||||||
|
temporary escape or View switch. It must not be presented as a permission
|
||||||
|
denial.
|
||||||
|
|
||||||
|
This boundary lets Views improve focus without creating a second, weaker RBAC
|
||||||
|
system.
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
Core owns the versioned, module-neutral surface contract and WebUI runtime
|
||||||
|
hooks. Modules declare stable surfaces and use shared hooks to respect the
|
||||||
|
effective projection. Modules do not import `govoplan-views`.
|
||||||
|
|
||||||
|
`govoplan-views` owns:
|
||||||
|
|
||||||
|
- draft and immutable published View revisions
|
||||||
|
- system, tenant, group, and user assignments
|
||||||
|
- default, mandatory, and user-selectable Views
|
||||||
|
- active per-user View state
|
||||||
|
- effective projection resolution and provenance
|
||||||
|
- the View editor, preview, validation, and stale-surface diagnostics
|
||||||
|
|
||||||
|
Policy optionally owns inherited ceilings and explainable decisions. Workflow
|
||||||
|
optionally references a pinned View revision for an instance or step and may
|
||||||
|
narrow it further.
|
||||||
|
|
||||||
|
## Surface Contract
|
||||||
|
|
||||||
|
Modules announce only useful, semantic surfaces:
|
||||||
|
|
||||||
|
- module
|
||||||
|
- navigation item
|
||||||
|
- route or workspace
|
||||||
|
- section or panel
|
||||||
|
- command or action
|
||||||
|
|
||||||
|
Each descriptor has a stable namespaced id, parent id, kind, label, default
|
||||||
|
visibility, ordering, and dependency metadata where needed. Surface ids are
|
||||||
|
public module contracts, not CSS selectors, component paths, or arbitrary DOM
|
||||||
|
fragments.
|
||||||
|
|
||||||
|
The first release supports visible or hidden. Read-only states, layout
|
||||||
|
replacement, visual emphasis, and arbitrary styling are separate concerns and
|
||||||
|
are deferred.
|
||||||
|
|
||||||
|
## Effective Resolution
|
||||||
|
|
||||||
|
The effective interface is the intersection of:
|
||||||
|
|
||||||
|
1. installed and enabled modules
|
||||||
|
2. actor permissions and resource access
|
||||||
|
3. administrator and Policy ceilings
|
||||||
|
4. an assigned or user-selected View
|
||||||
|
5. an optional workflow instance or step overlay
|
||||||
|
|
||||||
|
Lower scopes and workflow overlays may narrow inherited visibility but cannot
|
||||||
|
broaden it. Every inherited, locked, hidden, unavailable, or stale choice
|
||||||
|
should carry provenance that the editor and runtime can explain.
|
||||||
|
|
||||||
|
Published View revisions are immutable. Active workflow instances pin the
|
||||||
|
revision they use. Unknown or retired surface ids produce diagnostics rather
|
||||||
|
than breaking startup. If no valid effective View can be resolved, the system
|
||||||
|
uses the last valid projection or the normal authorized interface and reports
|
||||||
|
the configuration problem to administrators.
|
||||||
|
|
||||||
|
## Workflow Behavior
|
||||||
|
|
||||||
|
A workflow definition may reference a View for the whole instance or a
|
||||||
|
particular step. Starting, resuming, or advancing the workflow activates the
|
||||||
|
appropriate projection. Users can intentionally leave focused mode and return
|
||||||
|
from an open-work widget or notification without losing workflow state.
|
||||||
|
|
||||||
|
Module handoffs carry the workflow and View context through Core contracts.
|
||||||
|
Workflow does not import the target module or the Views implementation.
|
||||||
|
|
||||||
|
## Delivery Order
|
||||||
|
|
||||||
|
1. Define the Core surface registry and runtime hooks.
|
||||||
|
2. Initialize `govoplan-views` and persist versioned definitions.
|
||||||
|
3. Add assignment, selection, resolution, provenance, and the editor.
|
||||||
|
4. Add Policy inheritance and administrator ceilings.
|
||||||
|
5. Add Workflow instance and step activation.
|
||||||
|
6. Adopt semantic section/action descriptors module by module.
|
||||||
|
|
||||||
|
## Implementation Status
|
||||||
|
|
||||||
|
Implemented in the initial Views slice:
|
||||||
|
|
||||||
|
- Core contract version `1`, stable module/navigation/route identifiers, custom
|
||||||
|
section/action descriptors, manifest validation, and platform API metadata
|
||||||
|
- shell navigation, route-boundary, settings, administration, dashboard-widget,
|
||||||
|
embedded-capability, and organization-action filtering
|
||||||
|
- `govoplan-views` definitions, immutable revisions, system/tenant/group/user
|
||||||
|
assignments, user selection, provenance, and stale-surface recovery
|
||||||
|
- a system and tenant administration editor with unsaved-change protection,
|
||||||
|
publish/archive controls, assignment management, and server-enforced lockout
|
||||||
|
prevention
|
||||||
|
- surface declarations for every currently installed module that contributes a
|
||||||
|
WebUI, including finer-grained shared administration and settings surfaces
|
||||||
|
|
||||||
|
Still intentionally separate:
|
||||||
|
|
||||||
|
- Policy-owned inherited ceilings and policy decision provenance
|
||||||
|
- workflow-instance and workflow-step activation of pinned View revisions
|
||||||
|
- read-only and layout-replacement projections beyond the version `1`
|
||||||
|
visible/hidden contract
|
||||||
|
|
||||||
|
## Gitea Work Packages
|
||||||
|
|
||||||
|
- `govoplan#17`: task-focused Views user story
|
||||||
|
- `govoplan#16`: initialize and implement `govoplan-views`
|
||||||
|
- `govoplan-core#271`: versioned surface and runtime contracts
|
||||||
|
- `govoplan-policy#9`: inheritance, ceilings, and provenance
|
||||||
|
- `govoplan-workflow#7`: workflow instance and step activation
|
||||||
|
- `govoplan-workflow#3`: focused workflow mode user story
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://govoplan.add-ideas.de/schemas/backup-evidence-keyring-v1.json",
|
||||||
|
"title": "GovOPlaN backup evidence trust keyring",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["schema_version", "purpose", "keys"],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "const": "1" },
|
||||||
|
"purpose": { "const": "govoplan-backup-evidence" },
|
||||||
|
"keys": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 64,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"key_id",
|
||||||
|
"algorithm",
|
||||||
|
"status",
|
||||||
|
"public_key_pem",
|
||||||
|
"not_before",
|
||||||
|
"expires_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"key_id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||||
|
"algorithm": { "const": "ed25519" },
|
||||||
|
"status": { "enum": ["active", "retired", "revoked"] },
|
||||||
|
"public_key_pem": { "type": "string", "minLength": 1, "maxLength": 8192 },
|
||||||
|
"not_before": { "type": "string", "format": "date-time" },
|
||||||
|
"expires_at": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://govoplan.add-ideas.de/schemas/backup-evidence-v1.json",
|
||||||
|
"title": "GovOPlaN coordinated backup and restore evidence",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"evidence_id",
|
||||||
|
"installation_id",
|
||||||
|
"deployment_subject",
|
||||||
|
"release",
|
||||||
|
"recovery_point",
|
||||||
|
"components",
|
||||||
|
"restore_drill",
|
||||||
|
"issued_at",
|
||||||
|
"expires_at",
|
||||||
|
"revoked",
|
||||||
|
"signatures"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "const": "1" },
|
||||||
|
"evidence_id": { "$ref": "#/$defs/token" },
|
||||||
|
"installation_id": { "$ref": "#/$defs/token" },
|
||||||
|
"deployment_subject": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["profile", "topology", "subject_ref"],
|
||||||
|
"properties": {
|
||||||
|
"profile": { "enum": ["evaluation", "self-hosted"] },
|
||||||
|
"topology": { "$ref": "#/$defs/token" },
|
||||||
|
"subject_ref": { "$ref": "#/$defs/reference" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"channel",
|
||||||
|
"version",
|
||||||
|
"manifest_sha256",
|
||||||
|
"composition_sha256",
|
||||||
|
"api_image",
|
||||||
|
"web_image"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"channel": { "$ref": "#/$defs/token" },
|
||||||
|
"version": { "$ref": "#/$defs/token" },
|
||||||
|
"manifest_sha256": { "$ref": "#/$defs/sha256" },
|
||||||
|
"composition_sha256": { "$ref": "#/$defs/sha256" },
|
||||||
|
"api_image": { "$ref": "#/$defs/digest_image" },
|
||||||
|
"web_image": { "$ref": "#/$defs/digest_image" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"recovery_point": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["id", "captured_at", "consistency", "rpo_seconds", "write_fence"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "$ref": "#/$defs/token" },
|
||||||
|
"captured_at": { "type": "string", "format": "date-time" },
|
||||||
|
"consistency": {
|
||||||
|
"enum": ["provider-atomic", "application-quiesced", "transaction-consistent"]
|
||||||
|
},
|
||||||
|
"rpo_seconds": { "$ref": "#/$defs/duration" },
|
||||||
|
"write_fence": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["mode", "token_sha256", "established_at"],
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"enum": ["provider-snapshot", "application-quiesce", "transaction-boundary"]
|
||||||
|
},
|
||||||
|
"token_sha256": { "$ref": "#/$defs/sha256" },
|
||||||
|
"established_at": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["database", "objects", "configuration", "key_custody"],
|
||||||
|
"properties": {
|
||||||
|
"database": { "$ref": "#/$defs/database" },
|
||||||
|
"objects": { "$ref": "#/$defs/objects" },
|
||||||
|
"configuration": { "$ref": "#/$defs/configuration" },
|
||||||
|
"key_custody": { "$ref": "#/$defs/key_custody" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"restore_drill": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"drill_id",
|
||||||
|
"recovery_point_id",
|
||||||
|
"started_at",
|
||||||
|
"completed_at",
|
||||||
|
"isolated_target_ref",
|
||||||
|
"release_manifest_sha256",
|
||||||
|
"migration_heads_sha256",
|
||||||
|
"representative_object_manifest_sha256",
|
||||||
|
"database_verified",
|
||||||
|
"objects_verified",
|
||||||
|
"configuration_verified",
|
||||||
|
"key_custody_verified",
|
||||||
|
"semantic_checks",
|
||||||
|
"measured_rpo_seconds",
|
||||||
|
"measured_rto_seconds",
|
||||||
|
"evidence_ref"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"drill_id": { "$ref": "#/$defs/token" },
|
||||||
|
"recovery_point_id": { "$ref": "#/$defs/token" },
|
||||||
|
"started_at": { "type": "string", "format": "date-time" },
|
||||||
|
"completed_at": { "type": "string", "format": "date-time" },
|
||||||
|
"isolated_target_ref": { "$ref": "#/$defs/reference" },
|
||||||
|
"release_manifest_sha256": { "$ref": "#/$defs/sha256" },
|
||||||
|
"migration_heads_sha256": { "$ref": "#/$defs/sha256" },
|
||||||
|
"representative_object_manifest_sha256": { "$ref": "#/$defs/sha256" },
|
||||||
|
"database_verified": { "const": true },
|
||||||
|
"objects_verified": { "const": true },
|
||||||
|
"configuration_verified": { "const": true },
|
||||||
|
"key_custody_verified": { "const": true },
|
||||||
|
"semantic_checks": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 128,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["id", "status", "evidence_ref"],
|
||||||
|
"properties": {
|
||||||
|
"id": { "$ref": "#/$defs/token" },
|
||||||
|
"status": { "const": "passed" },
|
||||||
|
"evidence_ref": { "$ref": "#/$defs/reference" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"measured_rpo_seconds": { "$ref": "#/$defs/duration" },
|
||||||
|
"measured_rto_seconds": { "$ref": "#/$defs/duration" },
|
||||||
|
"evidence_ref": { "$ref": "#/$defs/reference" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"issued_at": { "type": "string", "format": "date-time" },
|
||||||
|
"expires_at": { "type": "string", "format": "date-time" },
|
||||||
|
"revoked": { "const": false },
|
||||||
|
"signatures": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 16,
|
||||||
|
"items": { "$ref": "#/$defs/signature" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"token": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 128,
|
||||||
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
|
||||||
|
},
|
||||||
|
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
||||||
|
"digest_image": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 300,
|
||||||
|
"pattern": "^[^@\\s]+@sha256:[0-9a-f]{64}$"
|
||||||
|
},
|
||||||
|
"reference": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 2048,
|
||||||
|
"pattern": "^[A-Za-z][A-Za-z0-9+.-]*:[^\\s]+$"
|
||||||
|
},
|
||||||
|
"duration": { "type": "integer", "minimum": 0, "maximum": 2592000 },
|
||||||
|
"protected_key": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"protected": { "const": true },
|
||||||
|
"encryption_key_ref": { "$ref": "#/$defs/reference" },
|
||||||
|
"captured_at": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"provider", "artifact_ref", "artifact_sha256", "snapshot_id", "lsn",
|
||||||
|
"protected", "encryption_key_ref", "captured_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"provider": { "$ref": "#/$defs/token" },
|
||||||
|
"artifact_ref": { "$ref": "#/$defs/reference" },
|
||||||
|
"artifact_sha256": { "$ref": "#/$defs/sha256" },
|
||||||
|
"snapshot_id": { "$ref": "#/$defs/token" },
|
||||||
|
"lsn": { "type": "string", "minLength": 1, "maxLength": 256 },
|
||||||
|
"protected": { "const": true },
|
||||||
|
"encryption_key_ref": { "$ref": "#/$defs/reference" },
|
||||||
|
"captured_at": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"objects": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"provider", "artifact_ref", "manifest_sha256", "version_id",
|
||||||
|
"object_count", "total_bytes", "protected", "encryption_key_ref", "captured_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"provider": { "$ref": "#/$defs/token" },
|
||||||
|
"artifact_ref": { "$ref": "#/$defs/reference" },
|
||||||
|
"manifest_sha256": { "$ref": "#/$defs/sha256" },
|
||||||
|
"version_id": { "$ref": "#/$defs/token" },
|
||||||
|
"object_count": { "type": "integer", "minimum": 0 },
|
||||||
|
"total_bytes": { "type": "integer", "minimum": 0 },
|
||||||
|
"protected": { "const": true },
|
||||||
|
"encryption_key_ref": { "$ref": "#/$defs/reference" },
|
||||||
|
"captured_at": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["artifact_ref", "sha256", "protected", "encryption_key_ref", "captured_at"],
|
||||||
|
"properties": {
|
||||||
|
"artifact_ref": { "$ref": "#/$defs/reference" },
|
||||||
|
"sha256": { "$ref": "#/$defs/sha256" },
|
||||||
|
"protected": { "const": true },
|
||||||
|
"encryption_key_ref": { "$ref": "#/$defs/reference" },
|
||||||
|
"captured_at": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"key_custody": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["provider", "keyset_ref", "keyset_version", "recoverable", "captured_at"],
|
||||||
|
"properties": {
|
||||||
|
"provider": { "$ref": "#/$defs/token" },
|
||||||
|
"keyset_ref": { "$ref": "#/$defs/reference" },
|
||||||
|
"keyset_version": { "$ref": "#/$defs/token" },
|
||||||
|
"recoverable": { "const": true },
|
||||||
|
"captured_at": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"signature": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["key_id", "algorithm", "value"],
|
||||||
|
"properties": {
|
||||||
|
"key_id": { "$ref": "#/$defs/token" },
|
||||||
|
"algorithm": { "const": "ed25519" },
|
||||||
|
"value": { "type": "string", "minLength": 1, "maxLength": 256 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.add-ideas.de/add-ideas/govoplan/src/branch/main/docs/capability-fit-boundary-evidence.schema.json",
|
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/capability-fit-boundary-evidence.schema.json",
|
||||||
"title": "GovOPlaN externally issued capability-fit boundary evidence",
|
"title": "GovOPlaN externally issued capability-fit boundary evidence",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
"claims": {
|
"claims": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"minItems": 1,
|
"minItems": 1,
|
||||||
"maxItems": 3,
|
"maxItems": 8,
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/$defs/claim"
|
"$ref": "#/$defs/claim"
|
||||||
}
|
}
|
||||||
@@ -80,6 +80,11 @@
|
|||||||
"enum": [
|
"enum": [
|
||||||
"target_environment",
|
"target_environment",
|
||||||
"external_providers",
|
"external_providers",
|
||||||
|
"accessibility",
|
||||||
|
"privacy",
|
||||||
|
"security",
|
||||||
|
"operations",
|
||||||
|
"recovery",
|
||||||
"production_approval"
|
"production_approval"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/capability-fit-boundary-run.schema.json",
|
||||||
|
"title": "GovOPlaN private target-run claim manifest",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"evidence_kind",
|
||||||
|
"proof_id",
|
||||||
|
"expires_at",
|
||||||
|
"claims"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"$schema": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri-reference"
|
||||||
|
},
|
||||||
|
"schema_version": {
|
||||||
|
"const": "0.1.0"
|
||||||
|
},
|
||||||
|
"evidence_kind": {
|
||||||
|
"const": "govoplan.capability-fit-boundary-run"
|
||||||
|
},
|
||||||
|
"proof_id": {
|
||||||
|
"$ref": "#/$defs/opaque_id"
|
||||||
|
},
|
||||||
|
"expires_at": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
|
"claims": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 8,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/claim"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"opaque_id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 160,
|
||||||
|
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$"
|
||||||
|
},
|
||||||
|
"claim": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["scope", "result", "control_ids", "artifacts"],
|
||||||
|
"properties": {
|
||||||
|
"scope": {
|
||||||
|
"enum": [
|
||||||
|
"target_environment",
|
||||||
|
"external_providers",
|
||||||
|
"accessibility",
|
||||||
|
"privacy",
|
||||||
|
"security",
|
||||||
|
"operations",
|
||||||
|
"recovery",
|
||||||
|
"production_approval"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"result": {
|
||||||
|
"enum": ["passed", "failed", "approved", "rejected"]
|
||||||
|
},
|
||||||
|
"control_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 256,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/opaque_id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"artifacts": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 256,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/artifact"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"artifact": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["artifact_id", "path"],
|
||||||
|
"properties": {
|
||||||
|
"artifact_id": {
|
||||||
|
"$ref": "#/$defs/opaque_id"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 4096
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -507,7 +507,7 @@
|
|||||||
{
|
{
|
||||||
"kind": "issue",
|
"kind": "issue",
|
||||||
"scope": "documented_model",
|
"scope": "documented_model",
|
||||||
"locator": "https://git.add-ideas.de/add-ideas/govoplan/issues/12"
|
"locator": "https://git.add-ideas.de/GovOPlaN/govoplan/issues/12"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"conditions": [],
|
"conditions": [],
|
||||||
@@ -793,7 +793,7 @@
|
|||||||
{
|
{
|
||||||
"kind": "issue",
|
"kind": "issue",
|
||||||
"scope": "documented_model",
|
"scope": "documented_model",
|
||||||
"locator": "https://git.add-ideas.de/add-ideas/govoplan-core/issues/29"
|
"locator": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/29"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"conditions": [],
|
"conditions": [],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.add-ideas.de/add-ideas/govoplan/src/branch/main/docs/capability-fit-proof-authority-keyring.schema.json",
|
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/capability-fit-proof-authority-keyring.schema.json",
|
||||||
"title": "GovOPlaN capability-fit proof authority keyring",
|
"title": "GovOPlaN capability-fit proof authority keyring",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
@@ -51,12 +51,17 @@
|
|||||||
"allowed_scopes": {
|
"allowed_scopes": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"minItems": 1,
|
"minItems": 1,
|
||||||
"maxItems": 3,
|
"maxItems": 8,
|
||||||
"uniqueItems": true,
|
"uniqueItems": true,
|
||||||
"items": {
|
"items": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"target_environment",
|
"target_environment",
|
||||||
"external_providers",
|
"external_providers",
|
||||||
|
"accessibility",
|
||||||
|
"privacy",
|
||||||
|
"security",
|
||||||
|
"operations",
|
||||||
|
"recovery",
|
||||||
"production_approval"
|
"production_approval"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.add-ideas.de/add-ideas/govoplan/src/branch/main/docs/capability-fit.schema.json",
|
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/capability-fit.schema.json",
|
||||||
"title": "GovOPlaN capability and infrastructure fit assessment",
|
"title": "GovOPlaN capability and infrastructure fit assessment",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@@ -113,6 +113,12 @@
|
|||||||
"description": "GovOPlaN Appointments module behavior or integration.",
|
"description": "GovOPlaN Appointments module behavior or integration.",
|
||||||
"exclusive": false
|
"exclusive": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "module/approvals",
|
||||||
|
"color": "d93f0b",
|
||||||
|
"description": "GovOPlaN Approvals module behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "module/audit",
|
"name": "module/audit",
|
||||||
"color": "0e8a16",
|
"color": "0e8a16",
|
||||||
@@ -143,30 +149,72 @@
|
|||||||
"description": "GovOPlaN Connectors module behavior or integration.",
|
"description": "GovOPlaN Connectors module behavior or integration.",
|
||||||
"exclusive": false
|
"exclusive": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "module/committee",
|
||||||
|
"color": "5319e7",
|
||||||
|
"description": "GovOPlaN Committee module behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "module/core",
|
"name": "module/core",
|
||||||
"color": "0052cc",
|
"color": "0052cc",
|
||||||
"description": "GovOPlaN core runner, shared primitives, shell, or extension points.",
|
"description": "GovOPlaN core runner, shared primitives, shell, or extension points.",
|
||||||
"exclusive": false
|
"exclusive": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "module/dashboard",
|
||||||
|
"color": "1d76db",
|
||||||
|
"description": "GovOPlaN Dashboard module behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "module/dataflow",
|
||||||
|
"color": "1d76db",
|
||||||
|
"description": "GovOPlaN Dataflow module behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "module/datasources",
|
||||||
|
"color": "006b75",
|
||||||
|
"description": "GovOPlaN governed datasource contracts, catalogs, and integrations.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "module/dms",
|
"name": "module/dms",
|
||||||
"color": "c5def5",
|
"color": "c5def5",
|
||||||
"description": "GovOPlaN Dms module behavior or integration.",
|
"description": "GovOPlaN Dms module behavior or integration.",
|
||||||
"exclusive": false
|
"exclusive": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "module/docs",
|
||||||
|
"color": "c5def5",
|
||||||
|
"description": "GovOPlaN Docs module behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "module/dist-lists",
|
"name": "module/dist-lists",
|
||||||
"color": "0e8a16",
|
"color": "0e8a16",
|
||||||
"description": "GovOPlaN Distribution Lists module behavior or integration.",
|
"description": "GovOPlaN Distribution Lists module behavior or integration.",
|
||||||
"exclusive": false
|
"exclusive": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "module/decisions",
|
||||||
|
"color": "d93f0b",
|
||||||
|
"description": "GovOPlaN formal Decisions module behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "module/erp",
|
"name": "module/erp",
|
||||||
"color": "fef2c0",
|
"color": "fef2c0",
|
||||||
"description": "GovOPlaN Erp module behavior or integration.",
|
"description": "GovOPlaN Erp module behavior or integration.",
|
||||||
"exclusive": false
|
"exclusive": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "module/encryption",
|
||||||
|
"color": "b60205",
|
||||||
|
"description": "GovOPlaN Encryption key custody, cryptographic policy, and E2EE integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "module/evaluation",
|
"name": "module/evaluation",
|
||||||
"color": "bfdadc",
|
"color": "bfdadc",
|
||||||
@@ -191,6 +239,18 @@
|
|||||||
"description": "GovOPlaN Forms module behavior or integration.",
|
"description": "GovOPlaN Forms module behavior or integration.",
|
||||||
"exclusive": false
|
"exclusive": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "module/forms-runtime",
|
||||||
|
"color": "f9d0c4",
|
||||||
|
"description": "GovOPlaN Forms Runtime module behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "module/helpdesk",
|
||||||
|
"color": "c2e0c6",
|
||||||
|
"description": "GovOPlaN Helpdesk module behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "module/identity-trust",
|
"name": "module/identity-trust",
|
||||||
"color": "d4c5f9",
|
"color": "d4c5f9",
|
||||||
@@ -221,6 +281,12 @@
|
|||||||
"description": "GovOPlaN mail module behavior or integration.",
|
"description": "GovOPlaN mail module behavior or integration.",
|
||||||
"exclusive": false
|
"exclusive": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "module/mandates",
|
||||||
|
"color": "006b75",
|
||||||
|
"description": "GovOPlaN Mandates, jurisdiction, responsibility, and authority behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "module/notifications",
|
"name": "module/notifications",
|
||||||
"color": "d876e3",
|
"color": "d876e3",
|
||||||
@@ -245,6 +311,12 @@
|
|||||||
"description": "GovOPlaN Payments module behavior or integration.",
|
"description": "GovOPlaN Payments module behavior or integration.",
|
||||||
"exclusive": false
|
"exclusive": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "module/parties",
|
||||||
|
"color": "bfd4f2",
|
||||||
|
"description": "GovOPlaN procedure Parties, representation, and delivery-authority behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "module/permits",
|
"name": "module/permits",
|
||||||
"color": "fbca04",
|
"color": "fbca04",
|
||||||
@@ -275,12 +347,24 @@
|
|||||||
"description": "GovOPlaN Postbox module behavior or integration.",
|
"description": "GovOPlaN Postbox module behavior or integration.",
|
||||||
"exclusive": false
|
"exclusive": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "module/projects",
|
||||||
|
"color": "5319e7",
|
||||||
|
"description": "GovOPlaN Projects module behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "module/reporting",
|
"name": "module/reporting",
|
||||||
"color": "c2e0c6",
|
"color": "c2e0c6",
|
||||||
"description": "GovOPlaN Reporting module behavior or integration.",
|
"description": "GovOPlaN Reporting module behavior or integration.",
|
||||||
"exclusive": false
|
"exclusive": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "module/risk-compliance",
|
||||||
|
"color": "b60205",
|
||||||
|
"description": "GovOPlaN Risk Compliance module behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "module/search",
|
"name": "module/search",
|
||||||
"color": "bfdadc",
|
"color": "bfdadc",
|
||||||
@@ -293,6 +377,12 @@
|
|||||||
"description": "GovOPlaN Scheduling module behavior or integration.",
|
"description": "GovOPlaN Scheduling module behavior or integration.",
|
||||||
"exclusive": false
|
"exclusive": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "module/services",
|
||||||
|
"color": "1d76db",
|
||||||
|
"description": "GovOPlaN versioned institutional Services behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "module/tasks",
|
"name": "module/tasks",
|
||||||
"color": "c5def5",
|
"color": "c5def5",
|
||||||
@@ -311,6 +401,24 @@
|
|||||||
"description": "GovOPlaN Tenancy module behavior or integration.",
|
"description": "GovOPlaN Tenancy module behavior or integration.",
|
||||||
"exclusive": false
|
"exclusive": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "module/tickets",
|
||||||
|
"color": "0e8a16",
|
||||||
|
"description": "GovOPlaN Tickets module behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "module/views",
|
||||||
|
"color": "c5def5",
|
||||||
|
"description": "GovOPlaN governed task views, interface projections, and workflow view integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "module/wiki",
|
||||||
|
"color": "006b75",
|
||||||
|
"description": "GovOPlaN Wiki module behavior or integration.",
|
||||||
|
"exclusive": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "module/workflow",
|
"name": "module/workflow",
|
||||||
"color": "f9d0c4",
|
"color": "f9d0c4",
|
||||||
|
|||||||
@@ -0,0 +1,381 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://govoplan.add-ideas.de/schemas/installation-spec-v1.json",
|
||||||
|
"title": "GovOPlaN installation specification",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"installation_id",
|
||||||
|
"profile",
|
||||||
|
"public_url",
|
||||||
|
"listen",
|
||||||
|
"network_subnet",
|
||||||
|
"release",
|
||||||
|
"components",
|
||||||
|
"enabled_modules"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": {
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"installation_id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z][a-z0-9-]{1,47}$"
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"enum": [
|
||||||
|
"evaluation",
|
||||||
|
"self-hosted"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"public_url": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri",
|
||||||
|
"pattern": "^https?://"
|
||||||
|
},
|
||||||
|
"listen": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"address",
|
||||||
|
"port"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"address": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65535
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"network_subnet": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"channel",
|
||||||
|
"version",
|
||||||
|
"manifest_url",
|
||||||
|
"manifest_sha256",
|
||||||
|
"api_image",
|
||||||
|
"web_image"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"channel": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z][a-z0-9-]{1,31}$"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 80
|
||||||
|
},
|
||||||
|
"manifest_url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"manifest_sha256": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^$|^[0-9a-f]{64}$"
|
||||||
|
},
|
||||||
|
"manifest_keyring_sha256": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^$|^[0-9a-f]{64}$"
|
||||||
|
},
|
||||||
|
"manifest_signature_key_id": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^$|^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
|
||||||
|
},
|
||||||
|
"composition_sha256": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^$|^[0-9a-f]{64}$"
|
||||||
|
},
|
||||||
|
"api_image": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 300
|
||||||
|
},
|
||||||
|
"web_image": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"postgres",
|
||||||
|
"redis",
|
||||||
|
"mail",
|
||||||
|
"storage"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"postgres": {
|
||||||
|
"$ref": "#/$defs/postgres"
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"$ref": "#/$defs/redis"
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"$ref": "#/$defs/mail"
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"mode"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"enum": [
|
||||||
|
"local",
|
||||||
|
"garage",
|
||||||
|
"s3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"load_balancer": {
|
||||||
|
"$ref": "#/$defs/load_balancer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"replicas": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"api",
|
||||||
|
"web",
|
||||||
|
"worker"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"api": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 64
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 64
|
||||||
|
},
|
||||||
|
"worker": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 128
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ingress": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"mode",
|
||||||
|
"image",
|
||||||
|
"trusted_proxy_cidrs",
|
||||||
|
"http_port",
|
||||||
|
"https_port",
|
||||||
|
"acme_email"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"enum": [
|
||||||
|
"local",
|
||||||
|
"existing-proxy",
|
||||||
|
"managed",
|
||||||
|
"unconfigured"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 300
|
||||||
|
},
|
||||||
|
"trusted_proxy_cidrs": {
|
||||||
|
"type": "array",
|
||||||
|
"maxItems": 16,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 64
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"http_port": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65535
|
||||||
|
},
|
||||||
|
"https_port": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65535
|
||||||
|
},
|
||||||
|
"acme_email": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 254
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enabled_modules": {
|
||||||
|
"type": "array",
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z][a-z0-9_]{1,63}$"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"service": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"mode",
|
||||||
|
"image",
|
||||||
|
"url_env"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"url_env": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"postgres": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"enum": [
|
||||||
|
"managed",
|
||||||
|
"external"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"url_env": {
|
||||||
|
"const": "DATABASE_URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"enum": [
|
||||||
|
"managed",
|
||||||
|
"external",
|
||||||
|
"disabled"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"url_env": {
|
||||||
|
"const": "REDIS_URL"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"enum": [
|
||||||
|
"disabled",
|
||||||
|
"external-relay",
|
||||||
|
"test-mail"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"url_env": {
|
||||||
|
"const": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"load_balancer": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/service"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"const": "managed"
|
||||||
|
},
|
||||||
|
"url_env": {
|
||||||
|
"const": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"profile": {
|
||||||
|
"const": "self-hosted"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"public_url": {
|
||||||
|
"pattern": "^https://"
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"properties": {
|
||||||
|
"redis": {
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"enum": [
|
||||||
|
"managed",
|
||||||
|
"external"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mail": {
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"enum": [
|
||||||
|
"disabled",
|
||||||
|
"external-relay"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.add-ideas.de/add-ideas/govoplan/src/branch/main/docs/installed-composition-evidence.schema.json",
|
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/installed-composition-evidence.schema.json",
|
||||||
"title": "GovOPlaN installed composition evidence",
|
"title": "GovOPlaN installed composition evidence",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.add-ideas.de/add-ideas/govoplan/src/branch/main/docs/installer-receipt-authority-keyring.schema.json",
|
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/installer-receipt-authority-keyring.schema.json",
|
||||||
"title": "GovOPlaN installer receipt authority keyring",
|
"title": "GovOPlaN installer receipt authority keyring",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://git.add-ideas.de/add-ideas/govoplan/src/branch/main/docs/installer-receipt.schema.json",
|
"$id": "https://git.add-ideas.de/GovOPlaN/govoplan/src/branch/main/docs/installer-receipt.schema.json",
|
||||||
"title": "GovOPlaN signed installer receipt",
|
"title": "GovOPlaN signed installer receipt",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"parent_issue": "https://git.add-ideas.de/GovOPlaN/govoplan/issues/36",
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"id": "campaign.build.publish-artifacts",
|
||||||
|
"repository": "govoplan-campaign",
|
||||||
|
"resources": ["postgresql", "object-storage", "templates-capability", "files-capability"],
|
||||||
|
"mode": "compensation",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "reference-implementation",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/92"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "campaign.delivery.external-channels",
|
||||||
|
"repository": "govoplan-campaign",
|
||||||
|
"resources": ["postgresql", "queue", "smtp", "imap", "postbox", "print-provider"],
|
||||||
|
"mode": "forward_recovery",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/92"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "campaign.retention.generated-artifacts",
|
||||||
|
"repository": "govoplan-campaign",
|
||||||
|
"resources": ["postgresql", "object-storage"],
|
||||||
|
"mode": "forward_recovery",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/92"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "files.upload.finalize",
|
||||||
|
"repository": "govoplan-files",
|
||||||
|
"resources": ["postgresql", "object-storage", "filesystem-staging"],
|
||||||
|
"mode": "compensation",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/41"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "files.retention.purge",
|
||||||
|
"repository": "govoplan-files",
|
||||||
|
"resources": ["postgresql", "object-storage", "encryption-key-custody"],
|
||||||
|
"mode": "irreversible",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "planned",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/41"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "files.integrity.reconcile",
|
||||||
|
"repository": "govoplan-files",
|
||||||
|
"resources": ["postgresql", "object-storage"],
|
||||||
|
"mode": "forward_recovery",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/41"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "files.connector.write-sync",
|
||||||
|
"repository": "govoplan-files",
|
||||||
|
"resources": ["postgresql", "object-storage", "external-connector"],
|
||||||
|
"mode": "forward_recovery",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "planned",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-files/issues/41"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mail.outbox.smtp-submit",
|
||||||
|
"repository": "govoplan-mail",
|
||||||
|
"resources": ["postgresql", "queue", "smtp"],
|
||||||
|
"mode": "forward_recovery",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mail.sent.imap-append",
|
||||||
|
"repository": "govoplan-mail",
|
||||||
|
"resources": ["postgresql", "imap"],
|
||||||
|
"mode": "forward_recovery",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mail.mailbox.imap-mutate",
|
||||||
|
"repository": "govoplan-mail",
|
||||||
|
"resources": ["postgresql", "imap"],
|
||||||
|
"mode": "forward_recovery",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "planned",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mail.mailbox.sync-cursor",
|
||||||
|
"repository": "govoplan-mail",
|
||||||
|
"resources": ["postgresql", "imap"],
|
||||||
|
"mode": "atomic",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-mail/issues/19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "connectors.sync.read-snapshot",
|
||||||
|
"repository": "govoplan-connectors",
|
||||||
|
"resources": ["postgresql", "external-provider"],
|
||||||
|
"mode": "atomic",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "connectors.sync.external-mutation",
|
||||||
|
"repository": "govoplan-connectors",
|
||||||
|
"resources": ["postgresql", "queue", "external-provider"],
|
||||||
|
"mode": "forward_recovery",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "planned",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-connectors/issues/15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dataflow.run.database-only",
|
||||||
|
"repository": "govoplan-dataflow",
|
||||||
|
"resources": ["postgresql"],
|
||||||
|
"mode": "atomic",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "dataflow.run.publish-output",
|
||||||
|
"repository": "govoplan-dataflow",
|
||||||
|
"resources": ["postgresql", "queue", "object-storage", "external-sink"],
|
||||||
|
"mode": "forward_recovery",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-dataflow/issues/19"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "workflow-engine.instance.state-transition",
|
||||||
|
"repository": "govoplan-workflow-engine",
|
||||||
|
"resources": ["postgresql"],
|
||||||
|
"mode": "atomic",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine/issues/1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "workflow-engine.activity.external-effect",
|
||||||
|
"repository": "govoplan-workflow-engine",
|
||||||
|
"resources": ["postgresql", "queue", "module-capability", "external-provider"],
|
||||||
|
"mode": "forward_recovery",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-workflow-engine/issues/1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core.module-lifecycle.pre-migration",
|
||||||
|
"repository": "govoplan-core",
|
||||||
|
"resources": ["postgresql", "package-environment", "webui-bundle", "filesystem"],
|
||||||
|
"mode": "compensation",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/281"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core.module-lifecycle.post-migration",
|
||||||
|
"repository": "govoplan-core",
|
||||||
|
"resources": ["postgresql", "package-environment", "webui-bundle", "runtime-nodes"],
|
||||||
|
"mode": "forward_recovery",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/281"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core.module-retirement.destroy-data",
|
||||||
|
"repository": "govoplan-core",
|
||||||
|
"resources": ["postgresql", "object-storage", "package-environment"],
|
||||||
|
"mode": "snapshot_restore",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/281"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "core.module-runtime.apply-graph",
|
||||||
|
"repository": "govoplan-core",
|
||||||
|
"resources": ["postgresql", "runtime-nodes", "module-registry"],
|
||||||
|
"mode": "compensation",
|
||||||
|
"fenced": true,
|
||||||
|
"adoption": "adopted",
|
||||||
|
"issue": "https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/281"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://govoplan.add-ideas.de/schemas/runtime-distribution-keyring-v1.json",
|
||||||
|
"title": "GovOPlaN runtime distribution trust keyring",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["schema_version", "purpose", "keys"],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "const": "1" },
|
||||||
|
"purpose": { "const": "govoplan-runtime-distribution" },
|
||||||
|
"keys": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"key_id",
|
||||||
|
"algorithm",
|
||||||
|
"status",
|
||||||
|
"public_key_pem",
|
||||||
|
"not_before",
|
||||||
|
"expires_at"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"key_id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||||
|
"algorithm": { "const": "ed25519" },
|
||||||
|
"status": { "enum": ["active", "retired", "revoked"] },
|
||||||
|
"public_key_pem": { "type": "string", "minLength": 1, "maxLength": 8192 },
|
||||||
|
"not_before": { "type": "string", "format": "date-time" },
|
||||||
|
"expires_at": { "type": "string", "format": "date-time" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://govoplan.add-ideas.de/schemas/runtime-distribution-manifest-v1.json",
|
||||||
|
"title": "GovOPlaN runtime distribution manifest",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"schema_version",
|
||||||
|
"channel",
|
||||||
|
"sequence",
|
||||||
|
"version",
|
||||||
|
"issued_at",
|
||||||
|
"expires_at",
|
||||||
|
"revoked",
|
||||||
|
"deployer",
|
||||||
|
"package_lock",
|
||||||
|
"images",
|
||||||
|
"dependencies",
|
||||||
|
"composition",
|
||||||
|
"signatures"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"schema_version": { "const": "1" },
|
||||||
|
"channel": { "type": "string", "pattern": "^[a-z][a-z0-9_]{1,63}$" },
|
||||||
|
"sequence": { "type": "integer", "minimum": 1 },
|
||||||
|
"version": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||||
|
"issued_at": { "type": "string", "format": "date-time" },
|
||||||
|
"expires_at": { "type": "string", "format": "date-time" },
|
||||||
|
"revoked": { "const": false },
|
||||||
|
"deployer": { "$ref": "#/$defs/artifact" },
|
||||||
|
"package_lock": { "$ref": "#/$defs/artifact" },
|
||||||
|
"images": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["api", "web"],
|
||||||
|
"properties": {
|
||||||
|
"api": { "$ref": "#/$defs/image" },
|
||||||
|
"web": { "$ref": "#/$defs/image" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"type": "object",
|
||||||
|
"minProperties": 1,
|
||||||
|
"propertyNames": { "pattern": "^[a-z][a-z0-9_]{1,63}$" },
|
||||||
|
"additionalProperties": { "$ref": "#/$defs/imageReference" }
|
||||||
|
},
|
||||||
|
"composition": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["sha256", "module_ids", "packages"],
|
||||||
|
"properties": {
|
||||||
|
"sha256": { "$ref": "#/$defs/sha256" },
|
||||||
|
"module_ids": {
|
||||||
|
"type": "array",
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": { "type": "string", "pattern": "^[a-z][a-z0-9_]{1,63}$" }
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["name", "version", "wheel_sha256"],
|
||||||
|
"properties": {
|
||||||
|
"name": { "type": "string", "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" },
|
||||||
|
"version": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||||
|
"wheel_sha256": { "$ref": "#/$defs/sha256" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"signatures": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["key_id", "algorithm", "value"],
|
||||||
|
"properties": {
|
||||||
|
"key_id": { "type": "string", "minLength": 1, "maxLength": 128 },
|
||||||
|
"algorithm": { "const": "ed25519" },
|
||||||
|
"value": { "type": "string", "minLength": 1, "maxLength": 256 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
||||||
|
"imageReference": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[^@\\s]+@sha256:[0-9a-f]{64}$",
|
||||||
|
"maxLength": 300
|
||||||
|
},
|
||||||
|
"artifact": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["url", "sha256"],
|
||||||
|
"properties": {
|
||||||
|
"url": { "type": "string", "format": "uri", "pattern": "^https://" },
|
||||||
|
"sha256": { "$ref": "#/$defs/sha256" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"image": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["index", "platforms", "sbom", "provenance"],
|
||||||
|
"properties": {
|
||||||
|
"index": { "$ref": "#/$defs/imageReference" },
|
||||||
|
"platforms": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["linux/amd64", "linux/arm64"],
|
||||||
|
"properties": {
|
||||||
|
"linux/amd64": { "$ref": "#/$defs/imageReference" },
|
||||||
|
"linux/arm64": { "$ref": "#/$defs/imageReference" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sbom": { "$ref": "#/$defs/artifact" },
|
||||||
|
"provenance": { "$ref": "#/$defs/artifact" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# GovOPlaN developer meta-package
|
||||||
|
|
||||||
|
`govoplan` is an optional convenience package for local development and
|
||||||
|
composition tests. The default dependency set matches the reviewed runtime
|
||||||
|
release roots; `govoplan[full]` adds every packageable module present in the
|
||||||
|
workspace at generation time.
|
||||||
|
|
||||||
|
This package is not a production deployment artifact. Production installations
|
||||||
|
consume the signed runtime distribution manifest and digest-pinned OCI images.
|
||||||
|
The package does not enable modules, apply migrations, choose infrastructure,
|
||||||
|
or replace installation and recovery evidence.
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=69", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "govoplan"
|
||||||
|
version = "0.1.15"
|
||||||
|
description = "Developer convenience package for a versioned GovOPlaN composition"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
license = { text = "AGPL-3.0-or-later" }
|
||||||
|
dependencies = [
|
||||||
|
"govoplan-core[server]==0.1.15",
|
||||||
|
"govoplan-tenancy==0.1.15",
|
||||||
|
"govoplan-organizations==0.1.15",
|
||||||
|
"govoplan-identity==0.1.15",
|
||||||
|
"govoplan-idm==0.1.15",
|
||||||
|
"govoplan-access==0.1.15",
|
||||||
|
"govoplan-admin==0.1.15",
|
||||||
|
"govoplan-policy==0.1.15",
|
||||||
|
"govoplan-audit==0.1.15",
|
||||||
|
"govoplan-dashboard==0.1.15",
|
||||||
|
"govoplan-files==0.1.15",
|
||||||
|
"govoplan-mail==0.1.15",
|
||||||
|
"govoplan-campaign==0.1.15",
|
||||||
|
"govoplan-calendar==0.1.15",
|
||||||
|
"govoplan-docs==0.1.15",
|
||||||
|
"govoplan-ops==0.1.15",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
full = [
|
||||||
|
"govoplan-addresses==0.1.15",
|
||||||
|
"govoplan-approvals==0.1.15",
|
||||||
|
"govoplan-assets==0.1.15",
|
||||||
|
"govoplan-booking==0.1.15",
|
||||||
|
"govoplan-cases==0.1.15",
|
||||||
|
"govoplan-certificates==0.1.15",
|
||||||
|
"govoplan-committee==0.1.15",
|
||||||
|
"govoplan-connectors==0.1.15",
|
||||||
|
"govoplan-consultation==0.1.15",
|
||||||
|
"govoplan-contracts==0.1.15",
|
||||||
|
"govoplan-dataflow==0.1.15",
|
||||||
|
"govoplan-datasources==0.1.15",
|
||||||
|
"govoplan-decisions==0.1.15",
|
||||||
|
"govoplan-dist-lists==0.1.15",
|
||||||
|
"govoplan-encryption==0.1.15",
|
||||||
|
"govoplan-evaluation==0.1.15",
|
||||||
|
"govoplan-facilities==0.1.15",
|
||||||
|
"govoplan-forms==0.1.15",
|
||||||
|
"govoplan-forms-runtime==0.1.15",
|
||||||
|
"govoplan-grants==0.1.15",
|
||||||
|
"govoplan-helpdesk==0.1.15",
|
||||||
|
"govoplan-identity-trust==0.1.15",
|
||||||
|
"govoplan-inspections==0.1.15",
|
||||||
|
"govoplan-learning==0.1.15",
|
||||||
|
"govoplan-mandates==0.1.15",
|
||||||
|
"govoplan-notifications==0.1.15",
|
||||||
|
"govoplan-parties==0.1.15",
|
||||||
|
"govoplan-permits==0.1.15",
|
||||||
|
"govoplan-poll==0.1.15",
|
||||||
|
"govoplan-portal==0.1.15",
|
||||||
|
"govoplan-postbox==0.1.15",
|
||||||
|
"govoplan-procurement==0.1.15",
|
||||||
|
"govoplan-projects==0.1.15",
|
||||||
|
"govoplan-records==0.1.15",
|
||||||
|
"govoplan-reporting==0.1.15",
|
||||||
|
"govoplan-resources==0.1.15",
|
||||||
|
"govoplan-rest==0.1.15",
|
||||||
|
"govoplan-risk-compliance==0.1.15",
|
||||||
|
"govoplan-scheduling==0.1.15",
|
||||||
|
"govoplan-search==0.1.15",
|
||||||
|
"govoplan-services==0.1.15",
|
||||||
|
"govoplan-soap==0.1.15",
|
||||||
|
"govoplan-templates==0.1.15",
|
||||||
|
"govoplan-tickets==0.1.15",
|
||||||
|
"govoplan-transparency==0.1.15",
|
||||||
|
"govoplan-views==0.1.15",
|
||||||
|
"govoplan-voting==0.1.15",
|
||||||
|
"govoplan-wiki==0.1.15",
|
||||||
|
"govoplan-workflow==0.1.15",
|
||||||
|
"govoplan-workflow-engine==0.1.15",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Repository = "https://git.add-ideas.de/GovOPlaN/govoplan"
|
||||||
|
Documentation = "https://govoplan.add-ideas.de"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
"""Metadata helpers for the optional GovOPlaN developer composition."""
|
||||||
|
|
||||||
|
from importlib.metadata import PackageNotFoundError, version
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
__version__ = version("govoplan")
|
||||||
|
except PackageNotFoundError: # pragma: no cover - source checkout only
|
||||||
|
__version__ = "0+unknown"
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["__version__"]
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Governed Communication Product Package
|
||||||
|
|
||||||
|
This package composes Campaign, Mail, Postbox, Files, Templates, Policy, Audit,
|
||||||
|
and Access into one governed-delivery capability. Modules retain their own
|
||||||
|
tables and lifecycle. The package declares installation and configuration
|
||||||
|
expectations; it does not import module internals.
|
||||||
|
|
||||||
|
## Reference journey
|
||||||
|
|
||||||
|
1. Select governed recipient and attachment data.
|
||||||
|
2. Build and review an immutable Campaign version.
|
||||||
|
3. Apply attachment, access, and delivery policy.
|
||||||
|
4. Deliver through Mail and/or function-bound Postbox targets.
|
||||||
|
5. Reconcile outcomes, acknowledgements, correction, and reports against Audit
|
||||||
|
evidence.
|
||||||
|
|
||||||
|
## Reference-readiness gates
|
||||||
|
|
||||||
|
The artifact remains a `product` package. Promotion to `reference` requires:
|
||||||
|
|
||||||
|
- target-environment delivery and reconciliation tests, including an
|
||||||
|
outcome-unknown transport result;
|
||||||
|
- backup/restore and interrupted-dispatch recovery evidence;
|
||||||
|
- security evidence for recipient, attachment, and postbox isolation;
|
||||||
|
- operator runbooks for queue health, retry, reconciliation, and retirement;
|
||||||
|
- keyboard, focus, screen-reader, and responsive workflow evidence;
|
||||||
|
- privacy evidence for minimization, purpose, retention, redaction, and report
|
||||||
|
access; and
|
||||||
|
- version-pinned user and administrator documentation.
|
||||||
|
|
||||||
|
Optional Notifications, Portal, Reporting, and Workflow Engine integrations do
|
||||||
|
not change the package boundary when absent.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"package_id": "product.governed-communication",
|
||||||
|
"name": "Governed Communication",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"package_class": "product",
|
||||||
|
"description": "Compose evidence-backed campaigns, mail and function-bound postboxes without merging their domain ownership.",
|
||||||
|
"publisher": "GovOPlaN",
|
||||||
|
"category": "communication",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"required_modules": [
|
||||||
|
{"module_id": "access"},
|
||||||
|
{"module_id": "audit"},
|
||||||
|
{"module_id": "campaign"},
|
||||||
|
{"module_id": "files"},
|
||||||
|
{"module_id": "mail"},
|
||||||
|
{"module_id": "policy"},
|
||||||
|
{"module_id": "postbox"},
|
||||||
|
{"module_id": "templates"}
|
||||||
|
],
|
||||||
|
"optional_modules": [
|
||||||
|
{"module_id": "notifications"},
|
||||||
|
{"module_id": "portal"},
|
||||||
|
{"module_id": "reporting"},
|
||||||
|
{"module_id": "workflow_engine"}
|
||||||
|
],
|
||||||
|
"evidence": [
|
||||||
|
{
|
||||||
|
"kind": "documentation",
|
||||||
|
"reference": "packages/product/governed-communication/README.md",
|
||||||
|
"summary": "Defines the package boundary, journey, and reference-readiness gates."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": ["campaign", "mail", "postbox", "public-sector"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Governed Data and Assurance Product Package
|
||||||
|
|
||||||
|
This package composes Datasources, Dataflow, Reporting, Search, Risk
|
||||||
|
Compliance, Policy, Audit, and Access. Connectors may acquire external data,
|
||||||
|
but Datasources owns the governed catalogue and immutable snapshots; Dataflow
|
||||||
|
owns transformation definitions and runs; Reporting owns measures and
|
||||||
|
presentation; Risk Compliance owns controls, findings, and effectiveness
|
||||||
|
review.
|
||||||
|
|
||||||
|
## Reference journey
|
||||||
|
|
||||||
|
1. Register a typed datasource with source authority, purpose, classification,
|
||||||
|
owner, freshness, and correction policy.
|
||||||
|
2. Acquire or upload an immutable source state.
|
||||||
|
3. execute a versioned flow and retain intermediate materializations and
|
||||||
|
provenance;
|
||||||
|
4. publish a report or decision input against exact source and flow revisions;
|
||||||
|
5. link obligation, governed object, risk, control, evidence, finding,
|
||||||
|
corrective measure, and effectiveness review; and
|
||||||
|
6. search current authorized objects while preserving ownership and access
|
||||||
|
rechecks.
|
||||||
|
|
||||||
|
## Reference-readiness gates
|
||||||
|
|
||||||
|
The artifact remains a `product` package. Promotion to `reference` requires:
|
||||||
|
|
||||||
|
- a target-tested monthly-data and sanctions-screening fixture with expected
|
||||||
|
outputs and complete provenance;
|
||||||
|
- database/object-storage backup and restore plus interrupted-run recovery;
|
||||||
|
- security evidence for datasource credentials, staged data, intermediate
|
||||||
|
states, search documents, and assurance references;
|
||||||
|
- operator runbooks for stale sources, failed runs, index rebuilds, and graph
|
||||||
|
reconciliation;
|
||||||
|
- accessibility evidence for the catalogue, graph editors, result inspection,
|
||||||
|
reports, and assurance graph;
|
||||||
|
- privacy evidence for minimization, purpose, retention, field visibility, and
|
||||||
|
aggregate disclosure; and
|
||||||
|
- version-pinned user and administrator documentation.
|
||||||
|
|
||||||
|
Optional Connectors, Files, Notifications, and Workflow Engine integrations
|
||||||
|
must remain capability-based and absence-safe.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"package_id": "product.governed-data-assurance",
|
||||||
|
"name": "Governed Data and Assurance",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"package_class": "product",
|
||||||
|
"description": "Compose governed sources, transformations, reports, search, controls and assurance evidence through stable contracts.",
|
||||||
|
"publisher": "GovOPlaN",
|
||||||
|
"category": "data-governance",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"required_modules": [
|
||||||
|
{"module_id": "access"},
|
||||||
|
{"module_id": "audit"},
|
||||||
|
{"module_id": "dataflow"},
|
||||||
|
{"module_id": "datasources"},
|
||||||
|
{"module_id": "policy"},
|
||||||
|
{"module_id": "reporting"},
|
||||||
|
{"module_id": "risk_compliance"},
|
||||||
|
{"module_id": "search"}
|
||||||
|
],
|
||||||
|
"optional_modules": [
|
||||||
|
{"module_id": "connectors"},
|
||||||
|
{"module_id": "files"},
|
||||||
|
{"module_id": "notifications"},
|
||||||
|
{"module_id": "workflow_engine"}
|
||||||
|
],
|
||||||
|
"evidence": [
|
||||||
|
{
|
||||||
|
"kind": "documentation",
|
||||||
|
"reference": "packages/product/governed-data-assurance/README.md",
|
||||||
|
"summary": "Defines the package boundary, provenance chain, and reference-readiness gates."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": ["datasources", "dataflow", "reporting", "assurance"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# Governed Service To Decision
|
||||||
|
|
||||||
|
This product package composes independently owned institutional semantics into
|
||||||
|
one reconstructable administrative journey:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Service discovery -> Case intake -> Party and representation -> Mandate
|
||||||
|
resolution -> approval/deliberation -> formal Decision -> observed delivery
|
||||||
|
effect -> record and review references
|
||||||
|
```
|
||||||
|
|
||||||
|
An installed Forms and Forms Runtime pair adds an alternative governed entry
|
||||||
|
path before case/workflow handoff:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Service discovery -> exact Form revision -> validated draft/submission
|
||||||
|
-> receipt and handoff evidence -> Case or Workflow owner
|
||||||
|
```
|
||||||
|
|
||||||
|
Services, Cases, Parties, Mandates, Committee, and Decisions retain immutable
|
||||||
|
provider-owned revisions for the parts they own. Portal, Cases, and Committee
|
||||||
|
consume capabilities for cross-module semantics only. The package does not
|
||||||
|
grant cross-module table access and can omit optional presentation, work,
|
||||||
|
deliberation, delivery, or records modules while retaining explicit references
|
||||||
|
to externally performed steps.
|
||||||
|
|
||||||
|
## Security And Recovery
|
||||||
|
|
||||||
|
Every provider is tenant-bound. Missing or conflicting authority fails closed.
|
||||||
|
Protected Decision content has a separate permission. Writes are replay-safe
|
||||||
|
and OCC-guarded. Database restore is the semantic-state recovery unit; file and
|
||||||
|
communication effects remain governed by their owning providers and are linked
|
||||||
|
through requested/observed effect, evidence, and audit references.
|
||||||
|
|
||||||
|
The executable fixture in
|
||||||
|
`tests/test_institutional_governance_journey.py` proves SQL-backed Service,
|
||||||
|
Case, Party, Mandate, Committee meeting/agendum/vote/minute, and Decision state.
|
||||||
|
`tests/test_institutional_service_journey.py` separately proves exact Portal
|
||||||
|
Form launch, persisted submission provenance, and idempotent replay.
|
||||||
|
Target-environment accessibility, security, operator, privacy,
|
||||||
|
delivery-provider, and recovery evidence are still required before this product
|
||||||
|
package may claim `reference_ready` maturity.
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"package_id": "product.service-to-decision",
|
||||||
|
"name": "Governed Service To Decision",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"package_class": "product",
|
||||||
|
"description": "Carry one exact institutional context from service discovery and case intake through parties, authority, formal outcome, communication evidence, and review.",
|
||||||
|
"publisher": "GovOPlaN",
|
||||||
|
"category": "institutional-governance",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"required_modules": [
|
||||||
|
{"module_id": "audit"},
|
||||||
|
{"module_id": "cases"},
|
||||||
|
{"module_id": "decisions"},
|
||||||
|
{"module_id": "mandates"},
|
||||||
|
{"module_id": "parties"},
|
||||||
|
{"module_id": "policy"},
|
||||||
|
{"module_id": "portal"},
|
||||||
|
{"module_id": "services"}
|
||||||
|
],
|
||||||
|
"required_capabilities": [
|
||||||
|
"cases.party_context",
|
||||||
|
"cases.service_intake",
|
||||||
|
"decisions.registry",
|
||||||
|
"mandates.resolver",
|
||||||
|
"parties.resolver",
|
||||||
|
"portal.service_directory",
|
||||||
|
"services.availability",
|
||||||
|
"services.definitions"
|
||||||
|
],
|
||||||
|
"optional_modules": [
|
||||||
|
{"module_id": "approvals"},
|
||||||
|
{"module_id": "committee"},
|
||||||
|
{"module_id": "files"},
|
||||||
|
{"module_id": "forms"},
|
||||||
|
{"module_id": "forms_runtime"},
|
||||||
|
{"module_id": "postbox"},
|
||||||
|
{"module_id": "records"},
|
||||||
|
{"module_id": "tasks"},
|
||||||
|
{"module_id": "workflow_engine"}
|
||||||
|
],
|
||||||
|
"evidence": [
|
||||||
|
{
|
||||||
|
"kind": "documentation",
|
||||||
|
"reference": "packages/product/service-to-decision/README.md",
|
||||||
|
"summary": "Defines the package boundary, authority path, recovery contract, and known operational limits."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "target_test",
|
||||||
|
"reference": "tests/test_institutional_governance_journey.py",
|
||||||
|
"summary": "Executes the SQL-backed provider composition from service discovery through persisted formal Decision reconstruction."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "target_test",
|
||||||
|
"reference": "tests/test_institutional_service_journey.py",
|
||||||
|
"summary": "Executes Portal delegation from an exact Service revision through an exact immutable Form revision to a replay-safe persisted submission."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": ["service", "case", "mandate", "party", "decision", "public-sector"]
|
||||||
|
}
|
||||||
+78
-66
@@ -1,73 +1,85 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"organization": "add-ideas",
|
"organization": "GovOPlaN",
|
||||||
"default_parent": "/mnt/DATA/git",
|
"default_parent": "/mnt/DATA/git",
|
||||||
"repositories": [
|
"repositories": [
|
||||||
{"name": "govoplan", "category": "system", "subtype": "meta", "remote": "git@git.add-ideas.de:add-ideas/govoplan.git", "path": "govoplan"},
|
{"name": "govoplan", "category": "system", "subtype": "meta", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan.git", "path": "govoplan"},
|
||||||
{"name": "govoplan-core", "category": "system", "subtype": "kernel", "remote": "git@git.add-ideas.de:add-ideas/govoplan-core.git", "path": "govoplan-core"},
|
{"name": "govoplan-core", "category": "system", "subtype": "kernel", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-core.git", "path": "govoplan-core"},
|
||||||
{"name": "govoplan-access", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-access.git", "path": "govoplan-access"},
|
{"name": "govoplan-access", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-access.git", "path": "govoplan-access"},
|
||||||
{"name": "govoplan-addresses", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-addresses.git", "path": "govoplan-addresses"},
|
{"name": "govoplan-addresses", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-addresses.git", "path": "govoplan-addresses"},
|
||||||
{"name": "govoplan-admin", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-admin.git", "path": "govoplan-admin"},
|
{"name": "govoplan-admin", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-admin.git", "path": "govoplan-admin"},
|
||||||
{"name": "govoplan-appointments", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-appointments.git", "path": "govoplan-appointments"},
|
{"name": "govoplan-appointments", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-appointments.git", "path": "govoplan-appointments"},
|
||||||
{"name": "govoplan-approvals", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-approvals.git", "path": "govoplan-approvals"},
|
{"name": "govoplan-approvals", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-approvals.git", "path": "govoplan-approvals"},
|
||||||
{"name": "govoplan-assets", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-assets.git", "path": "govoplan-assets"},
|
{"name": "govoplan-assets", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-assets.git", "path": "govoplan-assets"},
|
||||||
{"name": "govoplan-audit", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-audit.git", "path": "govoplan-audit"},
|
{"name": "govoplan-audit", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-audit.git", "path": "govoplan-audit"},
|
||||||
{"name": "govoplan-booking", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-booking.git", "path": "govoplan-booking"},
|
{"name": "govoplan-booking", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-booking.git", "path": "govoplan-booking"},
|
||||||
{"name": "govoplan-calendar", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-calendar.git", "path": "govoplan-calendar"},
|
{"name": "govoplan-calendar", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-calendar.git", "path": "govoplan-calendar"},
|
||||||
{"name": "govoplan-campaign", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-campaign.git", "path": "govoplan-campaign"},
|
{"name": "govoplan-campaign", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-campaign.git", "path": "govoplan-campaign"},
|
||||||
{"name": "govoplan-cases", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-cases.git", "path": "govoplan-cases"},
|
{"name": "govoplan-cases", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-cases.git", "path": "govoplan-cases"},
|
||||||
{"name": "govoplan-certificates", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-certificates.git", "path": "govoplan-certificates"},
|
{"name": "govoplan-certificates", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-certificates.git", "path": "govoplan-certificates"},
|
||||||
{"name": "govoplan-committee", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-committee.git", "path": "govoplan-committee"},
|
{"name": "govoplan-committee", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-committee.git", "path": "govoplan-committee"},
|
||||||
{"name": "govoplan-connectors", "category": "connector", "subtype": "connector-hub", "remote": "git@git.add-ideas.de:add-ideas/govoplan-connectors.git", "path": "govoplan-connectors"},
|
{"name": "govoplan-connectors", "category": "connector", "subtype": "connector-hub", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-connectors.git", "path": "govoplan-connectors"},
|
||||||
{"name": "govoplan-consultation", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-consultation.git", "path": "govoplan-consultation"},
|
{"name": "govoplan-consultation", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-consultation.git", "path": "govoplan-consultation"},
|
||||||
{"name": "govoplan-contracts", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-contracts.git", "path": "govoplan-contracts"},
|
{"name": "govoplan-contracts", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-contracts.git", "path": "govoplan-contracts"},
|
||||||
{"name": "govoplan-dashboard", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-dashboard.git", "path": "govoplan-dashboard"},
|
{"name": "govoplan-dashboard", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-dashboard.git", "path": "govoplan-dashboard"},
|
||||||
{"name": "govoplan-dms", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-dms.git", "path": "govoplan-dms"},
|
{"name": "govoplan-dataflow", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-dataflow.git", "path": "govoplan-dataflow"},
|
||||||
{"name": "govoplan-dist-lists", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-dist-lists.git", "path": "govoplan-dist-lists"},
|
{"name": "govoplan-datasources", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-datasources.git", "path": "govoplan-datasources"},
|
||||||
{"name": "govoplan-docs", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-docs.git", "path": "govoplan-docs"},
|
{"name": "govoplan-decisions", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-decisions.git", "path": "govoplan-decisions"},
|
||||||
{"name": "govoplan-erp", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-erp.git", "path": "govoplan-erp"},
|
{"name": "govoplan-dms", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-dms.git", "path": "govoplan-dms"},
|
||||||
{"name": "govoplan-evaluation", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-evaluation.git", "path": "govoplan-evaluation"},
|
{"name": "govoplan-dist-lists", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-dist-lists.git", "path": "govoplan-dist-lists"},
|
||||||
{"name": "govoplan-facilities", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-facilities.git", "path": "govoplan-facilities"},
|
{"name": "govoplan-docs", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-docs.git", "path": "govoplan-docs"},
|
||||||
{"name": "govoplan-files", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-files.git", "path": "govoplan-files"},
|
{"name": "govoplan-encryption", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-encryption.git", "path": "govoplan-encryption"},
|
||||||
{"name": "govoplan-fit-connect", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:add-ideas/govoplan-fit-connect.git", "path": "govoplan-fit-connect"},
|
{"name": "govoplan-erp", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-erp.git", "path": "govoplan-erp"},
|
||||||
{"name": "govoplan-forms", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-forms.git", "path": "govoplan-forms"},
|
{"name": "govoplan-evaluation", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-evaluation.git", "path": "govoplan-evaluation"},
|
||||||
{"name": "govoplan-forms-runtime", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-forms-runtime.git", "path": "govoplan-forms-runtime"},
|
{"name": "govoplan-facilities", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-facilities.git", "path": "govoplan-facilities"},
|
||||||
{"name": "govoplan-grants", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-grants.git", "path": "govoplan-grants"},
|
{"name": "govoplan-files", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-files.git", "path": "govoplan-files"},
|
||||||
{"name": "govoplan-helpdesk", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-helpdesk.git", "path": "govoplan-helpdesk"},
|
{"name": "govoplan-fit-connect", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-fit-connect.git", "path": "govoplan-fit-connect"},
|
||||||
{"name": "govoplan-identity", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-identity.git", "path": "govoplan-identity"},
|
{"name": "govoplan-forms", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-forms.git", "path": "govoplan-forms"},
|
||||||
{"name": "govoplan-identity-trust", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-identity-trust.git", "path": "govoplan-identity-trust"},
|
{"name": "govoplan-forms-runtime", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-forms-runtime.git", "path": "govoplan-forms-runtime"},
|
||||||
{"name": "govoplan-idm", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-idm.git", "path": "govoplan-idm"},
|
{"name": "govoplan-grants", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-grants.git", "path": "govoplan-grants"},
|
||||||
{"name": "govoplan-inspections", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-inspections.git", "path": "govoplan-inspections"},
|
{"name": "govoplan-helpdesk", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-helpdesk.git", "path": "govoplan-helpdesk"},
|
||||||
{"name": "govoplan-issue-reporting", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-issue-reporting.git", "path": "govoplan-issue-reporting"},
|
{"name": "govoplan-identity", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-identity.git", "path": "govoplan-identity"},
|
||||||
{"name": "govoplan-learning", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-learning.git", "path": "govoplan-learning"},
|
{"name": "govoplan-identity-trust", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-identity-trust.git", "path": "govoplan-identity-trust"},
|
||||||
{"name": "govoplan-ledger", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-ledger.git", "path": "govoplan-ledger"},
|
{"name": "govoplan-idm", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-idm.git", "path": "govoplan-idm"},
|
||||||
{"name": "govoplan-mail", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-mail.git", "path": "govoplan-mail"},
|
{"name": "govoplan-inspections", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-inspections.git", "path": "govoplan-inspections"},
|
||||||
{"name": "govoplan-notifications", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-notifications.git", "path": "govoplan-notifications"},
|
{"name": "govoplan-learning", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-learning.git", "path": "govoplan-learning"},
|
||||||
{"name": "govoplan-ops", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-ops.git", "path": "govoplan-ops"},
|
{"name": "govoplan-ledger", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-ledger.git", "path": "govoplan-ledger"},
|
||||||
{"name": "govoplan-organizations", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-organizations.git", "path": "govoplan-organizations"},
|
{"name": "govoplan-mail", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-mail.git", "path": "govoplan-mail"},
|
||||||
{"name": "govoplan-payments", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-payments.git", "path": "govoplan-payments"},
|
{"name": "govoplan-mandates", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-mandates.git", "path": "govoplan-mandates"},
|
||||||
{"name": "govoplan-permits", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-permits.git", "path": "govoplan-permits"},
|
{"name": "govoplan-notifications", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-notifications.git", "path": "govoplan-notifications"},
|
||||||
{"name": "govoplan-policy", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-policy.git", "path": "govoplan-policy"},
|
{"name": "govoplan-ops", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-ops.git", "path": "govoplan-ops"},
|
||||||
{"name": "govoplan-poll", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-poll.git", "path": "govoplan-poll"},
|
{"name": "govoplan-organizations", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-organizations.git", "path": "govoplan-organizations"},
|
||||||
{"name": "govoplan-portal", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-portal.git", "path": "govoplan-portal"},
|
{"name": "govoplan-payments", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-payments.git", "path": "govoplan-payments"},
|
||||||
{"name": "govoplan-postbox", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-postbox.git", "path": "govoplan-postbox"},
|
{"name": "govoplan-parties", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-parties.git", "path": "govoplan-parties"},
|
||||||
{"name": "govoplan-procurement", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-procurement.git", "path": "govoplan-procurement"},
|
{"name": "govoplan-permits", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-permits.git", "path": "govoplan-permits"},
|
||||||
{"name": "govoplan-records", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-records.git", "path": "govoplan-records"},
|
{"name": "govoplan-policy", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-policy.git", "path": "govoplan-policy"},
|
||||||
{"name": "govoplan-reporting", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-reporting.git", "path": "govoplan-reporting"},
|
{"name": "govoplan-poll", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-poll.git", "path": "govoplan-poll"},
|
||||||
{"name": "govoplan-resources", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-resources.git", "path": "govoplan-resources"},
|
{"name": "govoplan-portal", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-portal.git", "path": "govoplan-portal"},
|
||||||
{"name": "govoplan-rest", "category": "connector", "subtype": "protocol", "remote": "git@git.add-ideas.de:add-ideas/govoplan-rest.git", "path": "govoplan-rest"},
|
{"name": "govoplan-postbox", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-postbox.git", "path": "govoplan-postbox"},
|
||||||
{"name": "govoplan-risk-compliance", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-risk-compliance.git", "path": "govoplan-risk-compliance"},
|
{"name": "govoplan-procurement", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-procurement.git", "path": "govoplan-procurement"},
|
||||||
{"name": "govoplan-scheduling", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-scheduling.git", "path": "govoplan-scheduling"},
|
{"name": "govoplan-projects", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-projects.git", "path": "govoplan-projects"},
|
||||||
{"name": "govoplan-search", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-search.git", "path": "govoplan-search"},
|
{"name": "govoplan-records", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-records.git", "path": "govoplan-records"},
|
||||||
{"name": "govoplan-soap", "category": "connector", "subtype": "protocol", "remote": "git@git.add-ideas.de:add-ideas/govoplan-soap.git", "path": "govoplan-soap"},
|
{"name": "govoplan-reporting", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-reporting.git", "path": "govoplan-reporting"},
|
||||||
{"name": "govoplan-tasks", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-tasks.git", "path": "govoplan-tasks"},
|
{"name": "govoplan-resources", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-resources.git", "path": "govoplan-resources"},
|
||||||
{"name": "govoplan-templates", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-templates.git", "path": "govoplan-templates"},
|
{"name": "govoplan-rest", "category": "connector", "subtype": "protocol", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-rest.git", "path": "govoplan-rest"},
|
||||||
{"name": "govoplan-tenancy", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-tenancy.git", "path": "govoplan-tenancy"},
|
{"name": "govoplan-risk-compliance", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-risk-compliance.git", "path": "govoplan-risk-compliance"},
|
||||||
{"name": "govoplan-transparency", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-transparency.git", "path": "govoplan-transparency"},
|
{"name": "govoplan-scheduling", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-scheduling.git", "path": "govoplan-scheduling"},
|
||||||
|
{"name": "govoplan-search", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-search.git", "path": "govoplan-search"},
|
||||||
|
{"name": "govoplan-services", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-services.git", "path": "govoplan-services"},
|
||||||
|
{"name": "govoplan-soap", "category": "connector", "subtype": "protocol", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-soap.git", "path": "govoplan-soap"},
|
||||||
|
{"name": "govoplan-tasks", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-tasks.git", "path": "govoplan-tasks"},
|
||||||
|
{"name": "govoplan-templates", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-templates.git", "path": "govoplan-templates"},
|
||||||
|
{"name": "govoplan-tenancy", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-tenancy.git", "path": "govoplan-tenancy"},
|
||||||
|
{"name": "govoplan-tickets", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-tickets.git", "path": "govoplan-tickets"},
|
||||||
|
{"name": "govoplan-transparency", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-transparency.git", "path": "govoplan-transparency"},
|
||||||
|
{"name": "govoplan-views", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-views.git", "path": "govoplan-views"},
|
||||||
|
{"name": "govoplan-voting", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-voting.git", "path": "govoplan-voting"},
|
||||||
|
{"name": "govoplan-wiki", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-wiki.git", "path": "govoplan-wiki"},
|
||||||
{"name": "addideas-govoplan-website", "category": "website", "subtype": "public-site", "remote": "git@git.add-ideas.de:add-ideas/addideas-govoplan-website.git", "path": "addideas-govoplan-website", "bootstrap_transport": "registered"},
|
{"name": "addideas-govoplan-website", "category": "website", "subtype": "public-site", "remote": "git@git.add-ideas.de:add-ideas/addideas-govoplan-website.git", "path": "addideas-govoplan-website", "bootstrap_transport": "registered"},
|
||||||
{"name": "govoplan-workflow", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-workflow.git", "path": "govoplan-workflow"},
|
{"name": "govoplan-workflow", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-workflow.git", "path": "govoplan-workflow"},
|
||||||
{"name": "govoplan-xoev", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:add-ideas/govoplan-xoev.git", "path": "govoplan-xoev"},
|
{"name": "govoplan-workflow-engine", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-workflow-engine.git", "path": "govoplan-workflow-engine"},
|
||||||
{"name": "govoplan-xrechnung", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:add-ideas/govoplan-xrechnung.git", "path": "govoplan-xrechnung"},
|
{"name": "govoplan-xoev", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-xoev.git", "path": "govoplan-xoev"},
|
||||||
{"name": "govoplan-xta-osci", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:add-ideas/govoplan-xta-osci.git", "path": "govoplan-xta-osci"}
|
{"name": "govoplan-xrechnung", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-xrechnung.git", "path": "govoplan-xrechnung"},
|
||||||
|
{"name": "govoplan-xta-osci", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-xta-osci.git", "path": "govoplan-xta-osci"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,18 +12,41 @@
|
|||||||
-e ../govoplan-admin
|
-e ../govoplan-admin
|
||||||
-e ../govoplan-policy
|
-e ../govoplan-policy
|
||||||
-e ../govoplan-audit
|
-e ../govoplan-audit
|
||||||
|
-e ../govoplan-approvals
|
||||||
-e ../govoplan-dashboard
|
-e ../govoplan-dashboard
|
||||||
-e ../govoplan-addresses
|
-e ../govoplan-addresses
|
||||||
-e ../govoplan-dist-lists
|
-e ../govoplan-dist-lists
|
||||||
|
-e ../govoplan-templates
|
||||||
-e ../govoplan-files
|
-e ../govoplan-files
|
||||||
|
-e ../govoplan-forms
|
||||||
|
-e ../govoplan-forms-runtime
|
||||||
-e ../govoplan-mail
|
-e ../govoplan-mail
|
||||||
-e ../govoplan-campaign
|
-e ../govoplan-campaign
|
||||||
-e ../govoplan-calendar
|
-e ../govoplan-calendar
|
||||||
|
-e ../govoplan-committee
|
||||||
|
-e ../govoplan-cases
|
||||||
|
-e ../govoplan-portal
|
||||||
|
-e ../govoplan-services
|
||||||
|
-e ../govoplan-parties
|
||||||
|
-e ../govoplan-mandates
|
||||||
|
-e ../govoplan-decisions
|
||||||
|
-e ../govoplan-connectors
|
||||||
|
-e ../govoplan-datasources
|
||||||
|
-e ../govoplan-dataflow
|
||||||
|
-e ../govoplan-workflow-engine
|
||||||
|
-e ../govoplan-workflow
|
||||||
|
-e ../govoplan-views
|
||||||
|
-e ../govoplan-voting
|
||||||
|
-e ../govoplan-search
|
||||||
|
-e ../govoplan-risk-compliance
|
||||||
|
-e ../govoplan-postbox
|
||||||
-e ../govoplan-poll
|
-e ../govoplan-poll
|
||||||
-e ../govoplan-scheduling
|
-e ../govoplan-scheduling
|
||||||
-e ../govoplan-notifications
|
-e ../govoplan-notifications
|
||||||
-e ../govoplan-evaluation
|
-e ../govoplan-evaluation
|
||||||
-e ../govoplan-docs
|
-e ../govoplan-docs
|
||||||
|
-e ../govoplan-encryption
|
||||||
|
-e ../govoplan-identity-trust
|
||||||
-e ../govoplan-ops
|
-e ../govoplan-ops
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
httpx2>=2.5,<3
|
httpx2>=2.5,<3
|
||||||
@@ -32,5 +55,7 @@ idna>=3.15
|
|||||||
jsonschema>=4,<5
|
jsonschema>=4,<5
|
||||||
pip>=26.1.2
|
pip>=26.1.2
|
||||||
pip-audit>=2.9,<3
|
pip-audit>=2.9,<3
|
||||||
|
pytest>=9.0.3,<10
|
||||||
|
pygments>=2.20,<3
|
||||||
python-multipart>=0.0.31
|
python-multipart>=0.0.31
|
||||||
ruff>=0.14,<1
|
ruff>=0.14,<1
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Test-harness dependencies used against immutable release source tags.
|
||||||
|
# Keep these separate from requirements-release.txt so they are not part of the
|
||||||
|
# deployable product dependency set.
|
||||||
|
pytest>=9.0.3,<10
|
||||||
|
pygments>=2.20,<3
|
||||||
+15
-15
@@ -1,18 +1,18 @@
|
|||||||
# Whole-product release install from immutable, independently versioned module tags.
|
# Whole-product release install from immutable, independently versioned module tags.
|
||||||
# Only add a module after its referenced tag has been published.
|
# Only add a module after its referenced tag has been published.
|
||||||
../govoplan-core[server]
|
../govoplan-core[server]
|
||||||
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-tenancy.git@v0.1.8
|
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.15
|
||||||
govoplan-organizations @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-organizations.git@v0.1.8
|
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.15
|
||||||
govoplan-identity @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-identity.git@v0.1.8
|
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.15
|
||||||
govoplan-idm @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-idm.git@v0.1.8
|
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.15
|
||||||
govoplan-access @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-access.git@v0.1.8
|
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.15
|
||||||
govoplan-admin @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-admin.git@v0.1.8
|
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.15
|
||||||
govoplan-policy @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-policy.git@v0.1.8
|
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.15
|
||||||
govoplan-audit @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-audit.git@v0.1.8
|
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.15
|
||||||
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-dashboard.git@v0.1.8
|
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.15
|
||||||
govoplan-files @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git@v0.1.8
|
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.15
|
||||||
govoplan-mail @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-mail.git@v0.1.10
|
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.15
|
||||||
govoplan-campaign @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-campaign.git@v0.1.11
|
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.15
|
||||||
govoplan-calendar @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-calendar.git@v0.1.8
|
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.15
|
||||||
govoplan-docs @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-docs.git@v0.1.8
|
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.15
|
||||||
govoplan-ops @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-ops.git@v0.1.8
|
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.15
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||||
|
from jsonschema import Draft202012Validator, FormatChecker
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
GENERATOR = META_ROOT / "tools" / "assessments" / "generate-authority-keypair.py"
|
||||||
|
|
||||||
|
|
||||||
|
class AssessmentAuthorityKeypairTests(unittest.TestCase):
|
||||||
|
def test_generates_schema_valid_scoped_proof_authority(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
output_dir = Path(temp_dir)
|
||||||
|
output_dir.chmod(0o700)
|
||||||
|
private_path = output_dir / "target.pem"
|
||||||
|
keyring_path = output_dir / "target.json"
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
(
|
||||||
|
sys.executable,
|
||||||
|
str(GENERATOR),
|
||||||
|
"--purpose",
|
||||||
|
"proof",
|
||||||
|
"--key-id",
|
||||||
|
"authority:target-2026",
|
||||||
|
"--scope",
|
||||||
|
"target_environment",
|
||||||
|
"--scope",
|
||||||
|
"operations",
|
||||||
|
"--private-key",
|
||||||
|
str(private_path),
|
||||||
|
"--keyring",
|
||||||
|
str(keyring_path),
|
||||||
|
),
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(0, result.returncode, result.stderr)
|
||||||
|
self.assertEqual(0o600, stat.S_IMODE(private_path.stat().st_mode))
|
||||||
|
self.assertEqual(0o600, stat.S_IMODE(keyring_path.stat().st_mode))
|
||||||
|
keyring = json.loads(keyring_path.read_text(encoding="utf-8"))
|
||||||
|
schema = json.loads(
|
||||||
|
(
|
||||||
|
META_ROOT
|
||||||
|
/ "docs"
|
||||||
|
/ "capability-fit-proof-authority-keyring.schema.json"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
errors = tuple(
|
||||||
|
Draft202012Validator(
|
||||||
|
schema, format_checker=FormatChecker()
|
||||||
|
).iter_errors(keyring)
|
||||||
|
)
|
||||||
|
self.assertEqual((), errors)
|
||||||
|
self.assertEqual(
|
||||||
|
["target_environment", "operations"],
|
||||||
|
keyring["keys"][0]["allowed_scopes"],
|
||||||
|
)
|
||||||
|
private_key = serialization.load_pem_private_key(
|
||||||
|
private_path.read_bytes(), password=None
|
||||||
|
)
|
||||||
|
self.assertIsInstance(private_key, Ed25519PrivateKey)
|
||||||
|
public_key = base64.b64encode(
|
||||||
|
private_key.public_key().public_bytes(
|
||||||
|
encoding=serialization.Encoding.Raw,
|
||||||
|
format=serialization.PublicFormat.Raw,
|
||||||
|
)
|
||||||
|
).decode("ascii")
|
||||||
|
self.assertEqual(public_key, keyring["keys"][0]["public_key"])
|
||||||
|
|
||||||
|
def test_installer_authority_uses_fixed_scope_and_refuses_overwrite(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
output_dir = Path(temp_dir)
|
||||||
|
output_dir.chmod(0o700)
|
||||||
|
private_path = output_dir / "installer.pem"
|
||||||
|
keyring_path = output_dir / "installer.json"
|
||||||
|
command = (
|
||||||
|
sys.executable,
|
||||||
|
str(GENERATOR),
|
||||||
|
"--purpose",
|
||||||
|
"installer",
|
||||||
|
"--key-id",
|
||||||
|
"authority:installer-2026",
|
||||||
|
"--private-key",
|
||||||
|
str(private_path),
|
||||||
|
"--keyring",
|
||||||
|
str(keyring_path),
|
||||||
|
)
|
||||||
|
|
||||||
|
first = subprocess.run(
|
||||||
|
command, check=False, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
second = subprocess.run(
|
||||||
|
command, check=False, capture_output=True, text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(0, first.returncode, first.stderr)
|
||||||
|
self.assertNotEqual(0, second.returncode)
|
||||||
|
keyring = json.loads(keyring_path.read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(
|
||||||
|
["installed_release_origin"],
|
||||||
|
keyring["keys"][0]["allowed_scopes"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,430 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from contextlib import redirect_stderr, redirect_stdout
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
import hashlib
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(META_ROOT / "tools" / "deployment"))
|
||||||
|
|
||||||
|
from govoplan_deploy.backup_evidence import verify_backup_evidence # noqa: E402
|
||||||
|
from govoplan_deploy.bundle import ( # noqa: E402
|
||||||
|
atomic_write,
|
||||||
|
bundle_paths,
|
||||||
|
canonical_json,
|
||||||
|
read_env,
|
||||||
|
)
|
||||||
|
from govoplan_deploy.cli import main as deploy_main # noqa: E402
|
||||||
|
from govoplan_deploy.distribution import ( # noqa: E402
|
||||||
|
DistributionError,
|
||||||
|
canonical_signed_payload,
|
||||||
|
canonical_json as canonical_distribution_json,
|
||||||
|
)
|
||||||
|
from govoplan_deploy.model import default_spec, parse_spec # noqa: E402
|
||||||
|
from govoplan_deploy.planning import ( # noqa: E402
|
||||||
|
release_change_requires_backup,
|
||||||
|
verify_stored_backup_evidence,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BackupEvidenceTests(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.now = datetime(2026, 8, 3, 12, tzinfo=UTC)
|
||||||
|
self.private = Ed25519PrivateKey.generate()
|
||||||
|
public = (
|
||||||
|
self.private.public_key()
|
||||||
|
.public_bytes(
|
||||||
|
serialization.Encoding.PEM,
|
||||||
|
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||||
|
)
|
||||||
|
.decode("ascii")
|
||||||
|
)
|
||||||
|
self.keyring = {
|
||||||
|
"schema_version": "1",
|
||||||
|
"purpose": "govoplan-backup-evidence",
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"key_id": "backup-controller-1",
|
||||||
|
"algorithm": "ed25519",
|
||||||
|
"status": "active",
|
||||||
|
"public_key_pem": public,
|
||||||
|
"not_before": (self.now - timedelta(days=1)).isoformat(),
|
||||||
|
"expires_at": (self.now + timedelta(days=365)).isoformat(),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
self.release = {
|
||||||
|
"channel": "stable",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"manifest_sha256": "a" * 64,
|
||||||
|
"composition_sha256": "b" * 64,
|
||||||
|
"api_image": "registry.example/api@sha256:" + "c" * 64,
|
||||||
|
"web_image": "registry.example/web@sha256:" + "d" * 64,
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_verifies_coordinated_restore_drill_and_release_binding(self) -> None:
|
||||||
|
summary = verify_backup_evidence(
|
||||||
|
self._evidence(),
|
||||||
|
self.keyring,
|
||||||
|
installation_id="govoplan-test",
|
||||||
|
profile="self-hosted",
|
||||||
|
release=self.release,
|
||||||
|
now=self.now,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("recovery-1", summary["recovery_point_id"])
|
||||||
|
self.assertEqual("restore-1", summary["restore_drill_id"])
|
||||||
|
self.assertEqual("backup-controller-1", summary["signature_key_id"])
|
||||||
|
|
||||||
|
def test_tampering_staleness_and_partial_restore_fail_closed(self) -> None:
|
||||||
|
tampered = self._evidence()
|
||||||
|
tampered["components"]["objects"]["object_count"] = 999
|
||||||
|
with self.assertRaisesRegex(DistributionError, "signature verification"):
|
||||||
|
verify_backup_evidence(
|
||||||
|
tampered,
|
||||||
|
self.keyring,
|
||||||
|
installation_id="govoplan-test",
|
||||||
|
profile="self-hosted",
|
||||||
|
release=self.release,
|
||||||
|
now=self.now,
|
||||||
|
)
|
||||||
|
|
||||||
|
stale = self._evidence(captured=self.now - timedelta(days=2))
|
||||||
|
with self.assertRaisesRegex(DistributionError, "stale"):
|
||||||
|
verify_backup_evidence(
|
||||||
|
stale,
|
||||||
|
self.keyring,
|
||||||
|
installation_id="govoplan-test",
|
||||||
|
profile="self-hosted",
|
||||||
|
release=self.release,
|
||||||
|
now=self.now,
|
||||||
|
)
|
||||||
|
|
||||||
|
partial = self._evidence()
|
||||||
|
partial["restore_drill"]["objects_verified"] = False
|
||||||
|
partial["signatures"] = [self._signature(partial)]
|
||||||
|
with self.assertRaisesRegex(DistributionError, "objects_verified"):
|
||||||
|
verify_backup_evidence(
|
||||||
|
partial,
|
||||||
|
self.keyring,
|
||||||
|
installation_id="govoplan-test",
|
||||||
|
profile="self-hosted",
|
||||||
|
release=self.release,
|
||||||
|
now=self.now,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_wrong_release_key_purpose_and_component_skew_fail_closed(self) -> None:
|
||||||
|
wrong_release = dict(self.release)
|
||||||
|
wrong_release["version"] = "1.2.4"
|
||||||
|
with self.assertRaisesRegex(DistributionError, "release field"):
|
||||||
|
verify_backup_evidence(
|
||||||
|
self._evidence(),
|
||||||
|
self.keyring,
|
||||||
|
installation_id="govoplan-test",
|
||||||
|
profile="self-hosted",
|
||||||
|
release=wrong_release,
|
||||||
|
now=self.now,
|
||||||
|
)
|
||||||
|
|
||||||
|
wrong_keyring = dict(self.keyring)
|
||||||
|
wrong_keyring["purpose"] = "govoplan-runtime-distribution"
|
||||||
|
with self.assertRaisesRegex(DistributionError, "wrong purpose"):
|
||||||
|
verify_backup_evidence(
|
||||||
|
self._evidence(),
|
||||||
|
wrong_keyring,
|
||||||
|
installation_id="govoplan-test",
|
||||||
|
profile="self-hosted",
|
||||||
|
release=self.release,
|
||||||
|
now=self.now,
|
||||||
|
)
|
||||||
|
|
||||||
|
skewed = self._evidence()
|
||||||
|
skewed["components"]["database"]["captured_at"] = (
|
||||||
|
self.now - timedelta(hours=1)
|
||||||
|
).isoformat()
|
||||||
|
skewed["signatures"] = [self._signature(skewed)]
|
||||||
|
with self.assertRaisesRegex(DistributionError, "one recovery point"):
|
||||||
|
verify_backup_evidence(
|
||||||
|
skewed,
|
||||||
|
self.keyring,
|
||||||
|
installation_id="govoplan-test",
|
||||||
|
profile="self-hosted",
|
||||||
|
release=self.release,
|
||||||
|
now=self.now,
|
||||||
|
)
|
||||||
|
|
||||||
|
false_rto = self._evidence()
|
||||||
|
false_rto["restore_drill"]["measured_rto_seconds"] = 1
|
||||||
|
false_rto["signatures"] = [self._signature(false_rto)]
|
||||||
|
with self.assertRaisesRegex(DistributionError, "RTO"):
|
||||||
|
verify_backup_evidence(
|
||||||
|
false_rto,
|
||||||
|
self.keyring,
|
||||||
|
installation_id="govoplan-test",
|
||||||
|
profile="self-hosted",
|
||||||
|
release=self.release,
|
||||||
|
now=self.now,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_provider_signing_tool_emits_canonical_verified_evidence(self) -> None:
|
||||||
|
self.now = datetime.now(UTC)
|
||||||
|
self.keyring["keys"][0]["not_before"] = (
|
||||||
|
self.now - timedelta(days=1)
|
||||||
|
).isoformat()
|
||||||
|
self.keyring["keys"][0]["expires_at"] = (
|
||||||
|
self.now + timedelta(days=365)
|
||||||
|
).isoformat()
|
||||||
|
evidence = self._evidence()
|
||||||
|
evidence["signatures"] = []
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-backup-signer-") as value:
|
||||||
|
root = Path(value)
|
||||||
|
source = root / "unsigned.json"
|
||||||
|
output = root / "signed.json"
|
||||||
|
keyring = root / "keyring.json"
|
||||||
|
private_key = root / "private.pem"
|
||||||
|
atomic_write(source, canonical_json(evidence), mode=0o600)
|
||||||
|
atomic_write(keyring, canonical_json(self.keyring), mode=0o600)
|
||||||
|
atomic_write(
|
||||||
|
private_key,
|
||||||
|
self.private.private_bytes(
|
||||||
|
serialization.Encoding.PEM,
|
||||||
|
serialization.PrivateFormat.PKCS8,
|
||||||
|
serialization.NoEncryption(),
|
||||||
|
),
|
||||||
|
mode=0o600,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[
|
||||||
|
sys.executable,
|
||||||
|
str(META_ROOT / "tools/deployment/sign-backup-evidence.py"),
|
||||||
|
"--input",
|
||||||
|
str(source),
|
||||||
|
"--output",
|
||||||
|
str(output),
|
||||||
|
"--trusted-keyring",
|
||||||
|
str(keyring),
|
||||||
|
"--signing-key",
|
||||||
|
f"backup-controller-1={private_key}",
|
||||||
|
],
|
||||||
|
cwd=META_ROOT,
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(0, result.returncode, result.stderr)
|
||||||
|
encoded = output.read_bytes()
|
||||||
|
signed = json.loads(encoded)
|
||||||
|
self.assertEqual(canonical_distribution_json(signed), encoded)
|
||||||
|
summary = verify_backup_evidence(
|
||||||
|
signed,
|
||||||
|
self.keyring,
|
||||||
|
installation_id="govoplan-test",
|
||||||
|
profile="self-hosted",
|
||||||
|
release=self.release,
|
||||||
|
)
|
||||||
|
self.assertEqual("backup-controller-1", summary["signature_key_id"])
|
||||||
|
|
||||||
|
def test_cli_adoption_gates_the_next_release_against_previous_receipt(self) -> None:
|
||||||
|
self.now = datetime.now(UTC)
|
||||||
|
self.keyring["keys"][0]["not_before"] = (
|
||||||
|
self.now - timedelta(days=1)
|
||||||
|
).isoformat()
|
||||||
|
self.keyring["keys"][0]["expires_at"] = (
|
||||||
|
self.now + timedelta(days=365)
|
||||||
|
).isoformat()
|
||||||
|
evidence = self._evidence()
|
||||||
|
encoded_evidence = canonical_distribution_json(evidence)
|
||||||
|
encoded_keyring = canonical_distribution_json(self.keyring)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-backup-evidence-") as value:
|
||||||
|
paths = bundle_paths(Path(value))
|
||||||
|
paths.root.chmod(0o700)
|
||||||
|
raw = default_spec(
|
||||||
|
installation_id="govoplan-test",
|
||||||
|
profile="self-hosted",
|
||||||
|
public_url="https://govoplan.example.test",
|
||||||
|
ingress_mode="existing-proxy",
|
||||||
|
trusted_proxy_cidrs=("127.0.0.1/32",),
|
||||||
|
).to_dict()
|
||||||
|
raw["release"] = {
|
||||||
|
**raw["release"],
|
||||||
|
**self.release,
|
||||||
|
}
|
||||||
|
current = parse_spec(raw)
|
||||||
|
atomic_write(paths.spec, canonical_json(current.to_dict()), mode=0o600)
|
||||||
|
source_evidence = paths.root / "source-backup.json"
|
||||||
|
source_keyring = paths.root / "source-keyring.json"
|
||||||
|
atomic_write(source_evidence, encoded_evidence, mode=0o600)
|
||||||
|
atomic_write(source_keyring, encoded_keyring, mode=0o600)
|
||||||
|
|
||||||
|
output = io.StringIO()
|
||||||
|
with redirect_stdout(output), redirect_stderr(output):
|
||||||
|
result = deploy_main(
|
||||||
|
[
|
||||||
|
"verify-backup",
|
||||||
|
"--directory",
|
||||||
|
str(paths.root),
|
||||||
|
"--evidence",
|
||||||
|
str(source_evidence),
|
||||||
|
"--evidence-sha256",
|
||||||
|
hashlib.sha256(encoded_evidence).hexdigest(),
|
||||||
|
"--trusted-keyring",
|
||||||
|
str(source_keyring),
|
||||||
|
"--adopt",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertEqual(0, result, output.getvalue())
|
||||||
|
runtime_environment = read_env(paths.env)
|
||||||
|
self.assertEqual(
|
||||||
|
"verified",
|
||||||
|
runtime_environment["GOVOPLAN_BACKUP_EVIDENCE_STATE"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"recovery-1",
|
||||||
|
runtime_environment["GOVOPLAN_BACKUP_RECOVERY_POINT_ID"],
|
||||||
|
)
|
||||||
|
self.assertNotIn("snapshot:postgres", str(runtime_environment))
|
||||||
|
self.assertNotIn("urn:kms", str(runtime_environment))
|
||||||
|
|
||||||
|
receipt = {
|
||||||
|
"installation_id": current.installation_id,
|
||||||
|
"profile": current.profile,
|
||||||
|
"release": dict(self.release),
|
||||||
|
}
|
||||||
|
atomic_write(paths.receipt, canonical_json(receipt), mode=0o600)
|
||||||
|
target_raw = current.to_dict()
|
||||||
|
target_raw["release"]["version"] = "1.2.4"
|
||||||
|
target_raw["release"]["manifest_sha256"] = "9" * 64
|
||||||
|
target = parse_spec(target_raw)
|
||||||
|
|
||||||
|
self.assertTrue(release_change_requires_backup(target, receipt))
|
||||||
|
summary = verify_stored_backup_evidence(
|
||||||
|
target,
|
||||||
|
paths,
|
||||||
|
receipt=receipt,
|
||||||
|
)
|
||||||
|
self.assertEqual("recovery-1", summary["recovery_point_id"])
|
||||||
|
|
||||||
|
def _evidence(self, *, captured: datetime | None = None) -> dict[str, object]:
|
||||||
|
captured = captured or self.now - timedelta(hours=2)
|
||||||
|
started = captured + timedelta(minutes=15)
|
||||||
|
completed = captured + timedelta(minutes=30)
|
||||||
|
issued = completed + timedelta(minutes=10)
|
||||||
|
artifact_time = captured.isoformat()
|
||||||
|
payload: dict[str, object] = {
|
||||||
|
"schema_version": "1",
|
||||||
|
"evidence_id": "backup-1",
|
||||||
|
"installation_id": "govoplan-test",
|
||||||
|
"deployment_subject": {
|
||||||
|
"profile": "self-hosted",
|
||||||
|
"topology": "compose",
|
||||||
|
"subject_ref": "urn:govoplan:installation:govoplan-test",
|
||||||
|
},
|
||||||
|
"release": dict(self.release),
|
||||||
|
"recovery_point": {
|
||||||
|
"id": "recovery-1",
|
||||||
|
"captured_at": captured.isoformat(),
|
||||||
|
"consistency": "application-quiesced",
|
||||||
|
"rpo_seconds": 300,
|
||||||
|
"write_fence": {
|
||||||
|
"mode": "application-quiesce",
|
||||||
|
"token_sha256": "e" * 64,
|
||||||
|
"established_at": captured.isoformat(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"database": {
|
||||||
|
"provider": "postgres",
|
||||||
|
"artifact_ref": "snapshot:postgres:backup-1",
|
||||||
|
"artifact_sha256": "1" * 64,
|
||||||
|
"snapshot_id": "pg-snapshot-1",
|
||||||
|
"lsn": "0/16B6C50",
|
||||||
|
"protected": True,
|
||||||
|
"encryption_key_ref": "urn:kms:key:database-backup",
|
||||||
|
"captured_at": artifact_time,
|
||||||
|
},
|
||||||
|
"objects": {
|
||||||
|
"provider": "s3",
|
||||||
|
"artifact_ref": "s3://backup/govoplan-test/recovery-1",
|
||||||
|
"manifest_sha256": "2" * 64,
|
||||||
|
"version_id": "object-snapshot-1",
|
||||||
|
"object_count": 4,
|
||||||
|
"total_bytes": 1024,
|
||||||
|
"protected": True,
|
||||||
|
"encryption_key_ref": "urn:kms:key:object-backup",
|
||||||
|
"captured_at": artifact_time,
|
||||||
|
},
|
||||||
|
"configuration": {
|
||||||
|
"artifact_ref": "backup:configuration:recovery-1",
|
||||||
|
"sha256": "3" * 64,
|
||||||
|
"protected": True,
|
||||||
|
"encryption_key_ref": "urn:kms:key:configuration-backup",
|
||||||
|
"captured_at": artifact_time,
|
||||||
|
},
|
||||||
|
"key_custody": {
|
||||||
|
"provider": "kms",
|
||||||
|
"keyset_ref": "urn:kms:keyset:govoplan-test",
|
||||||
|
"keyset_version": "version-4",
|
||||||
|
"recoverable": True,
|
||||||
|
"captured_at": artifact_time,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"restore_drill": {
|
||||||
|
"drill_id": "restore-1",
|
||||||
|
"recovery_point_id": "recovery-1",
|
||||||
|
"started_at": started.isoformat(),
|
||||||
|
"completed_at": completed.isoformat(),
|
||||||
|
"isolated_target_ref": "urn:govoplan:restore-target:restore-1",
|
||||||
|
"release_manifest_sha256": self.release["manifest_sha256"],
|
||||||
|
"migration_heads_sha256": "4" * 64,
|
||||||
|
"representative_object_manifest_sha256": "2" * 64,
|
||||||
|
"database_verified": True,
|
||||||
|
"objects_verified": True,
|
||||||
|
"configuration_verified": True,
|
||||||
|
"key_custody_verified": True,
|
||||||
|
"semantic_checks": [
|
||||||
|
{
|
||||||
|
"id": "institutional-journey",
|
||||||
|
"status": "passed",
|
||||||
|
"evidence_ref": "evidence:journey:institutional-1",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"measured_rpo_seconds": 120,
|
||||||
|
"measured_rto_seconds": 900,
|
||||||
|
"evidence_ref": "evidence:restore:restore-1",
|
||||||
|
},
|
||||||
|
"issued_at": issued.isoformat(),
|
||||||
|
"expires_at": (self.now + timedelta(days=7)).isoformat(),
|
||||||
|
"revoked": False,
|
||||||
|
"signatures": [],
|
||||||
|
}
|
||||||
|
payload["signatures"] = [self._signature(payload)]
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def _signature(self, payload: dict[str, object]) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"key_id": "backup-controller-1",
|
||||||
|
"algorithm": "ed25519",
|
||||||
|
"value": base64.b64encode(
|
||||||
|
self.private.sign(canonical_signed_payload(payload))
|
||||||
|
).decode("ascii"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
ASSESSMENT_TOOLS_ROOT = META_ROOT / "tools" / "assessments"
|
||||||
|
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
|
||||||
|
for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
|
||||||
|
if str(tools_root) not in sys.path:
|
||||||
|
sys.path.insert(0, str(tools_root))
|
||||||
|
|
||||||
|
from govoplan_assessment.boundary_evidence import ( # noqa: E402
|
||||||
|
issue_boundary_evidence,
|
||||||
|
)
|
||||||
|
from govoplan_assessment.evidence import ( # noqa: E402
|
||||||
|
InstalledEvidenceReview,
|
||||||
|
canonical_sha256,
|
||||||
|
review_boundary_evidence,
|
||||||
|
validate_payload,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BoundaryEvidenceIssuerTests(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.assessment = {
|
||||||
|
"assessment_id": "assessment:test",
|
||||||
|
"release": {"ref": "stable-catalog-202608020001"},
|
||||||
|
"deployment_profile": {"id": "deployment:target"},
|
||||||
|
}
|
||||||
|
self.installed = {
|
||||||
|
"schema_version": "0.1.0",
|
||||||
|
"assessment_id": "assessment:test",
|
||||||
|
"artifacts": [],
|
||||||
|
}
|
||||||
|
self.private_key = Ed25519PrivateKey.generate()
|
||||||
|
public_key = base64.b64encode(
|
||||||
|
self.private_key.public_key().public_bytes(
|
||||||
|
encoding=serialization.Encoding.Raw,
|
||||||
|
format=serialization.PublicFormat.Raw,
|
||||||
|
)
|
||||||
|
).decode("ascii")
|
||||||
|
self.authority = {
|
||||||
|
"$schema": "./capability-fit-proof-authority-keyring.schema.json",
|
||||||
|
"schema_version": "0.1.0",
|
||||||
|
"purpose": "govoplan.capability-fit-proof-authorities",
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"key_id": "authority:target",
|
||||||
|
"status": "active",
|
||||||
|
"public_key": public_key,
|
||||||
|
"allowed_scopes": ["target_environment", "recovery"],
|
||||||
|
"not_before": "2026-08-01T00:00:00Z",
|
||||||
|
"not_after": "2026-09-01T00:00:00Z",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
self.boundary_schema = json.loads(
|
||||||
|
(
|
||||||
|
META_ROOT / "docs" / "capability-fit-boundary-evidence.schema.json"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
self.authority_schema = json.loads(
|
||||||
|
(
|
||||||
|
META_ROOT
|
||||||
|
/ "docs"
|
||||||
|
/ "capability-fit-proof-authority-keyring.schema.json"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_issues_sanitized_hash_bound_proof_and_verifies_it(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
result_path = Path(temp_dir) / "recovery-result.json"
|
||||||
|
result_path.write_text(
|
||||||
|
'{"rto_seconds":42,"rpo_seconds":0,"reconstructed":true}\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
proof = self.issue(
|
||||||
|
claims=[
|
||||||
|
{
|
||||||
|
"scope": "target_environment",
|
||||||
|
"result": "passed",
|
||||||
|
"control_ids": ["topology:shared-state-v1"],
|
||||||
|
"artifacts": [
|
||||||
|
{"artifact_id": "target:run-1", "path": result_path}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"scope": "recovery",
|
||||||
|
"result": "passed",
|
||||||
|
"control_ids": ["recovery:restore-v1"],
|
||||||
|
"artifacts": [
|
||||||
|
{"artifact_id": "recovery:run-1", "path": result_path}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
(), validate_payload(payload=proof, schema=self.boundary_schema)
|
||||||
|
)
|
||||||
|
self.assertNotIn(str(result_path), json.dumps(proof))
|
||||||
|
self.assertEqual(
|
||||||
|
canonical_sha256(self.installed), proof["installed_evidence_sha256"]
|
||||||
|
)
|
||||||
|
installed_review = InstalledEvidenceReview(
|
||||||
|
findings=(),
|
||||||
|
changes=(),
|
||||||
|
changed_repositories=frozenset(),
|
||||||
|
affected_module_ids=frozenset(),
|
||||||
|
proof_scope={
|
||||||
|
"installed_artifacts": {"valid": True},
|
||||||
|
"installed_release_origin": {"valid": True},
|
||||||
|
},
|
||||||
|
evidence_sha256=canonical_sha256(self.installed),
|
||||||
|
)
|
||||||
|
review = review_boundary_evidence(
|
||||||
|
assessment=self.assessment,
|
||||||
|
installed_review=installed_review,
|
||||||
|
evidence=proof,
|
||||||
|
evidence_schema=self.boundary_schema,
|
||||||
|
authority_keyring=self.authority,
|
||||||
|
authority_keyring_schema=self.authority_schema,
|
||||||
|
verification_time=datetime(2026, 8, 2, 12, 30, tzinfo=UTC),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual((), review.findings)
|
||||||
|
self.assertTrue(review.proof_scope["target_environment"]["valid"])
|
||||||
|
self.assertTrue(review.proof_scope["recovery"]["valid"])
|
||||||
|
|
||||||
|
def test_refuses_missing_release_origin_or_scope_authority(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
artifact = Path(temp_dir) / "result.txt"
|
||||||
|
artifact.write_text("passed\n", encoding="utf-8")
|
||||||
|
claim = {
|
||||||
|
"scope": "security",
|
||||||
|
"result": "passed",
|
||||||
|
"control_ids": ["security:sast-v1"],
|
||||||
|
"artifacts": [{"artifact_id": "security:run-1", "path": artifact}],
|
||||||
|
}
|
||||||
|
with self.assertRaisesRegex(ValueError, "release origin"):
|
||||||
|
self.issue(claims=[claim], release_origin_verified=False)
|
||||||
|
with self.assertRaisesRegex(ValueError, "authorized for scopes: security"):
|
||||||
|
self.issue(claims=[claim])
|
||||||
|
|
||||||
|
def test_refuses_authority_that_expires_before_the_proof(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
artifact = Path(temp_dir) / "result.txt"
|
||||||
|
artifact.write_text("passed\n", encoding="utf-8")
|
||||||
|
with self.assertRaisesRegex(ValueError, "full proof interval"):
|
||||||
|
self.issue(
|
||||||
|
expires_at=datetime(2026, 10, 1, tzinfo=UTC),
|
||||||
|
claims=[
|
||||||
|
{
|
||||||
|
"scope": "recovery",
|
||||||
|
"result": "passed",
|
||||||
|
"control_ids": ["recovery:restore-v1"],
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"artifact_id": "recovery:run-1",
|
||||||
|
"path": artifact,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
def issue(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
claims: list[dict[str, object]],
|
||||||
|
expires_at: datetime = datetime(2026, 8, 3, tzinfo=UTC),
|
||||||
|
release_origin_verified: bool = True,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
return issue_boundary_evidence(
|
||||||
|
assessment=self.assessment,
|
||||||
|
installed_evidence=self.installed,
|
||||||
|
proof_id="proof:target:1",
|
||||||
|
claims=claims,
|
||||||
|
authority_keyring=self.authority,
|
||||||
|
signing_keys={"authority:target": self.private_key},
|
||||||
|
issued_at=datetime(2026, 8, 2, 12, tzinfo=UTC),
|
||||||
|
expires_at=expires_at,
|
||||||
|
release_origin_verified=release_origin_verified,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1007,6 +1007,44 @@ class CapabilityFitEvidenceTests(unittest.TestCase):
|
|||||||
report["proof_scope"]["target_environment"]["observed_subject_id"],
|
report["proof_scope"]["target_environment"]["observed_subject_id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_reference_readiness_scopes_require_release_bound_signed_proof(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
installed = matching_installed_evidence(self.assessment)
|
||||||
|
catalog, keyring = signed_catalog(
|
||||||
|
self.assessment,
|
||||||
|
release_artifacts=catalog_artifacts_for(installed),
|
||||||
|
)
|
||||||
|
readiness_scopes = (
|
||||||
|
"target_environment",
|
||||||
|
"accessibility",
|
||||||
|
"privacy",
|
||||||
|
"security",
|
||||||
|
"operations",
|
||||||
|
"recovery",
|
||||||
|
)
|
||||||
|
proof, authority = signed_boundary_evidence(
|
||||||
|
assessment=self.assessment,
|
||||||
|
installed=installed,
|
||||||
|
claims=[boundary_claim(scope, "passed") for scope in readiness_scopes],
|
||||||
|
allowed_scopes=list(readiness_scopes),
|
||||||
|
)
|
||||||
|
|
||||||
|
report = self.review(
|
||||||
|
catalog=catalog,
|
||||||
|
keyring=keyring,
|
||||||
|
installed_evidence=installed,
|
||||||
|
boundary_evidence=proof,
|
||||||
|
authority_keyring=authority,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("current", report["status"])
|
||||||
|
for scope in readiness_scopes:
|
||||||
|
with self.subTest(scope=scope):
|
||||||
|
self.assertTrue(report["proof_scope"][scope]["checked"])
|
||||||
|
self.assertTrue(report["proof_scope"][scope]["valid"])
|
||||||
|
self.assertTrue(report["proof_scope"]["reference_readiness"]["valid"])
|
||||||
|
|
||||||
def test_external_provider_claim_requires_explicit_matching_subject(self) -> None:
|
def test_external_provider_claim_requires_explicit_matching_subject(self) -> None:
|
||||||
catalog, keyring = signed_catalog(self.assessment)
|
catalog, keyring = signed_catalog(self.assessment)
|
||||||
installed = matching_installed_evidence(self.assessment)
|
installed = matching_installed_evidence(self.assessment)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ for tools_root in (ASSESSMENT_TOOLS_ROOT, RELEASE_TOOLS_ROOT):
|
|||||||
sys.path.insert(0, str(tools_root))
|
sys.path.insert(0, str(tools_root))
|
||||||
|
|
||||||
from govoplan_assessment.capability_fit import ( # noqa: E402
|
from govoplan_assessment.capability_fit import ( # noqa: E402
|
||||||
|
enforce_required_boundary_scopes,
|
||||||
local_tag_provenance,
|
local_tag_provenance,
|
||||||
render_review,
|
render_review,
|
||||||
review_capability_fit,
|
review_capability_fit,
|
||||||
@@ -74,6 +75,34 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
|||||||
json.dumps(report, sort_keys=True), json.dumps(repeated, sort_keys=True)
|
json.dumps(report, sort_keys=True), json.dumps(repeated, sort_keys=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_required_boundary_scope_blocks_admission_until_positive(self) -> None:
|
||||||
|
catalog, keyring = signed_catalog(self.assessment)
|
||||||
|
report = review_capability_fit(
|
||||||
|
assessment=deepcopy(self.assessment),
|
||||||
|
schema=self.schema,
|
||||||
|
catalog=catalog,
|
||||||
|
published_keyring=keyring,
|
||||||
|
trusted_keyring=keyring,
|
||||||
|
)
|
||||||
|
|
||||||
|
enforce_required_boundary_scopes(
|
||||||
|
report, required_scopes=("security", "recovery")
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("blocked", report["status"])
|
||||||
|
self.assertFalse(report["proof_scope"]["admission"]["valid"])
|
||||||
|
self.assertEqual(
|
||||||
|
["recovery", "security"],
|
||||||
|
report["proof_scope"]["admission"]["failed_scopes"],
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
2,
|
||||||
|
sum(
|
||||||
|
item["code"] == "required_boundary_scope_unsatisfied"
|
||||||
|
for item in report["findings"]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def test_release_drift_identifies_affected_conclusions(self) -> None:
|
def test_release_drift_identifies_affected_conclusions(self) -> None:
|
||||||
catalog, keyring = signed_catalog(
|
catalog, keyring = signed_catalog(
|
||||||
self.assessment, versions={"campaigns": "0.1.11"}, sequence=202607230001
|
self.assessment, versions={"campaigns": "0.1.11"}, sequence=202607230001
|
||||||
@@ -360,7 +389,7 @@ class CapabilityFitReviewTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertIn("Capability fit rerun: current", rendered)
|
self.assertIn("Capability fit rerun: current", rendered)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"No installed-composition, installed-release-origin, target-environment, external-provider, production-approval proof",
|
"No installed-composition, installed-release-origin, target-environment, external-providers, accessibility, privacy, security, operations, recovery, production-approval proof",
|
||||||
rendered,
|
rendered,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from govoplan_core.core.configuration_packages import (
|
||||||
|
ConfigurationPackageManifest,
|
||||||
|
ConfigurationPreflightContext,
|
||||||
|
configuration_package_claim_issues,
|
||||||
|
dry_run_configuration_package,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
PACKAGE_ROOT = ROOT / "packages"
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigurationPackageArtifactTests(unittest.TestCase):
|
||||||
|
def test_product_package_manifests_are_portable_and_evidence_backed(self) -> None:
|
||||||
|
paths = tuple(sorted(PACKAGE_ROOT.glob("*/*/package.json")))
|
||||||
|
self.assertGreaterEqual(len(paths), 2)
|
||||||
|
package_ids: set[str] = set()
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
manifest = ConfigurationPackageManifest.from_mapping(
|
||||||
|
json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
)
|
||||||
|
self.assertNotIn(manifest.package_id, package_ids)
|
||||||
|
package_ids.add(manifest.package_id)
|
||||||
|
self.assertEqual((), configuration_package_claim_issues(manifest))
|
||||||
|
for evidence in manifest.evidence:
|
||||||
|
evidence_path = ROOT / evidence.reference
|
||||||
|
self.assertTrue(
|
||||||
|
evidence_path.is_file(),
|
||||||
|
f"Missing evidence {evidence.reference} for {manifest.package_id}",
|
||||||
|
)
|
||||||
|
if evidence.checksum is not None:
|
||||||
|
self.assertEqual(
|
||||||
|
evidence.checksum,
|
||||||
|
"sha256:" + hashlib.sha256(evidence_path.read_bytes()).hexdigest(),
|
||||||
|
f"Stale evidence checksum for {manifest.package_id}: {evidence.reference}",
|
||||||
|
)
|
||||||
|
for requirement in (
|
||||||
|
*manifest.required_modules,
|
||||||
|
*manifest.optional_modules,
|
||||||
|
):
|
||||||
|
repository = ROOT.parent / (
|
||||||
|
"govoplan-" + requirement.module_id.replace("_", "-")
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
repository.is_dir(),
|
||||||
|
f"Missing repository for {requirement.module_id}",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = dry_run_configuration_package(
|
||||||
|
manifest,
|
||||||
|
(),
|
||||||
|
ConfigurationPreflightContext(
|
||||||
|
installed_modules={
|
||||||
|
item.module_id: item.version or "workspace"
|
||||||
|
for item in manifest.required_modules
|
||||||
|
},
|
||||||
|
capabilities=frozenset(manifest.required_capabilities),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
any(item.severity == "blocker" for item in result.diagnostics),
|
||||||
|
tuple(item.to_dict() for item in result.diagnostics),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("product.governed-communication", package_ids)
|
||||||
|
self.assertIn("product.governed-data-assurance", package_ids)
|
||||||
|
self.assertIn("product.service-to-decision", package_ids)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -88,9 +88,17 @@ class DependencyBoundaryDiscoveryTests(unittest.TestCase):
|
|||||||
"govoplan-calendar",
|
"govoplan-calendar",
|
||||||
"govoplan-poll",
|
"govoplan-poll",
|
||||||
"govoplan-scheduling",
|
"govoplan-scheduling",
|
||||||
|
"govoplan-workflow-engine",
|
||||||
):
|
):
|
||||||
self.assertIn(owner, boundary.REPOS)
|
self.assertIn(owner, boundary.REPOS)
|
||||||
|
|
||||||
|
def test_transitional_allowlist_entries_have_a_removal_target(self) -> None:
|
||||||
|
boundary = load_boundary_module()
|
||||||
|
|
||||||
|
for item in boundary.ALLOWLIST:
|
||||||
|
self.assertIn("remove", item.reason.casefold())
|
||||||
|
self.assertRegex(item.reason, r"#[0-9]+")
|
||||||
|
|
||||||
def test_webui_discovery_is_fail_closed(self) -> None:
|
def test_webui_discovery_is_fail_closed(self) -> None:
|
||||||
boundary = load_boundary_module()
|
boundary = load_boundary_module()
|
||||||
with tempfile.TemporaryDirectory(prefix="govoplan-boundary-webui-") as directory:
|
with tempfile.TemporaryDirectory(prefix="govoplan-boundary-webui-") as directory:
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,207 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(META_ROOT / "tools" / "deployment"))
|
||||||
|
|
||||||
|
from govoplan_deploy.bundle import bundle_paths # noqa: E402
|
||||||
|
from govoplan_deploy.cli import main # noqa: E402
|
||||||
|
from govoplan_deploy.distribution import ( # noqa: E402
|
||||||
|
canonical_json,
|
||||||
|
canonical_signed_payload,
|
||||||
|
)
|
||||||
|
from govoplan_deploy.model import load_spec # noqa: E402
|
||||||
|
from govoplan_deploy.planning import static_checks # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class DeploymentReleaseAdoptionTests(unittest.TestCase):
|
||||||
|
def test_adopts_verified_manifest_and_makes_release_checks_pass(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-release-adopt-") as value:
|
||||||
|
root = Path(value)
|
||||||
|
self.assertEqual(
|
||||||
|
0,
|
||||||
|
main(
|
||||||
|
[
|
||||||
|
"init",
|
||||||
|
"--directory",
|
||||||
|
str(root),
|
||||||
|
"--non-interactive",
|
||||||
|
"--module-set",
|
||||||
|
"core",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
manifest, keyring = self._signed_distribution()
|
||||||
|
manifest_path = root / "source-manifest.json"
|
||||||
|
keyring_path = root / "source-keyring.json"
|
||||||
|
encoded_manifest = canonical_json(manifest)
|
||||||
|
manifest_path.write_bytes(encoded_manifest)
|
||||||
|
keyring_path.write_bytes(canonical_json(keyring))
|
||||||
|
|
||||||
|
result = main(
|
||||||
|
[
|
||||||
|
"verify-release",
|
||||||
|
"--directory",
|
||||||
|
str(root),
|
||||||
|
"--manifest",
|
||||||
|
str(manifest_path),
|
||||||
|
"--manifest-sha256",
|
||||||
|
hashlib.sha256(encoded_manifest).hexdigest(),
|
||||||
|
"--trusted-keyring",
|
||||||
|
str(keyring_path),
|
||||||
|
"--adopt",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(0, result)
|
||||||
|
paths = bundle_paths(root)
|
||||||
|
spec = load_spec(paths.spec)
|
||||||
|
self.assertEqual("1.2.3", spec.release.version)
|
||||||
|
self.assertEqual("release-1", spec.release.manifest_signature_key_id)
|
||||||
|
self.assertTrue(spec.release.api_image.endswith("a" * 64))
|
||||||
|
release_checks = {
|
||||||
|
item.id: item for item in static_checks(spec, paths)
|
||||||
|
if item.id.startswith("release.") or item.id == "modules.image_composition"
|
||||||
|
}
|
||||||
|
self.assertEqual("ok", release_checks["release.manifest"].level)
|
||||||
|
self.assertEqual(
|
||||||
|
"ok", release_checks["release.signature_verification"].level
|
||||||
|
)
|
||||||
|
self.assertEqual("ok", release_checks["modules.image_composition"].level)
|
||||||
|
|
||||||
|
def test_rejects_manifest_whose_independent_digest_does_not_match(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-release-adopt-") as value:
|
||||||
|
root = Path(value)
|
||||||
|
main(
|
||||||
|
[
|
||||||
|
"init",
|
||||||
|
"--directory",
|
||||||
|
str(root),
|
||||||
|
"--non-interactive",
|
||||||
|
"--module-set",
|
||||||
|
"core",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
manifest, keyring = self._signed_distribution()
|
||||||
|
manifest_path = root / "source-manifest.json"
|
||||||
|
keyring_path = root / "source-keyring.json"
|
||||||
|
manifest_path.write_bytes(canonical_json(manifest))
|
||||||
|
keyring_path.write_bytes(canonical_json(keyring))
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
1,
|
||||||
|
main(
|
||||||
|
[
|
||||||
|
"verify-release",
|
||||||
|
"--directory",
|
||||||
|
str(root),
|
||||||
|
"--manifest",
|
||||||
|
str(manifest_path),
|
||||||
|
"--manifest-sha256",
|
||||||
|
"0" * 64,
|
||||||
|
"--trusted-keyring",
|
||||||
|
str(keyring_path),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _signed_distribution() -> tuple[dict[str, object], dict[str, object]]:
|
||||||
|
now = datetime.now(UTC)
|
||||||
|
private = Ed25519PrivateKey.generate()
|
||||||
|
public = private.public_key().public_bytes(
|
||||||
|
serialization.Encoding.PEM,
|
||||||
|
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||||
|
).decode("ascii")
|
||||||
|
artifact = {
|
||||||
|
"url": "https://downloads.example.test/artifact.json",
|
||||||
|
"sha256": "f" * 64,
|
||||||
|
}
|
||||||
|
payload: dict[str, object] = {
|
||||||
|
"schema_version": "1",
|
||||||
|
"channel": "stable",
|
||||||
|
"sequence": 1,
|
||||||
|
"version": "1.2.3",
|
||||||
|
"issued_at": (now - timedelta(minutes=1)).isoformat(),
|
||||||
|
"expires_at": (now + timedelta(days=30)).isoformat(),
|
||||||
|
"revoked": False,
|
||||||
|
"deployer": {
|
||||||
|
"url": "https://downloads.example.test/govoplan-deploy.pyz",
|
||||||
|
"sha256": "e" * 64,
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"api": {
|
||||||
|
"index": "registry.example/govoplan/api@sha256:" + "a" * 64,
|
||||||
|
"platforms": {
|
||||||
|
"linux/amd64": "registry.example/govoplan/api@sha256:" + "1" * 64,
|
||||||
|
"linux/arm64": "registry.example/govoplan/api@sha256:" + "2" * 64,
|
||||||
|
},
|
||||||
|
"sbom": dict(artifact),
|
||||||
|
"provenance": dict(artifact),
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"index": "registry.example/govoplan/web@sha256:" + "b" * 64,
|
||||||
|
"platforms": {
|
||||||
|
"linux/amd64": "registry.example/govoplan/web@sha256:" + "3" * 64,
|
||||||
|
"linux/arm64": "registry.example/govoplan/web@sha256:" + "4" * 64,
|
||||||
|
},
|
||||||
|
"sbom": dict(artifact),
|
||||||
|
"provenance": dict(artifact),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"postgres": "docker.io/library/postgres@sha256:" + "5" * 64,
|
||||||
|
"redis": "docker.io/library/redis@sha256:" + "6" * 64,
|
||||||
|
"load_balancer": "docker.io/library/haproxy@sha256:" + "7" * 64,
|
||||||
|
},
|
||||||
|
"composition": {
|
||||||
|
"sha256": "c" * 64,
|
||||||
|
"module_ids": [],
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "govoplan-core",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"wheel_sha256": "8" * 64,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
payload["signatures"] = [
|
||||||
|
{
|
||||||
|
"key_id": "release-1",
|
||||||
|
"algorithm": "ed25519",
|
||||||
|
"value": base64.b64encode(
|
||||||
|
private.sign(canonical_signed_payload(payload))
|
||||||
|
).decode("ascii"),
|
||||||
|
}
|
||||||
|
]
|
||||||
|
keyring = {
|
||||||
|
"schema_version": "1",
|
||||||
|
"purpose": "govoplan-runtime-distribution",
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"key_id": "release-1",
|
||||||
|
"algorithm": "ed25519",
|
||||||
|
"status": "active",
|
||||||
|
"public_key_pem": public,
|
||||||
|
"not_before": (now - timedelta(days=1)).isoformat(),
|
||||||
|
"expires_at": (now + timedelta(days=365)).isoformat(),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
return payload, keyring
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import pathlib
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||||
|
SCRIPT = ROOT / "tools" / "gitea" / "gitea-sync-wiki.py"
|
||||||
|
TOOLS = SCRIPT.parent
|
||||||
|
if str(TOOLS) not in sys.path:
|
||||||
|
sys.path.insert(0, str(TOOLS))
|
||||||
|
SPEC = importlib.util.spec_from_file_location("gitea_sync_wiki", SCRIPT)
|
||||||
|
assert SPEC and SPEC.loader
|
||||||
|
wiki_sync = importlib.util.module_from_spec(SPEC)
|
||||||
|
sys.modules[SPEC.name] = wiki_sync
|
||||||
|
SPEC.loader.exec_module(wiki_sync)
|
||||||
|
|
||||||
|
|
||||||
|
class WikiSourceDiscoveryTests(unittest.TestCase):
|
||||||
|
def test_generated_and_incidental_govoplan_files_are_not_docs(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = pathlib.Path(temporary)
|
||||||
|
paths = {
|
||||||
|
"readme": root / "README.md",
|
||||||
|
"architecture": root / "docs" / "DATASOURCE_ARCHITECTURE.md",
|
||||||
|
"plan": root / "workflow-plan.md",
|
||||||
|
"audit": root / "audit-reports" / "full" / "manifest.json",
|
||||||
|
"runtime": root / "runtime" / "release" / "manifest.json",
|
||||||
|
"incidental": root / "tools" / "semgrep" / "govoplan.yml",
|
||||||
|
"manifest": root / "manifest.json",
|
||||||
|
}
|
||||||
|
for path in paths.values():
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text("content\n", encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertTrue(wiki_sync.is_repo_doc(root, paths["readme"]))
|
||||||
|
self.assertTrue(wiki_sync.is_repo_doc(root, paths["architecture"]))
|
||||||
|
self.assertTrue(wiki_sync.is_repo_doc(root, paths["plan"]))
|
||||||
|
self.assertFalse(wiki_sync.is_repo_doc(root, paths["audit"]))
|
||||||
|
self.assertFalse(wiki_sync.is_repo_doc(root, paths["runtime"]))
|
||||||
|
self.assertFalse(wiki_sync.is_repo_doc(root, paths["incidental"]))
|
||||||
|
self.assertFalse(wiki_sync.is_repo_doc(root, paths["manifest"]))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,591 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, replace
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from govoplan_core.core.institutional import (
|
||||||
|
ActorRepresentationReference,
|
||||||
|
DecisionEffectReference,
|
||||||
|
EvidenceReference,
|
||||||
|
InformationGovernanceReference,
|
||||||
|
InstitutionalReference,
|
||||||
|
LegalBasisReference,
|
||||||
|
MandateDefinition,
|
||||||
|
PartyRepresentation,
|
||||||
|
PartySubjectReference,
|
||||||
|
ProcedureParty,
|
||||||
|
ServiceBinding,
|
||||||
|
ServiceDefinition,
|
||||||
|
TemporalRevision,
|
||||||
|
service_launch_capability,
|
||||||
|
)
|
||||||
|
from govoplan_cases.backend.party_context import CasePartyContext
|
||||||
|
from govoplan_cases.backend.db.models import (
|
||||||
|
CaseAccessGrant,
|
||||||
|
CaseIdentity,
|
||||||
|
CaseRecordRevision,
|
||||||
|
CaseStatusDefinition,
|
||||||
|
CaseTimelineEntry,
|
||||||
|
CaseTypeDefinition,
|
||||||
|
)
|
||||||
|
from govoplan_cases.backend.service import (
|
||||||
|
create_case_from_intake,
|
||||||
|
get_case,
|
||||||
|
upsert_case_status,
|
||||||
|
upsert_case_type,
|
||||||
|
)
|
||||||
|
from govoplan_cases.backend.service_intake import CaseServiceIntake
|
||||||
|
from govoplan_committee.backend.db.models import (
|
||||||
|
CommitteeDecisionProjection,
|
||||||
|
CommitteeWorkspaceEvent,
|
||||||
|
CommitteeWorkspaceRevision,
|
||||||
|
)
|
||||||
|
from govoplan_committee.backend.decision_path import (
|
||||||
|
CommitteeDecisionPath,
|
||||||
|
CommitteeDecisionProposal,
|
||||||
|
)
|
||||||
|
from govoplan_committee.backend.workspace import (
|
||||||
|
CommitteeWorkspaceRecord,
|
||||||
|
SqlCommitteeWorkspace,
|
||||||
|
get_workspace_object,
|
||||||
|
record_workspace_object,
|
||||||
|
)
|
||||||
|
from govoplan_portal.backend.service_directory import PortalServiceDirectory
|
||||||
|
from govoplan_decisions.backend.db.models import FormalDecisionRevision
|
||||||
|
from govoplan_decisions.backend.service import SqlDecisionRegistry
|
||||||
|
from govoplan_mandates.backend.db.models import MandateRevision
|
||||||
|
from govoplan_mandates.backend.service import SqlMandateResolver, record_mandate
|
||||||
|
from govoplan_parties.backend.db.models import ProcedurePartyRevision
|
||||||
|
from govoplan_parties.backend.service import SqlPartyResolver, record_procedure_party
|
||||||
|
from govoplan_services.backend.db.models import ServiceDefinitionRevision
|
||||||
|
from govoplan_services.backend.service import SqlServiceDefinitionProvider, record_service_definition
|
||||||
|
|
||||||
|
|
||||||
|
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
|
||||||
|
|
||||||
|
|
||||||
|
def _reference(
|
||||||
|
kind: str,
|
||||||
|
object_id: str,
|
||||||
|
*,
|
||||||
|
owner: str,
|
||||||
|
version: str = "1",
|
||||||
|
) -> InstitutionalReference:
|
||||||
|
return InstitutionalReference(
|
||||||
|
kind=kind, # type: ignore[arg-type]
|
||||||
|
owner_module=owner,
|
||||||
|
object_id=object_id,
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
version=version,
|
||||||
|
valid_at=NOW,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _evidence(kind: str, evidence_id: str, owner: str) -> EvidenceReference:
|
||||||
|
return EvidenceReference(
|
||||||
|
kind=kind, # type: ignore[arg-type]
|
||||||
|
owner_module=owner,
|
||||||
|
evidence_id=evidence_id,
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
version="1",
|
||||||
|
checksum=f"sha256:{evidence_id}",
|
||||||
|
captured_at=NOW,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _Principal:
|
||||||
|
tenant_id: str = "tenant-1"
|
||||||
|
account_id: str = "account-1"
|
||||||
|
|
||||||
|
|
||||||
|
class _Registry:
|
||||||
|
def __init__(self, capabilities: dict[str, object]) -> None:
|
||||||
|
self.capabilities = capabilities
|
||||||
|
|
||||||
|
def has(self, module_id: str) -> bool:
|
||||||
|
return module_id in {
|
||||||
|
"portal",
|
||||||
|
"cases",
|
||||||
|
"committee",
|
||||||
|
"postbox",
|
||||||
|
"services",
|
||||||
|
"parties",
|
||||||
|
"mandates",
|
||||||
|
"decisions",
|
||||||
|
}
|
||||||
|
|
||||||
|
def has_capability(self, name: str) -> bool:
|
||||||
|
return name in self.capabilities
|
||||||
|
|
||||||
|
def capability(self, name: str) -> object:
|
||||||
|
return self.capabilities[name]
|
||||||
|
|
||||||
|
|
||||||
|
class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||||
|
def test_service_to_formal_outcome_retains_governed_context(self) -> None:
|
||||||
|
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||||
|
for table in (
|
||||||
|
ServiceDefinitionRevision.__table__,
|
||||||
|
CaseStatusDefinition.__table__,
|
||||||
|
CaseTypeDefinition.__table__,
|
||||||
|
CaseIdentity.__table__,
|
||||||
|
CaseRecordRevision.__table__,
|
||||||
|
CaseAccessGrant.__table__,
|
||||||
|
CaseTimelineEntry.__table__,
|
||||||
|
ProcedurePartyRevision.__table__,
|
||||||
|
MandateRevision.__table__,
|
||||||
|
FormalDecisionRevision.__table__,
|
||||||
|
CommitteeWorkspaceRevision.__table__,
|
||||||
|
CommitteeWorkspaceEvent.__table__,
|
||||||
|
CommitteeDecisionProjection.__table__,
|
||||||
|
):
|
||||||
|
table.create(engine)
|
||||||
|
session = Session(engine)
|
||||||
|
principal = _Principal()
|
||||||
|
self.addCleanup(engine.dispose)
|
||||||
|
self.addCleanup(session.close)
|
||||||
|
|
||||||
|
organization = _reference(
|
||||||
|
"organization_unit",
|
||||||
|
"permit-office",
|
||||||
|
owner="organizations",
|
||||||
|
)
|
||||||
|
function = _reference("function", "permit-officer", owner="organizations")
|
||||||
|
jurisdiction = _reference(
|
||||||
|
"jurisdiction",
|
||||||
|
"city-1",
|
||||||
|
owner="organizations",
|
||||||
|
)
|
||||||
|
mandate_ref = _reference(
|
||||||
|
"mandate",
|
||||||
|
"permit-mandate",
|
||||||
|
owner="mandates",
|
||||||
|
version="4",
|
||||||
|
)
|
||||||
|
legal_basis = LegalBasisReference(
|
||||||
|
kind="law",
|
||||||
|
authority="Example legislature",
|
||||||
|
reference="permit-law:3",
|
||||||
|
version="2026-01",
|
||||||
|
effective_from=NOW - timedelta(days=100),
|
||||||
|
)
|
||||||
|
service = ServiceDefinition(
|
||||||
|
reference=_reference(
|
||||||
|
"service",
|
||||||
|
"permit-service",
|
||||||
|
owner="services",
|
||||||
|
version="5",
|
||||||
|
),
|
||||||
|
key="permit.apply",
|
||||||
|
temporal=TemporalRevision(
|
||||||
|
revision="5",
|
||||||
|
valid_from=NOW - timedelta(days=1),
|
||||||
|
valid_to=NOW + timedelta(days=30),
|
||||||
|
recorded_at=NOW - timedelta(days=2),
|
||||||
|
change_reason="Service published.",
|
||||||
|
),
|
||||||
|
title="Apply for a permit",
|
||||||
|
audience=("resident",),
|
||||||
|
legal_bases=(legal_basis,),
|
||||||
|
required_evidence_types=("application", "identity"),
|
||||||
|
channels=("portal", "postbox"),
|
||||||
|
responsible_organization_ref=organization,
|
||||||
|
responsible_function_ref=function,
|
||||||
|
mandate_ref=mandate_ref,
|
||||||
|
jurisdiction_refs=(jurisdiction,),
|
||||||
|
bindings=(
|
||||||
|
ServiceBinding("case", "permit-application"),
|
||||||
|
ServiceBinding("workflow", "workflow:permit-review"),
|
||||||
|
ServiceBinding("result", "decision:permit"),
|
||||||
|
),
|
||||||
|
remedy_refs=("review:administrative-court",),
|
||||||
|
publication_state="published",
|
||||||
|
)
|
||||||
|
record_service_definition(session, principal, definition=service)
|
||||||
|
service_registry = _Registry(
|
||||||
|
{
|
||||||
|
"services.definitions": SqlServiceDefinitionProvider(),
|
||||||
|
service_launch_capability("case"): object(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
entry = PortalServiceDirectory(service_registry).list_entries(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
effective_at=NOW,
|
||||||
|
audiences=("resident",),
|
||||||
|
)[0]
|
||||||
|
self.assertTrue(entry.available)
|
||||||
|
intake = CaseServiceIntake().plan(
|
||||||
|
entry.definition,
|
||||||
|
case_id="case-1",
|
||||||
|
effective_at=NOW,
|
||||||
|
)
|
||||||
|
application_evidence = _evidence("document", "application-1", "files")
|
||||||
|
upsert_case_status(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
status_key="intake",
|
||||||
|
label="Intake",
|
||||||
|
)
|
||||||
|
upsert_case_status(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
status_key="decided",
|
||||||
|
label="Decided",
|
||||||
|
category="decided",
|
||||||
|
)
|
||||||
|
upsert_case_type(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
type_key="permit-application",
|
||||||
|
label="Permit application",
|
||||||
|
initial_status_key="intake",
|
||||||
|
allowed_status_keys=("intake", "decided"),
|
||||||
|
)
|
||||||
|
case_record = create_case_from_intake(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
plan=intake,
|
||||||
|
case_number="PERMIT-2026-0001",
|
||||||
|
title="Permit application",
|
||||||
|
status_key=None,
|
||||||
|
opened_at=NOW,
|
||||||
|
recorded_at=NOW,
|
||||||
|
change_reason="Portal application received.",
|
||||||
|
idempotency_key="journey-case-create",
|
||||||
|
evidence_refs=(application_evidence,),
|
||||||
|
deadline_at=NOW + timedelta(days=30),
|
||||||
|
)
|
||||||
|
|
||||||
|
applicant = _reference("party", "applicant", owner="parties")
|
||||||
|
representative = _reference("party", "representative", owner="parties")
|
||||||
|
address_evidence = _evidence(
|
||||||
|
"snapshot",
|
||||||
|
"address-snapshot-1",
|
||||||
|
"addresses",
|
||||||
|
)
|
||||||
|
representative_party = ProcedureParty(
|
||||||
|
reference=representative,
|
||||||
|
procedure_ref=case_record.reference,
|
||||||
|
role="representative",
|
||||||
|
subject=PartySubjectReference(
|
||||||
|
kind="identity",
|
||||||
|
provider="identity",
|
||||||
|
subject_id="identity-2",
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
version="2",
|
||||||
|
),
|
||||||
|
temporal=TemporalRevision(
|
||||||
|
revision="1",
|
||||||
|
valid_from=NOW - timedelta(days=1),
|
||||||
|
recorded_at=NOW - timedelta(days=1),
|
||||||
|
change_reason="Representative added to the procedure.",
|
||||||
|
),
|
||||||
|
permitted_channels=("postbox",),
|
||||||
|
preferred_channels=("postbox",),
|
||||||
|
delivery_recipient=True,
|
||||||
|
representations=(
|
||||||
|
PartyRepresentation(
|
||||||
|
representative_party_ref=representative,
|
||||||
|
represented_party_ref=applicant,
|
||||||
|
power_ref="power-1",
|
||||||
|
permitted_actions=("submit", "receive"),
|
||||||
|
temporal=TemporalRevision(
|
||||||
|
revision="1",
|
||||||
|
valid_from=NOW - timedelta(days=1),
|
||||||
|
recorded_at=NOW - timedelta(days=1),
|
||||||
|
change_reason="Representation power recorded.",
|
||||||
|
),
|
||||||
|
evidence=(address_evidence,),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
contact_snapshot_refs=("addresses:snapshot-1",),
|
||||||
|
evidence=(address_evidence,),
|
||||||
|
)
|
||||||
|
record_procedure_party(session, principal, party=representative_party)
|
||||||
|
party_context = CasePartyContext(
|
||||||
|
_Registry({"parties.resolver": SqlPartyResolver()})
|
||||||
|
)
|
||||||
|
parties = party_context.resolve(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
case_ref=case_record.reference,
|
||||||
|
effective_at=NOW,
|
||||||
|
)
|
||||||
|
delivery_target = party_context.delivery_targets(
|
||||||
|
parties,
|
||||||
|
channel="postbox",
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
mandate = MandateDefinition(
|
||||||
|
reference=mandate_ref,
|
||||||
|
temporal=TemporalRevision(
|
||||||
|
revision="4",
|
||||||
|
valid_from=NOW - timedelta(days=30),
|
||||||
|
valid_to=NOW + timedelta(days=30),
|
||||||
|
recorded_at=NOW - timedelta(days=31),
|
||||||
|
change_reason="Permit authority delegated.",
|
||||||
|
),
|
||||||
|
task_types=("committee.formal_decision",),
|
||||||
|
authority_types=("permit",),
|
||||||
|
organization_unit_refs=(organization,),
|
||||||
|
function_refs=(function,),
|
||||||
|
jurisdiction_refs=(jurisdiction,),
|
||||||
|
legal_bases=(legal_basis,),
|
||||||
|
evidence=(_evidence("record", "mandate-record-4", "mandates"),),
|
||||||
|
authority_ceiling="permit:standard",
|
||||||
|
)
|
||||||
|
record_mandate(session, principal, definition=mandate)
|
||||||
|
workspace = SqlCommitteeWorkspace()
|
||||||
|
body = CommitteeWorkspaceRecord(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
object_kind="body",
|
||||||
|
object_id="permit-board",
|
||||||
|
revision=1,
|
||||||
|
state="active",
|
||||||
|
title="Permit board",
|
||||||
|
recorded_at=NOW,
|
||||||
|
change_reason="Permit board configured.",
|
||||||
|
attributes={
|
||||||
|
"organization_unit_ref": organization.to_dict(),
|
||||||
|
"function_refs": [function.to_dict()],
|
||||||
|
"quorum": {"minimum_count": 1},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
meeting = CommitteeWorkspaceRecord(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
object_kind="meeting",
|
||||||
|
object_id="meeting-1",
|
||||||
|
revision=1,
|
||||||
|
state="open",
|
||||||
|
title="Permit board meeting",
|
||||||
|
parent_id=body.object_id,
|
||||||
|
recorded_at=NOW,
|
||||||
|
change_reason="Meeting opened.",
|
||||||
|
attributes={
|
||||||
|
"starts_at": NOW.isoformat(),
|
||||||
|
"ends_at": (NOW + timedelta(hours=1)).isoformat(),
|
||||||
|
},
|
||||||
|
context=case_record.context,
|
||||||
|
)
|
||||||
|
agenda = CommitteeWorkspaceRecord(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
object_kind="agenda_item",
|
||||||
|
object_id="item-1",
|
||||||
|
revision=1,
|
||||||
|
state="deliberating",
|
||||||
|
title="Permit application",
|
||||||
|
parent_id=meeting.object_id,
|
||||||
|
recorded_at=NOW,
|
||||||
|
change_reason="Agenda item entered deliberation.",
|
||||||
|
attributes={
|
||||||
|
"position": 1,
|
||||||
|
"subject_refs": [case_record.reference.to_dict()],
|
||||||
|
},
|
||||||
|
context=case_record.context,
|
||||||
|
evidence=(application_evidence,),
|
||||||
|
)
|
||||||
|
approval_ref = _reference("approval", "approval-1", owner="approvals")
|
||||||
|
vote = CommitteeWorkspaceRecord(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
object_kind="vote",
|
||||||
|
object_id="vote-1",
|
||||||
|
revision=1,
|
||||||
|
state="closed",
|
||||||
|
title="Vote on permit application",
|
||||||
|
parent_id=agenda.object_id,
|
||||||
|
recorded_at=NOW,
|
||||||
|
change_reason="Vote result accepted.",
|
||||||
|
attributes={
|
||||||
|
"method": "recorded",
|
||||||
|
"choices": ["yes", "no"],
|
||||||
|
"eligible_count": 3,
|
||||||
|
"cast_count": 3,
|
||||||
|
"counts": {"yes": 3, "no": 0},
|
||||||
|
"quorum_met": True,
|
||||||
|
"approval_ref": approval_ref.to_dict(),
|
||||||
|
},
|
||||||
|
context=case_record.context,
|
||||||
|
evidence=(_evidence("record", "vote-result-1", "committee"),),
|
||||||
|
)
|
||||||
|
for record, key in (
|
||||||
|
(body, "journey-body"),
|
||||||
|
(meeting, "journey-meeting"),
|
||||||
|
(agenda, "journey-agenda"),
|
||||||
|
(vote, "journey-vote"),
|
||||||
|
):
|
||||||
|
record_workspace_object(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
record=record,
|
||||||
|
idempotency_key=key,
|
||||||
|
)
|
||||||
|
decision_registry = SqlDecisionRegistry()
|
||||||
|
decision_result = CommitteeDecisionPath(
|
||||||
|
_Registry(
|
||||||
|
{
|
||||||
|
"mandates.resolver": SqlMandateResolver(),
|
||||||
|
"decisions.registry": decision_registry,
|
||||||
|
"committee.workspace": workspace,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
).decide(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
proposal=CommitteeDecisionProposal(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
decision_id="decision-1",
|
||||||
|
revision="1",
|
||||||
|
effective_at=NOW,
|
||||||
|
meeting_ref="meeting-1",
|
||||||
|
agenda_item_ref="item-1",
|
||||||
|
decision_type="permit",
|
||||||
|
subject_refs=(case_record.reference,),
|
||||||
|
organization_unit_ref=organization,
|
||||||
|
function_ref=function,
|
||||||
|
actor=ActorRepresentationReference(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
account_id="account-1",
|
||||||
|
identity_id="identity-1",
|
||||||
|
represented_function_ref=function,
|
||||||
|
mandate_ref=mandate_ref,
|
||||||
|
),
|
||||||
|
approval_refs=(
|
||||||
|
approval_ref,
|
||||||
|
),
|
||||||
|
fact_evidence=(application_evidence, address_evidence),
|
||||||
|
legal_bases=(legal_basis,),
|
||||||
|
operative_result="Permit granted.",
|
||||||
|
reasoning="The application satisfies the effective rule.",
|
||||||
|
case_ref=case_record.reference,
|
||||||
|
jurisdiction_refs=(jurisdiction,),
|
||||||
|
party_refs=(applicant, representative),
|
||||||
|
record_refs=(
|
||||||
|
_reference("record", "record-1", owner="audit"),
|
||||||
|
),
|
||||||
|
remedy_refs=service.remedy_refs,
|
||||||
|
review_refs=("review:administrative-court",),
|
||||||
|
information_governance=InformationGovernanceReference(
|
||||||
|
classification="restricted",
|
||||||
|
purposes=("permit-decision", "party-delivery"),
|
||||||
|
legal_basis_refs=("permit-law:3@2026-01",),
|
||||||
|
retention_policy_ref="records:permit",
|
||||||
|
disclosure_state="partly_disclosable",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
observed_effects=(
|
||||||
|
DecisionEffectReference(
|
||||||
|
effect_key="postbox.deliver_decision",
|
||||||
|
state="confirmed",
|
||||||
|
resource_refs=(
|
||||||
|
f"postbox:{delivery_target.party_ref.object_id}",
|
||||||
|
),
|
||||||
|
audit_event_refs=("audit:delivery-1",),
|
||||||
|
evidence_refs=(address_evidence.evidence_id,),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
decided_agenda = replace(
|
||||||
|
agenda,
|
||||||
|
revision=2,
|
||||||
|
state="decided",
|
||||||
|
recorded_at=NOW + timedelta(minutes=1),
|
||||||
|
change_reason="Formal Decision recorded.",
|
||||||
|
attributes={
|
||||||
|
**dict(agenda.attributes),
|
||||||
|
"decision_ref": decision_result.decision.reference.to_dict(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
record_workspace_object(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
record=decided_agenda,
|
||||||
|
expected_revision=1,
|
||||||
|
idempotency_key="journey-agenda-decided",
|
||||||
|
)
|
||||||
|
closed_meeting = replace(
|
||||||
|
meeting,
|
||||||
|
revision=2,
|
||||||
|
state="closed",
|
||||||
|
recorded_at=NOW + timedelta(hours=1),
|
||||||
|
change_reason="All agenda items completed.",
|
||||||
|
)
|
||||||
|
record_workspace_object(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
record=closed_meeting,
|
||||||
|
expected_revision=1,
|
||||||
|
idempotency_key="journey-meeting-closed",
|
||||||
|
)
|
||||||
|
minute = CommitteeWorkspaceRecord(
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
object_kind="minute",
|
||||||
|
object_id="minute-1",
|
||||||
|
revision=1,
|
||||||
|
state="accepted",
|
||||||
|
title="Accepted permit board minutes",
|
||||||
|
parent_id=meeting.object_id,
|
||||||
|
recorded_at=NOW + timedelta(hours=2),
|
||||||
|
change_reason="Minutes approved.",
|
||||||
|
attributes={
|
||||||
|
"content_ref": _reference(
|
||||||
|
"record",
|
||||||
|
"meeting-minutes-1",
|
||||||
|
owner="records",
|
||||||
|
).to_dict(),
|
||||||
|
"approval_ref": _reference(
|
||||||
|
"approval",
|
||||||
|
"minutes-approval-1",
|
||||||
|
owner="approvals",
|
||||||
|
).to_dict(),
|
||||||
|
},
|
||||||
|
context=case_record.context,
|
||||||
|
evidence=(_evidence("record", "minutes-proof-1", "records"),),
|
||||||
|
)
|
||||||
|
record_workspace_object(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
record=minute,
|
||||||
|
idempotency_key="journey-minute",
|
||||||
|
)
|
||||||
|
reconstruction = decision_result.reconstruction_payload()["decision"]
|
||||||
|
persisted = decision_registry.get_decision(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
reference=decision_result.decision.reference,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(decision_result.persisted_by_decision_registry)
|
||||||
|
self.assertEqual(decision_result.decision, persisted)
|
||||||
|
self.assertEqual(
|
||||||
|
case_record,
|
||||||
|
get_case(session, principal, case_id="case-1"),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"decided",
|
||||||
|
get_workspace_object(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
object_kind="agenda_item",
|
||||||
|
object_id="item-1",
|
||||||
|
).state,
|
||||||
|
)
|
||||||
|
self.assertEqual(service.reference, intake.context.service_ref)
|
||||||
|
self.assertEqual("applicant", delivery_target.represented_party_refs[0].object_id)
|
||||||
|
self.assertEqual("permit-mandate", reconstruction["authority_context"]["mandate_ref"]["object_id"])
|
||||||
|
self.assertEqual("city-1", reconstruction["authority_context"]["jurisdiction_refs"][0]["object_id"])
|
||||||
|
self.assertEqual("confirmed", reconstruction["observed_effects"][0]["state"])
|
||||||
|
self.assertEqual("audit:delivery-1", reconstruction["observed_effects"][0]["audit_event_refs"][0])
|
||||||
|
self.assertEqual("application-1", reconstruction["fact_evidence"][0]["evidence_id"])
|
||||||
|
self.assertEqual("The application satisfies the effective rule.", reconstruction["reasoning"])
|
||||||
|
self.assertEqual("review:administrative-court", reconstruction["review_refs"][0])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from govoplan_core.core.institutional import (
|
||||||
|
CAPABILITY_FORM_DEFINITIONS,
|
||||||
|
CAPABILITY_SERVICE_DEFINITIONS,
|
||||||
|
FormDefinition,
|
||||||
|
FormFieldDefinition,
|
||||||
|
InstitutionalReference,
|
||||||
|
ServiceBinding,
|
||||||
|
ServiceDefinition,
|
||||||
|
TemporalRevision,
|
||||||
|
service_launch_capability,
|
||||||
|
)
|
||||||
|
from govoplan_cases.backend.service_intake import (
|
||||||
|
CAPABILITY_CASES_SERVICE_INTAKE,
|
||||||
|
CaseServiceIntake,
|
||||||
|
)
|
||||||
|
from govoplan_forms.backend.db.models import FormDefinitionRevision
|
||||||
|
from govoplan_forms.backend.service import (
|
||||||
|
SqlFormDefinitionProvider,
|
||||||
|
record_form_definition,
|
||||||
|
)
|
||||||
|
from govoplan_forms_runtime.backend.db.models import (
|
||||||
|
FormInstanceEvent,
|
||||||
|
FormInstanceIdentity,
|
||||||
|
FormInstanceRevision,
|
||||||
|
)
|
||||||
|
from govoplan_forms_runtime.backend.service import (
|
||||||
|
FormRuntimeService,
|
||||||
|
FormsServiceLauncher,
|
||||||
|
)
|
||||||
|
from govoplan_portal.backend.service_directory import PortalServiceDirectory
|
||||||
|
|
||||||
|
|
||||||
|
NOW = datetime(2026, 8, 1, 10, 0, tzinfo=UTC)
|
||||||
|
|
||||||
|
|
||||||
|
def _service() -> ServiceDefinition:
|
||||||
|
return ServiceDefinition(
|
||||||
|
reference=InstitutionalReference(
|
||||||
|
kind="service",
|
||||||
|
owner_module="portal",
|
||||||
|
object_id="permit",
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
version="5",
|
||||||
|
),
|
||||||
|
key="permit.apply",
|
||||||
|
temporal=TemporalRevision(
|
||||||
|
revision="5",
|
||||||
|
valid_from=NOW - timedelta(days=1),
|
||||||
|
valid_to=NOW + timedelta(days=1),
|
||||||
|
recorded_at=NOW - timedelta(days=2),
|
||||||
|
),
|
||||||
|
title="Apply for a permit",
|
||||||
|
audience=("resident",),
|
||||||
|
required_evidence_types=("application",),
|
||||||
|
bindings=(
|
||||||
|
ServiceBinding("capability", CAPABILITY_CASES_SERVICE_INTAKE),
|
||||||
|
ServiceBinding("case", "permit-application"),
|
||||||
|
ServiceBinding("workflow", "workflow:permit-review"),
|
||||||
|
),
|
||||||
|
publication_state="published",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _Provider:
|
||||||
|
def __init__(self, definition: ServiceDefinition) -> None:
|
||||||
|
self.definition = definition
|
||||||
|
|
||||||
|
def get_service_definition(self, session, principal, *, reference, effective_at=None):
|
||||||
|
return self.definition
|
||||||
|
|
||||||
|
def list_service_definitions(self, session, principal, *, tenant_id, query="", limit=100):
|
||||||
|
return (self.definition,)
|
||||||
|
|
||||||
|
|
||||||
|
class _Registry:
|
||||||
|
def __init__(self, definition: ServiceDefinition) -> None:
|
||||||
|
self.capabilities = {
|
||||||
|
CAPABILITY_SERVICE_DEFINITIONS: _Provider(definition),
|
||||||
|
CAPABILITY_CASES_SERVICE_INTAKE: CaseServiceIntake(),
|
||||||
|
service_launch_capability("case"): object(),
|
||||||
|
}
|
||||||
|
|
||||||
|
def has(self, module_id: str) -> bool:
|
||||||
|
return module_id in {"portal", "cases"}
|
||||||
|
|
||||||
|
def has_capability(self, name: str) -> bool:
|
||||||
|
return name in self.capabilities
|
||||||
|
|
||||||
|
def capability(self, name: str) -> object:
|
||||||
|
return self.capabilities[name]
|
||||||
|
|
||||||
|
def require_capability(self, name: str) -> object:
|
||||||
|
return self.capabilities[name]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _Principal:
|
||||||
|
tenant_id: str = "tenant-1"
|
||||||
|
account_id: str = "account-1"
|
||||||
|
|
||||||
|
|
||||||
|
class _FormRegistry(_Registry):
|
||||||
|
def __init__(self, definition: ServiceDefinition) -> None:
|
||||||
|
super().__init__(definition)
|
||||||
|
self.capabilities[CAPABILITY_FORM_DEFINITIONS] = (
|
||||||
|
SqlFormDefinitionProvider()
|
||||||
|
)
|
||||||
|
self.capabilities[service_launch_capability("form")] = (
|
||||||
|
FormsServiceLauncher(self)
|
||||||
|
)
|
||||||
|
|
||||||
|
def has(self, module_id: str) -> bool:
|
||||||
|
return module_id in {"portal", "forms", "forms_runtime"}
|
||||||
|
|
||||||
|
|
||||||
|
class InstitutionalServiceJourneyTests(unittest.TestCase):
|
||||||
|
def test_one_service_version_drives_portal_and_case_intake(self) -> None:
|
||||||
|
definition = _service()
|
||||||
|
registry = _Registry(definition)
|
||||||
|
|
||||||
|
entries = PortalServiceDirectory(registry).list_entries(
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
effective_at=NOW,
|
||||||
|
audiences=("resident",),
|
||||||
|
)
|
||||||
|
plan = registry.capability(CAPABILITY_CASES_SERVICE_INTAKE).plan(
|
||||||
|
entries[0].definition,
|
||||||
|
case_id="case-1",
|
||||||
|
effective_at=NOW,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(entries[0].available)
|
||||||
|
self.assertIs(definition, entries[0].definition)
|
||||||
|
self.assertEqual(definition.reference, plan.service_ref)
|
||||||
|
self.assertEqual("5", plan.context.service_ref.version)
|
||||||
|
self.assertEqual("workflow:permit-review", plan.workflow_refs[0])
|
||||||
|
|
||||||
|
def test_portal_launches_exact_form_revision_and_persists_submission(self) -> None:
|
||||||
|
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||||
|
for table in (
|
||||||
|
FormDefinitionRevision.__table__,
|
||||||
|
FormInstanceIdentity.__table__,
|
||||||
|
FormInstanceRevision.__table__,
|
||||||
|
FormInstanceEvent.__table__,
|
||||||
|
):
|
||||||
|
table.create(engine)
|
||||||
|
session = Session(engine)
|
||||||
|
principal = _Principal()
|
||||||
|
try:
|
||||||
|
form = record_form_definition(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
definition=FormDefinition(
|
||||||
|
reference=InstitutionalReference(
|
||||||
|
kind="form",
|
||||||
|
owner_module="forms",
|
||||||
|
object_id="permit-application",
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
version="3",
|
||||||
|
),
|
||||||
|
key="permit-application",
|
||||||
|
temporal=TemporalRevision(
|
||||||
|
revision="3",
|
||||||
|
valid_from=NOW - timedelta(days=1),
|
||||||
|
valid_to=NOW + timedelta(days=1),
|
||||||
|
recorded_at=NOW - timedelta(days=2),
|
||||||
|
change_reason="Publish the permit application.",
|
||||||
|
),
|
||||||
|
title="Permit application",
|
||||||
|
fields=(
|
||||||
|
FormFieldDefinition(
|
||||||
|
key="applicant_name",
|
||||||
|
label="Applicant name",
|
||||||
|
required=True,
|
||||||
|
constraints={"min_length": 2},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
publication_state="published",
|
||||||
|
allow_drafts=True,
|
||||||
|
handoff_kinds=("case",),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
binding = ServiceBinding(
|
||||||
|
"form",
|
||||||
|
f"{form.reference.object_id}/{form.reference.version}",
|
||||||
|
)
|
||||||
|
service = ServiceDefinition(
|
||||||
|
reference=InstitutionalReference(
|
||||||
|
kind="service",
|
||||||
|
owner_module="services",
|
||||||
|
object_id="permit",
|
||||||
|
tenant_id="tenant-1",
|
||||||
|
version="6",
|
||||||
|
),
|
||||||
|
key="permit.apply",
|
||||||
|
temporal=TemporalRevision(
|
||||||
|
revision="6",
|
||||||
|
valid_from=NOW - timedelta(days=1),
|
||||||
|
valid_to=NOW + timedelta(days=1),
|
||||||
|
recorded_at=NOW - timedelta(days=2),
|
||||||
|
),
|
||||||
|
title="Apply for a permit",
|
||||||
|
audience=("public",),
|
||||||
|
bindings=(binding,),
|
||||||
|
publication_state="published",
|
||||||
|
)
|
||||||
|
registry = _FormRegistry(service)
|
||||||
|
directory = PortalServiceDirectory(registry)
|
||||||
|
|
||||||
|
launched = directory.launch_service(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
reference=service.reference,
|
||||||
|
requested_at=NOW,
|
||||||
|
idempotency_key="portal-form-launch-1",
|
||||||
|
parameters={"applicant_name": "Ada Lovelace"},
|
||||||
|
)
|
||||||
|
replay = directory.launch_service(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
reference=service.reference,
|
||||||
|
requested_at=NOW,
|
||||||
|
idempotency_key="portal-form-launch-1",
|
||||||
|
parameters={"applicant_name": "Ada Lovelace"},
|
||||||
|
)
|
||||||
|
instance = FormRuntimeService(registry).get_instance(
|
||||||
|
session,
|
||||||
|
principal,
|
||||||
|
instance_id=str(launched.metadata["form_instance_id"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("form_submission", launched.target_ref.kind)
|
||||||
|
self.assertEqual(service.reference, launched.service_ref)
|
||||||
|
self.assertEqual("3", launched.metadata["form_definition_revision"])
|
||||||
|
self.assertTrue(replay.replayed)
|
||||||
|
self.assertEqual(launched.target_ref, replay.target_ref)
|
||||||
|
self.assertEqual(
|
||||||
|
(
|
||||||
|
form.reference.owner_module,
|
||||||
|
form.reference.object_id,
|
||||||
|
form.reference.tenant_id,
|
||||||
|
form.reference.version,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
instance.definition_ref.owner_module,
|
||||||
|
instance.definition_ref.object_id,
|
||||||
|
instance.definition_ref.tenant_id,
|
||||||
|
instance.definition_ref.version,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(NOW, instance.definition_ref.valid_at)
|
||||||
|
self.assertEqual(service.reference, instance.service_ref)
|
||||||
|
self.assertEqual("Ada Lovelace", instance.values["applicant_name"])
|
||||||
|
finally:
|
||||||
|
session.close()
|
||||||
|
engine.dispose()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _load_module():
|
||||||
|
path = ROOT / "tools/checks/managed-ingress-drill.py"
|
||||||
|
spec = importlib.util.spec_from_file_location("managed_ingress_drill", path)
|
||||||
|
assert spec is not None and spec.loader is not None
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[spec.name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
INGRESS = _load_module()
|
||||||
|
|
||||||
|
|
||||||
|
class ManagedIngressDrillTests(unittest.TestCase):
|
||||||
|
def test_config_is_streamed_into_a_daemon_visible_volume(self) -> None:
|
||||||
|
completed = subprocess.CompletedProcess([], 0, "", "")
|
||||||
|
with patch.object(INGRESS, "_run", return_value=completed) as run:
|
||||||
|
INGRESS._write_volume_file(
|
||||||
|
image="registry.example/caddy@sha256:" + "1" * 64,
|
||||||
|
volume="config-volume",
|
||||||
|
filename="Caddyfile",
|
||||||
|
content=":8080 { respond /health 200 }\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
argv = run.call_args.args[0]
|
||||||
|
self.assertIn("type=volume,src=config-volume,dst=/govoplan-config", argv)
|
||||||
|
self.assertIn("0:0", argv)
|
||||||
|
self.assertNotIn("type=bind", " ".join(argv))
|
||||||
|
self.assertEqual(
|
||||||
|
":8080 { respond /health 200 }\n",
|
||||||
|
run.call_args.kwargs["input_text"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_config_filename_cannot_escape_the_volume(self) -> None:
|
||||||
|
with self.assertRaisesRegex(ValueError, "invalid config filename"):
|
||||||
|
INGRESS._write_volume_file(
|
||||||
|
image="registry.example/caddy@sha256:" + "1" * 64,
|
||||||
|
volume="config-volume",
|
||||||
|
filename="../Caddyfile",
|
||||||
|
content="",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_drill_has_no_runner_local_bind_mounts(self) -> None:
|
||||||
|
source = (ROOT / "tools/checks/managed-ingress-drill.py").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotIn("type=bind", source)
|
||||||
|
self.assertIn('"--network-alias",\n "load-balancer"', source)
|
||||||
|
self.assertNotIn('"127.0.0.1::8080"', source)
|
||||||
|
self.assertIn("requested_http_port", source)
|
||||||
|
self.assertIn("requested_https_port", source)
|
||||||
|
self.assertIn('"--cap-add",\n "NET_BIND_SERVICE"', source)
|
||||||
|
|
||||||
|
def test_published_port_reads_the_docker_mapping(self) -> None:
|
||||||
|
completed = subprocess.CompletedProcess(
|
||||||
|
[],
|
||||||
|
0,
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"8443/tcp": [
|
||||||
|
{"HostIp": "127.0.0.1", "HostPort": "49152"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
with patch.object(INGRESS, "_run", return_value=completed) as run:
|
||||||
|
port = INGRESS._published_port("ingress", 8443)
|
||||||
|
|
||||||
|
self.assertEqual(49152, port)
|
||||||
|
self.assertEqual(
|
||||||
|
[
|
||||||
|
"docker",
|
||||||
|
"inspect",
|
||||||
|
"--format",
|
||||||
|
"{{json .HostConfig.PortBindings}}",
|
||||||
|
"ingress",
|
||||||
|
],
|
||||||
|
run.call_args.args[0],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_published_port_rejects_non_loopback_binding(self) -> None:
|
||||||
|
completed = subprocess.CompletedProcess(
|
||||||
|
[],
|
||||||
|
0,
|
||||||
|
'{"8443/tcp":[{"HostIp":"0.0.0.0","HostPort":"49152"}]}',
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
with patch.object(INGRESS, "_run", return_value=completed):
|
||||||
|
with self.assertRaisesRegex(RuntimeError, "loopback binding"):
|
||||||
|
INGRESS._published_port("ingress", 8443)
|
||||||
|
|
||||||
|
def test_probe_runs_as_a_network_sibling_from_a_digest_image(self) -> None:
|
||||||
|
completed = subprocess.CompletedProcess([], 0, "", "")
|
||||||
|
image = "registry.example/runtime-api@sha256:" + "1" * 64
|
||||||
|
with patch.object(INGRESS, "_run", return_value=completed) as run:
|
||||||
|
INGRESS._probe_ingress(
|
||||||
|
image=image,
|
||||||
|
network="deployment-network",
|
||||||
|
container="ingress",
|
||||||
|
)
|
||||||
|
|
||||||
|
argv = run.call_args.args[0]
|
||||||
|
self.assertEqual("docker", argv[0])
|
||||||
|
self.assertIn("deployment-network", argv)
|
||||||
|
self.assertIn(image, argv)
|
||||||
|
self.assertIn('(\"ingress\", port)', argv[-1])
|
||||||
|
self.assertIn("server_hostname=\"localhost\"", argv[-1])
|
||||||
|
self.assertNotIn("localhost:49152", argv[-1])
|
||||||
|
|
||||||
|
def test_probe_diagnostics_include_container_stderr(self) -> None:
|
||||||
|
probe_failure = subprocess.CalledProcessError(1, ["docker", "run"])
|
||||||
|
state = subprocess.CompletedProcess([], 0, '{"Running":false}', "")
|
||||||
|
logs = subprocess.CompletedProcess([], 0, "", "caddy startup failed")
|
||||||
|
with patch.object(
|
||||||
|
INGRESS,
|
||||||
|
"_run",
|
||||||
|
side_effect=[probe_failure, state, logs],
|
||||||
|
), patch.object(INGRESS.sys, "stderr") as stderr:
|
||||||
|
with self.assertRaises(subprocess.CalledProcessError):
|
||||||
|
INGRESS._probe_ingress(
|
||||||
|
image="registry.example/runtime-api@sha256:" + "1" * 64,
|
||||||
|
network="deployment-network",
|
||||||
|
container="ingress",
|
||||||
|
)
|
||||||
|
|
||||||
|
rendered = "".join(call.args[0] for call in stderr.write.call_args_list)
|
||||||
|
self.assertIn('"Running":false', rendered)
|
||||||
|
self.assertIn("caddy startup failed", rendered)
|
||||||
|
|
||||||
|
def test_standalone_workflow_is_dispatch_only_and_digest_bounded(self) -> None:
|
||||||
|
workflow = (
|
||||||
|
ROOT / ".gitea/workflows/runtime-ingress-drill.yml"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertIn("workflow_dispatch:", workflow)
|
||||||
|
self.assertNotIn("\n push:", workflow)
|
||||||
|
self.assertIn("--probe-image \"$PROBE_IMAGE\"", workflow)
|
||||||
|
self.assertIn("GOVOPLAN_REGISTRY_TOKEN", workflow)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
class ModulePackageWorkflowTests(unittest.TestCase):
|
||||||
|
def test_template_enforces_tag_version_hash_and_registry_contract(self) -> None:
|
||||||
|
workflow = (
|
||||||
|
META_ROOT / "tools/repo/templates/module-package-release.yml"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertIn("GITEA_REPOSITORY: ${{ gitea.repository }}", workflow)
|
||||||
|
self.assertNotIn("tag_protections", workflow)
|
||||||
|
self.assertNotIn("secrets.GITEA_TOKEN", workflow)
|
||||||
|
self.assertIn("git merge-base --is-ancestor", workflow)
|
||||||
|
self.assertIn("does not match", workflow)
|
||||||
|
self.assertIn("package-artifacts.json", workflow)
|
||||||
|
self.assertIn("api/packages/GovOPlaN/pypi", workflow)
|
||||||
|
self.assertIn("api/packages/GovOPlaN/npm", workflow)
|
||||||
|
self.assertIn('npm publish "./${webui_packages[0]}"', workflow)
|
||||||
|
self.assertIn("Check immutable registry state", workflow)
|
||||||
|
self.assertIn('files[0].get("sha256") != expected_sha256', workflow)
|
||||||
|
self.assertIn('if [[ "$PUBLISH_PYPI" == 1 ]]', workflow)
|
||||||
|
self.assertIn('[[ "$PUBLISH_NPM" == 1 ]]', workflow)
|
||||||
|
self.assertIn("GOVOPLAN_PACKAGE_TOKEN", workflow)
|
||||||
|
self.assertIn("must resolve to an exact registry version", workflow)
|
||||||
|
self.assertIn("git\\\\.add-ideas\\\\.de/(?:GovOPlaN|add-ideas)", workflow)
|
||||||
|
self.assertIn("release package identity does not match", workflow)
|
||||||
|
self.assertNotIn("Generic", workflow)
|
||||||
|
|
||||||
|
@unittest.skipUnless(shutil.which("node"), "Node.js is required")
|
||||||
|
def test_webui_publication_normalizes_internal_git_dependencies(self) -> None:
|
||||||
|
workflow = (
|
||||||
|
META_ROOT / "tools/repo/templates/module-package-release.yml"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
marker = " node <<'NODE'\n"
|
||||||
|
script = workflow.split(marker, 1)[1].split("\n NODE", 1)[0]
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
root = Path(temporary)
|
||||||
|
package_dir = root / ".package-webui"
|
||||||
|
package_dir.mkdir()
|
||||||
|
package_path = package_dir / "package.json"
|
||||||
|
package_path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"name": "@govoplan/core-webui",
|
||||||
|
"version": "0.1.14",
|
||||||
|
"private": True,
|
||||||
|
"dependencies": {
|
||||||
|
"@govoplan/access-webui": (
|
||||||
|
"git+ssh://git@git.add-ideas.de/GovOPlaN/"
|
||||||
|
"govoplan-access.git#v0.1.11"
|
||||||
|
),
|
||||||
|
"@govoplan/admin-webui": (
|
||||||
|
"git+ssh://git@git.add-ideas.de/add-ideas/"
|
||||||
|
"govoplan-admin.git#v0.1.8"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
["node"],
|
||||||
|
input=script,
|
||||||
|
cwd=root,
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
package = json.loads(package_path.read_text(encoding="utf-8"))
|
||||||
|
self.assertNotIn("private", package)
|
||||||
|
self.assertEqual(
|
||||||
|
"0.1.11", package["dependencies"]["@govoplan/access-webui"]
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"0.1.8", package["dependencies"]["@govoplan/admin-webui"]
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_sync_script_only_targets_packageable_govoplan_repositories(self) -> None:
|
||||||
|
namespace: dict[str, object] = {
|
||||||
|
"__file__": str(META_ROOT / "tools/repo/sync-module-package-workflows.py"),
|
||||||
|
"__name__": "test_sync_module_package_workflows",
|
||||||
|
}
|
||||||
|
script = (META_ROOT / "tools/repo/sync-module-package-workflows.py").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
exec(compile(script, str(namespace["__file__"]), "exec"), namespace)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temporary:
|
||||||
|
parent = Path(temporary)
|
||||||
|
package_repositories = namespace["package_repositories"]
|
||||||
|
# The production inventory is authoritative, so a temporary parent
|
||||||
|
# only exposes matching paths that are present in that inventory.
|
||||||
|
known = parent / "govoplan-core"
|
||||||
|
known.mkdir()
|
||||||
|
(known / "pyproject.toml").write_text("[project]\n", encoding="utf-8")
|
||||||
|
self.assertEqual(package_repositories(parent), (known,))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from io import BytesIO
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import tomllib
|
||||||
|
import unittest
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _load(name: str, path: Path):
|
||||||
|
spec = importlib.util.spec_from_file_location(name, path)
|
||||||
|
assert spec is not None and spec.loader is not None
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
PACKAGE_SET = _load(
|
||||||
|
"generate_release_package_set",
|
||||||
|
ROOT / "tools/release/generate-release-package-set.py",
|
||||||
|
)
|
||||||
|
ARTIFACTS = _load(
|
||||||
|
"resolve_package_artifacts",
|
||||||
|
ROOT / "tools/release/resolve-package-artifacts.py",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PackageRegistryReleaseTests(unittest.TestCase):
|
||||||
|
def test_current_release_sources_form_a_hash_bound_package_set(self) -> None:
|
||||||
|
core_version = tomllib.loads(
|
||||||
|
(ROOT.parent / "govoplan-core/pyproject.toml").read_text(encoding="utf-8")
|
||||||
|
)["project"]["version"]
|
||||||
|
payload = PACKAGE_SET.generate_package_set(
|
||||||
|
core_version=core_version,
|
||||||
|
requirements=ROOT / "requirements-release.txt",
|
||||||
|
workspace=ROOT.parent,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("1", payload["schema_version"])
|
||||||
|
self.assertEqual("govoplan-core", payload["python"][0]["name"])
|
||||||
|
self.assertIn(
|
||||||
|
"@govoplan/core-webui",
|
||||||
|
{item["name"] for item in payload["webui"]},
|
||||||
|
)
|
||||||
|
unsigned = dict(payload)
|
||||||
|
digest = unsigned.pop("package_set_sha256")
|
||||||
|
self.assertEqual(ARTIFACTS._canonical_sha256(unsigned), digest)
|
||||||
|
|
||||||
|
def test_wheel_and_webui_artifacts_are_verified_by_embedded_identity(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-package-artifacts-") as value:
|
||||||
|
root = Path(value)
|
||||||
|
wheels = root / "wheels"
|
||||||
|
webui = root / "webui"
|
||||||
|
wheels.mkdir()
|
||||||
|
webui.mkdir()
|
||||||
|
wheel = wheels / "govoplan_demo-1.2.3-py3-none-any.whl"
|
||||||
|
with zipfile.ZipFile(wheel, "w") as archive:
|
||||||
|
archive.writestr(
|
||||||
|
"govoplan_demo-1.2.3.dist-info/METADATA",
|
||||||
|
"Metadata-Version: 2.1\nName: govoplan-demo\nVersion: 1.2.3\n",
|
||||||
|
)
|
||||||
|
package_json = json.dumps(
|
||||||
|
{"name": "@govoplan/demo-webui", "version": "1.2.3"}
|
||||||
|
).encode("utf-8")
|
||||||
|
npm = webui / "govoplan-demo-webui-1.2.3.tgz"
|
||||||
|
with tarfile.open(npm, "w:gz") as archive:
|
||||||
|
member = tarfile.TarInfo("package/package.json")
|
||||||
|
member.size = len(package_json)
|
||||||
|
archive.addfile(member, BytesIO(package_json))
|
||||||
|
source = {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"repository": "govoplan-demo",
|
||||||
|
"tag": "v1.2.3",
|
||||||
|
"commit": "1" * 40,
|
||||||
|
}
|
||||||
|
|
||||||
|
python_rows = ARTIFACTS._verify_wheels(
|
||||||
|
({"name": "govoplan-demo", "extras": ["server"], **source},), wheels
|
||||||
|
)
|
||||||
|
webui_rows = ARTIFACTS._verify_webui(
|
||||||
|
({"name": "@govoplan/demo-webui", **source},), webui
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("govoplan-demo", python_rows[0]["name"])
|
||||||
|
self.assertEqual(["server"], python_rows[0]["extras"])
|
||||||
|
self.assertEqual("@govoplan/demo-webui", webui_rows[0]["name"])
|
||||||
|
self.assertTrue(str(webui_rows[0]["integrity"]).startswith("sha512-"))
|
||||||
|
|
||||||
|
def test_package_set_rejects_argument_shaped_package_names(self) -> None:
|
||||||
|
payload = {
|
||||||
|
"schema_version": "1",
|
||||||
|
"release_version": "1.2.3",
|
||||||
|
"registries": {
|
||||||
|
"python": "https://packages.example.test/pypi/simple",
|
||||||
|
"npm": "https://packages.example.test/npm/",
|
||||||
|
},
|
||||||
|
"python": [
|
||||||
|
{
|
||||||
|
"name": "--index-url",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"repository": "govoplan-demo",
|
||||||
|
"extras": [],
|
||||||
|
"tag": "v1.2.3",
|
||||||
|
"commit": "1" * 40,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"webui": [
|
||||||
|
{
|
||||||
|
"name": "@govoplan/demo-webui",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"repository": "govoplan-demo",
|
||||||
|
"tag": "v1.2.3",
|
||||||
|
"commit": "1" * 40,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
payload["package_set_sha256"] = ARTIFACTS._canonical_sha256(payload)
|
||||||
|
with tempfile.TemporaryDirectory() as value:
|
||||||
|
path = Path(value) / "packages.json"
|
||||||
|
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
ARTIFACTS.PackageArtifactError, "invalid identity"
|
||||||
|
):
|
||||||
|
ARTIFACTS._load_package_set(path)
|
||||||
|
|
||||||
|
def test_runtime_workflow_consumes_registry_artifacts_and_publishes_lock(self) -> None:
|
||||||
|
workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("resolve-package-artifacts.py", workflow)
|
||||||
|
self.assertIn("package-artifacts.lock.json", workflow)
|
||||||
|
self.assertIn(
|
||||||
|
"--package-lock runtime-output/package-artifacts.lock.json",
|
||||||
|
workflow,
|
||||||
|
)
|
||||||
|
self.assertNotIn(
|
||||||
|
"pip wheel --no-deps --wheel-dir runtime-output/local-wheels",
|
||||||
|
workflow,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_developer_meta_package_matches_workspace_versions(self) -> None:
|
||||||
|
script = _load(
|
||||||
|
"generate_developer_meta_package",
|
||||||
|
ROOT / "tools/release/generate-developer-meta-package.py",
|
||||||
|
)
|
||||||
|
expected = script.render(
|
||||||
|
workspace=ROOT.parent,
|
||||||
|
requirements=ROOT / "requirements-release.txt",
|
||||||
|
)
|
||||||
|
actual = (ROOT / "packages/govoplan-meta/pyproject.toml").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
self.assertEqual(expected, actual)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
TOOLS_ROOT = META_ROOT / "tools" / "gitea"
|
||||||
|
if str(TOOLS_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(TOOLS_ROOT))
|
||||||
|
SCRIPT = TOOLS_ROOT / "gitea-dispatch-package-set.py"
|
||||||
|
SPEC = importlib.util.spec_from_file_location("gitea_dispatch_package_set", SCRIPT)
|
||||||
|
assert SPEC is not None and SPEC.loader is not None
|
||||||
|
MODULE = importlib.util.module_from_spec(SPEC)
|
||||||
|
sys.modules[SPEC.name] = MODULE
|
||||||
|
SPEC.loader.exec_module(MODULE)
|
||||||
|
|
||||||
|
|
||||||
|
class PackageSetDispatchTests(unittest.TestCase):
|
||||||
|
def test_meta_package_resolves_to_exact_tagged_repository_targets(self) -> None:
|
||||||
|
targets = MODULE.package_targets()
|
||||||
|
|
||||||
|
self.assertEqual(66, len(targets))
|
||||||
|
self.assertEqual(66, len({target.distribution for target in targets}))
|
||||||
|
by_name = {target.distribution: target for target in targets}
|
||||||
|
self.assertEqual("v0.1.14", by_name["govoplan-core"].tag)
|
||||||
|
self.assertEqual("v0.1.8", by_name["govoplan-access"].tag)
|
||||||
|
self.assertTrue(by_name["govoplan-core"].tag_exists)
|
||||||
|
self.assertTrue(by_name["govoplan-access"].has_webui)
|
||||||
|
self.assertEqual(
|
||||||
|
"@govoplan/access-webui",
|
||||||
|
by_name["govoplan-access"].webui_package,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT = (
|
||||||
|
Path(__file__).resolve().parents[1]
|
||||||
|
/ "tools"
|
||||||
|
/ "inventory"
|
||||||
|
/ "platform-interface-inventory.py"
|
||||||
|
)
|
||||||
|
SPEC = importlib.util.spec_from_file_location("platform_interface_inventory", SCRIPT)
|
||||||
|
assert SPEC is not None and SPEC.loader is not None
|
||||||
|
inventory = importlib.util.module_from_spec(SPEC)
|
||||||
|
SPEC.loader.exec_module(inventory)
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformInterfaceInventoryTests(unittest.TestCase):
|
||||||
|
def test_workspace_resolution_prefers_populated_checkout_siblings(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
sibling = root / "checkout"
|
||||||
|
meta = sibling / "govoplan"
|
||||||
|
configured = root / "configured"
|
||||||
|
(sibling / "govoplan-core" / "src").mkdir(parents=True)
|
||||||
|
(configured / "govoplan-core").mkdir(parents=True)
|
||||||
|
previous = inventory.META_ROOT
|
||||||
|
inventory.META_ROOT = meta
|
||||||
|
try:
|
||||||
|
resolved = inventory._resolve_workspace_root(
|
||||||
|
{
|
||||||
|
"default_parent": str(configured),
|
||||||
|
"repositories": [{"path": "govoplan-core"}],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
inventory.META_ROOT = previous
|
||||||
|
|
||||||
|
self.assertEqual(sibling.resolve(), resolved)
|
||||||
|
|
||||||
|
def test_canonical_api_path_normalizes_versions_and_parameters(self) -> None:
|
||||||
|
self.assertEqual(
|
||||||
|
inventory.canonical_api_path(
|
||||||
|
"http://localhost/api/v1/campaigns/${campaignId}?limit=10"
|
||||||
|
),
|
||||||
|
"/campaigns/{}",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
inventory.canonical_api_path("/api/v2/campaigns/{campaign_id}"),
|
||||||
|
"/campaigns/{}",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
inventory.canonical_api_path("/api/v1/calendar/events/delta${querySuffix}"),
|
||||||
|
"/calendar/events/delta",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
inventory.canonical_api_path("/api/v1/calendar/events/${eventId}"),
|
||||||
|
"/calendar/events/{}",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_endpoint_declarations_are_exact_and_require_missing_ui_issue(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
path = Path(directory) / "endpoints.json"
|
||||||
|
path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"schema_version": 1,
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"repository": "govoplan-example",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/example/items/{}",
|
||||||
|
"category": "public_integration",
|
||||||
|
"rationale": "Published integration API.",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
declarations = inventory._load_endpoint_declarations(path)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
("govoplan-example", "GET", "/example/items/{}"),
|
||||||
|
declarations,
|
||||||
|
)
|
||||||
|
payload = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
payload["endpoints"][0]["category"] = "missing_ui"
|
||||||
|
path.write_text(json.dumps(payload), encoding="utf-8")
|
||||||
|
with self.assertRaisesRegex(ValueError, "tracking_issue"):
|
||||||
|
inventory._load_endpoint_declarations(path)
|
||||||
|
|
||||||
|
def test_inventory_reports_unclassified_and_stale_endpoint_declarations(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
webui = {
|
||||||
|
"frontendApiReferences": [],
|
||||||
|
"translationUsages": [],
|
||||||
|
"translationCatalog": {"en": {}, "de": {}},
|
||||||
|
"fields": [],
|
||||||
|
"labels": [],
|
||||||
|
"visibleText": [],
|
||||||
|
"routes": [],
|
||||||
|
"navigation": [],
|
||||||
|
"uiCapabilities": [],
|
||||||
|
"dynamicTranslationUsages": [],
|
||||||
|
}
|
||||||
|
endpoint = {
|
||||||
|
"repository": "govoplan-example",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/v1/example/items",
|
||||||
|
"file": "src/example.py",
|
||||||
|
"line": 1,
|
||||||
|
"handler": "items",
|
||||||
|
"router": "router",
|
||||||
|
}
|
||||||
|
stale = {
|
||||||
|
"repository": "govoplan-example",
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/example/removed",
|
||||||
|
"category": "removable",
|
||||||
|
"rationale": "Removal is pending.",
|
||||||
|
}
|
||||||
|
|
||||||
|
result = inventory._assemble_inventory(
|
||||||
|
webui=webui,
|
||||||
|
backend_endpoints=[endpoint],
|
||||||
|
manifests=[],
|
||||||
|
endpoint_declarations={
|
||||||
|
("govoplan-example", "GET", "/example/removed"): stale,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(1, result["summary"]["unclassified_backend_endpoints"])
|
||||||
|
self.assertEqual(1, result["summary"]["stale_endpoint_declarations"])
|
||||||
|
self.assertIsNone(result["api"]["backend_endpoints"][0]["surface"])
|
||||||
|
|
||||||
|
def test_endpoint_only_strict_mode_does_not_fail_on_translation_debt(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
result = {
|
||||||
|
"translation_health": {"missing_catalog_entries": ["missing.key"]},
|
||||||
|
"api": {
|
||||||
|
"unclassified_endpoints": [],
|
||||||
|
"stale_endpoint_declarations": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[],
|
||||||
|
inventory._strict_failures(
|
||||||
|
result,
|
||||||
|
check_translations=False,
|
||||||
|
check_endpoints=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
["used translation keys are missing from generated catalogs"],
|
||||||
|
inventory._strict_failures(
|
||||||
|
result,
|
||||||
|
check_translations=True,
|
||||||
|
check_endpoints=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_fastapi_route_scanner_includes_router_prefix(self) -> None:
|
||||||
|
tree = ast.parse(
|
||||||
|
"""
|
||||||
|
from fastapi import APIRouter
|
||||||
|
router = APIRouter(prefix="/api/v1/items")
|
||||||
|
|
||||||
|
@router.get("/{item_id}")
|
||||||
|
def read_item(item_id: str):
|
||||||
|
return item_id
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
prefixes = inventory._router_prefixes(tree)
|
||||||
|
function = next(
|
||||||
|
node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef)
|
||||||
|
)
|
||||||
|
|
||||||
|
route = inventory._endpoint_from_decorator(
|
||||||
|
function.decorator_list[0],
|
||||||
|
prefixes=prefixes,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
route,
|
||||||
|
{
|
||||||
|
"method": "GET",
|
||||||
|
"path": "/api/v1/items/{item_id}",
|
||||||
|
"router": "router",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_source_declarations_normalize_stable_control_and_contribution_ids(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
webui = {
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"repository": "govoplan-example",
|
||||||
|
"file": "webui/src/Example.tsx",
|
||||||
|
"line": 12,
|
||||||
|
"column": 3,
|
||||||
|
"id": "govoplan-example.field.example.name.abc123",
|
||||||
|
"idSource": "source_anchor",
|
||||||
|
"explicitId": None,
|
||||||
|
"context": "Example",
|
||||||
|
"helpId": "govoplan-example.field.example.name.abc123.help",
|
||||||
|
"helpDynamic": False,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"actions": [],
|
||||||
|
"contributions": [
|
||||||
|
{
|
||||||
|
"repository": "govoplan-example",
|
||||||
|
"file": "webui/src/module.ts",
|
||||||
|
"line": 20,
|
||||||
|
"column": 5,
|
||||||
|
"kind": "frontend_route",
|
||||||
|
"id": "/examples/:exampleId",
|
||||||
|
"path": "/examples/:exampleId",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"translationCatalog": {"en": {}, "de": {}},
|
||||||
|
}
|
||||||
|
manifests = [{"repository": "govoplan-example", "id": "examples"}]
|
||||||
|
|
||||||
|
declarations = inventory._source_interface_declarations(webui, manifests)
|
||||||
|
keys = {item["key"] for item in declarations}
|
||||||
|
|
||||||
|
self.assertIn("field:examples.field.example.name.abc123", keys)
|
||||||
|
self.assertIn(
|
||||||
|
"help:examples.field.example.name.abc123.help",
|
||||||
|
keys,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"frontend_route:examples.route.examples.exampleid",
|
||||||
|
keys,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_declaration_health_rejects_duplicate_and_undeclared_source_ids(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
declaration = {
|
||||||
|
"key": "frontend_route:example.route.unlisted",
|
||||||
|
"id": "example.route.unlisted",
|
||||||
|
"module_id": "example",
|
||||||
|
"kind": "frontend_route",
|
||||||
|
"origin": "webui_contribution",
|
||||||
|
}
|
||||||
|
manifests = [
|
||||||
|
{
|
||||||
|
"id": "example",
|
||||||
|
"repository": "govoplan-example",
|
||||||
|
"interface_catalog": {"declarations": []},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
health = inventory._declaration_health(
|
||||||
|
[declaration, dict(declaration)],
|
||||||
|
manifests,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(health["duplicate_ids"]))
|
||||||
|
self.assertEqual(1, len(health["undeclared_source_surfaces"]))
|
||||||
|
|
||||||
|
def test_runtime_snapshot_comparison_accepts_an_installed_subset(self) -> None:
|
||||||
|
manifests = [
|
||||||
|
{
|
||||||
|
"id": "one",
|
||||||
|
"interface_catalog": {
|
||||||
|
"contract_version": "1",
|
||||||
|
"module_id": "one",
|
||||||
|
"module_version": "1.0.0",
|
||||||
|
"digest": "sha256:one",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "two",
|
||||||
|
"interface_catalog": {
|
||||||
|
"contract_version": "1",
|
||||||
|
"module_id": "two",
|
||||||
|
"module_version": "1.0.0",
|
||||||
|
"digest": "sha256:two",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
snapshot = {
|
||||||
|
"contract_version": "1",
|
||||||
|
"modules": [dict(manifests[1]["interface_catalog"])],
|
||||||
|
}
|
||||||
|
|
||||||
|
comparison = inventory._compare_runtime_snapshot(snapshot, manifests)
|
||||||
|
|
||||||
|
self.assertEqual(["two"], comparison["matched_modules"])
|
||||||
|
self.assertEqual([], comparison["mismatches"])
|
||||||
|
|
||||||
|
snapshot["modules"][0]["digest"] = "sha256:changed"
|
||||||
|
comparison = inventory._compare_runtime_snapshot(snapshot, manifests)
|
||||||
|
self.assertEqual("digest_mismatch", comparison["mismatches"][0]["reason"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SCRIPT = META_ROOT / "tools" / "checks" / "postgres-integration-check.py"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_script():
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
"postgres_integration_check",
|
||||||
|
SCRIPT,
|
||||||
|
)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise RuntimeError(f"Could not load {SCRIPT}")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
class PostgresIntegrationCheckTests(unittest.TestCase):
|
||||||
|
def test_staging_check_has_explicit_runtime_safety_settings(self) -> None:
|
||||||
|
module = _load_script()
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
env = module._check_env(
|
||||||
|
database_url="postgresql+psycopg://user:password@postgres/db",
|
||||||
|
modules="tenancy,access,search",
|
||||||
|
app_env="staging",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
env["CORS_ORIGINS"],
|
||||||
|
"http://127.0.0.1:5173,http://localhost:5173",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
env["GOVOPLAN_TRUSTED_HOSTS"],
|
||||||
|
"127.0.0.1,localhost,testserver",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
env["GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS"],
|
||||||
|
"false",
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
env["GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE"],
|
||||||
|
"true",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_existing_runtime_safety_settings_are_preserved(self) -> None:
|
||||||
|
module = _load_script()
|
||||||
|
configured = {
|
||||||
|
"CORS_ORIGINS": "https://govoplan.example.test",
|
||||||
|
"GOVOPLAN_TRUSTED_HOSTS": "govoplan.example.test",
|
||||||
|
"GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS": "true",
|
||||||
|
"GOVOPLAN_ALLOW_PROCESS_LOCAL_LOGIN_THROTTLE": "false",
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.dict(os.environ, configured, clear=True):
|
||||||
|
env = module._check_env(
|
||||||
|
database_url="postgresql+psycopg://user:password@postgres/db",
|
||||||
|
modules="tenancy,access,search",
|
||||||
|
app_env="staging",
|
||||||
|
)
|
||||||
|
|
||||||
|
for key, value in configured.items():
|
||||||
|
self.assertEqual(env[key], value)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -77,6 +77,130 @@ class PythonEnvironmentSyncTests(unittest.TestCase):
|
|||||||
self.assertIn(str(root / "govoplan-core"), command)
|
self.assertIn(str(root / "govoplan-core"), command)
|
||||||
self.assertIn(str(root / "govoplan-access"), command)
|
self.assertIn(str(root / "govoplan-access"), command)
|
||||||
|
|
||||||
|
def test_missing_editable_distribution_is_not_hidden_by_current_stamp(self) -> None:
|
||||||
|
sync = load_sync_module()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-python-sync-") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
project_root = root / "govoplan-probe-missing"
|
||||||
|
project_root.mkdir()
|
||||||
|
(project_root / "pyproject.toml").write_text(
|
||||||
|
"\n".join(
|
||||||
|
(
|
||||||
|
"[project]",
|
||||||
|
'name = "govoplan-probe-definitely-not-installed"',
|
||||||
|
'version = "0.1.0"',
|
||||||
|
"",
|
||||||
|
'[project.entry-points."govoplan.modules"]',
|
||||||
|
'probe_missing = "govoplan_probe.backend.manifest:get_manifest"',
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
requirements = root / "requirements-dev.txt"
|
||||||
|
requirements.write_text("-e ./govoplan-probe-missing\n", encoding="utf-8")
|
||||||
|
entries = sync.local_requirement_entries(requirements)
|
||||||
|
|
||||||
|
validation = sync.validate_local_installations(sys.executable, entries)
|
||||||
|
plan = sync.build_environment_repair_plan(
|
||||||
|
python=sys.executable,
|
||||||
|
stale_requirements=validation.stale_requirements,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(validation.current)
|
||||||
|
self.assertEqual(validation.stale_requirements, entries)
|
||||||
|
self.assertIn("distribution is not installed", validation.issues[0])
|
||||||
|
self.assertEqual(plan.mode, "Selective Python environment repair")
|
||||||
|
self.assertEqual(plan.commands[0][-2:], ("-e", str(project_root)))
|
||||||
|
|
||||||
|
def test_metadata_sync_also_repairs_unrelated_missing_distribution(self) -> None:
|
||||||
|
sync = load_sync_module()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-python-sync-") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
requirements = root / "requirements-dev.txt"
|
||||||
|
requirements.write_text("-e ./govoplan-changed\n-e ./govoplan-missing\n", encoding="utf-8")
|
||||||
|
for project in ("govoplan-changed", "govoplan-missing"):
|
||||||
|
project_root = root / project
|
||||||
|
project_root.mkdir()
|
||||||
|
(project_root / "pyproject.toml").write_text(
|
||||||
|
f'[project]\nname = "{project}"\nversion = "0.1.10"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
entries = sync.local_requirement_entries(requirements)
|
||||||
|
fingerprint = sync.build_fingerprint(
|
||||||
|
requirements=requirements,
|
||||||
|
python="/test/venv/bin/python",
|
||||||
|
local_requirements=entries,
|
||||||
|
)
|
||||||
|
requirements_digest = hashlib.sha256(requirements.read_bytes()).hexdigest()
|
||||||
|
previous = {
|
||||||
|
"version": sync.STAMP_VERSION,
|
||||||
|
"python": "/test/venv/bin/python",
|
||||||
|
"inputs": [
|
||||||
|
{"path": str(requirements), "sha256": requirements_digest},
|
||||||
|
{"path": entries[0].pyproject, "sha256": "stale"},
|
||||||
|
{
|
||||||
|
"path": entries[1].pyproject,
|
||||||
|
"sha256": hashlib.sha256(Path(entries[1].pyproject).read_bytes()).hexdigest(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"requirements_entries": [
|
||||||
|
entry.as_dict() for entry in sync.parse_requirement_entries(requirements)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
plan = sync.build_install_plan(
|
||||||
|
previous=previous,
|
||||||
|
fingerprint=fingerprint,
|
||||||
|
requirements=requirements,
|
||||||
|
python="/test/venv/bin/python",
|
||||||
|
local_requirements=entries,
|
||||||
|
repair_requirements=(entries[1],),
|
||||||
|
force=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(plan.mode, "Selective Python environment sync")
|
||||||
|
self.assertEqual(len(plan.commands), 1)
|
||||||
|
command = plan.commands[0]
|
||||||
|
self.assertEqual(command.count("-e"), 2)
|
||||||
|
self.assertIn(str(root / "govoplan-changed"), command)
|
||||||
|
self.assertIn(str(root / "govoplan-missing"), command)
|
||||||
|
|
||||||
|
def test_declared_module_entry_points_are_part_of_environment_validation(self) -> None:
|
||||||
|
sync = load_sync_module()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-python-sync-") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
project_root = root / "govoplan-probe"
|
||||||
|
project_root.mkdir()
|
||||||
|
(project_root / "pyproject.toml").write_text(
|
||||||
|
"\n".join(
|
||||||
|
(
|
||||||
|
"[project]",
|
||||||
|
'name = "govoplan-probe"',
|
||||||
|
'version = "0.1.0"',
|
||||||
|
"",
|
||||||
|
'[project.entry-points."govoplan.modules"]',
|
||||||
|
'probe = "govoplan_probe.backend.manifest:get_manifest"',
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
requirements = root / "requirements-dev.txt"
|
||||||
|
requirements.write_text("-e ./govoplan-probe\n", encoding="utf-8")
|
||||||
|
|
||||||
|
expectations = sync.local_installation_expectations(
|
||||||
|
sync.local_requirement_entries(requirements)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(expectations), 1)
|
||||||
|
self.assertEqual(expectations[0].distribution, "govoplan-probe")
|
||||||
|
self.assertEqual(
|
||||||
|
expectations[0].entry_points,
|
||||||
|
(("govoplan.modules", "probe", "govoplan_probe.backend.manifest:get_manifest"),),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
INVENTORY = ROOT / "docs" / "recovery-operation-inventory.json"
|
||||||
|
MODES = {
|
||||||
|
"atomic",
|
||||||
|
"compensation",
|
||||||
|
"snapshot_restore",
|
||||||
|
"forward_recovery",
|
||||||
|
"irreversible",
|
||||||
|
}
|
||||||
|
ADOPTION_STATES = {"planned", "reference-implementation", "adopted"}
|
||||||
|
REQUIRED_PREFIXES = {
|
||||||
|
"campaign.",
|
||||||
|
"files.",
|
||||||
|
"mail.",
|
||||||
|
"connectors.",
|
||||||
|
"dataflow.",
|
||||||
|
"workflow-engine.",
|
||||||
|
"core.module-lifecycle.",
|
||||||
|
"core.module-runtime.",
|
||||||
|
}
|
||||||
|
ATOMIC_EXTERNAL_READS = {
|
||||||
|
"connectors.sync.read-snapshot",
|
||||||
|
"mail.mailbox.sync-cursor",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_recovery_operation_inventory_is_complete_and_actionable() -> None:
|
||||||
|
payload = json.loads(INVENTORY.read_text(encoding="utf-8"))
|
||||||
|
assert payload["schema_version"] == 1
|
||||||
|
operations = payload["operations"]
|
||||||
|
ids = [item["id"] for item in operations]
|
||||||
|
assert len(ids) == len(set(ids))
|
||||||
|
assert all(any(item.startswith(prefix) for item in ids) for prefix in REQUIRED_PREFIXES)
|
||||||
|
|
||||||
|
for item in operations:
|
||||||
|
assert item["mode"] in MODES
|
||||||
|
assert item["adoption"] in ADOPTION_STATES
|
||||||
|
assert item["repository"].startswith("govoplan-")
|
||||||
|
assert item["resources"]
|
||||||
|
assert item["fenced"] is True
|
||||||
|
issue = urlparse(item["issue"])
|
||||||
|
assert issue.scheme == "https"
|
||||||
|
assert issue.netloc == "git.add-ideas.de"
|
||||||
|
assert issue.path.startswith(f"/GovOPlaN/{item['repository']}/issues/")
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_atomic_operations_do_not_claim_plain_database_rollback() -> None:
|
||||||
|
operations = json.loads(INVENTORY.read_text(encoding="utf-8"))["operations"]
|
||||||
|
for item in operations:
|
||||||
|
if item["mode"] == "atomic":
|
||||||
|
assert (
|
||||||
|
item["resources"] == ["postgresql"]
|
||||||
|
or item["id"] in ATOMIC_EXTERNAL_READS
|
||||||
|
)
|
||||||
@@ -4,17 +4,101 @@ import sys
|
|||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from govoplan_core.core.modules import ModuleManifest
|
||||||
|
from govoplan_core.core.provider_governance import (
|
||||||
|
ExternalProviderDeclaration,
|
||||||
|
ModuleArchitectureDeclaration,
|
||||||
|
ModuleMaturityEvidence,
|
||||||
|
ProviderBehaviorDeclaration,
|
||||||
|
ProviderObjectDeclaration,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
META_ROOT = Path(__file__).resolve().parents[1]
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
RELEASE_ROOT = META_ROOT / "tools" / "release"
|
RELEASE_ROOT = META_ROOT / "tools" / "release"
|
||||||
if str(RELEASE_ROOT) not in sys.path:
|
if str(RELEASE_ROOT) not in sys.path:
|
||||||
sys.path.insert(0, str(RELEASE_ROOT))
|
sys.path.insert(0, str(RELEASE_ROOT))
|
||||||
|
|
||||||
from govoplan_release.catalog_entry_synthesis import validate_initial_entry_closure # noqa: E402
|
from govoplan_release.catalog_entry_synthesis import ( # noqa: E402
|
||||||
|
manifest_catalog_entry,
|
||||||
|
validate_initial_entry_closure,
|
||||||
|
)
|
||||||
from govoplan_release.selective_catalog import apply_repo_updates # noqa: E402
|
from govoplan_release.selective_catalog import apply_repo_updates # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
|
class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
|
||||||
|
def test_catalog_entry_preserves_architecture_and_provider_declarations(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
provider = ExternalProviderDeclaration(
|
||||||
|
id="example.records",
|
||||||
|
module_id="example",
|
||||||
|
label="Example records",
|
||||||
|
maturity="read",
|
||||||
|
operations=("read",),
|
||||||
|
objects=(
|
||||||
|
ProviderObjectDeclaration(
|
||||||
|
object_type="record",
|
||||||
|
field_groups=("identity", "content"),
|
||||||
|
authority_modes=("external_mirror",),
|
||||||
|
default_authority_mode="external_mirror",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
behavior=ProviderBehaviorDeclaration(
|
||||||
|
freshness="Reports the acquisition timestamp.",
|
||||||
|
health="Reports source and parser health.",
|
||||||
|
max_read_items=100,
|
||||||
|
classifications=("internal",),
|
||||||
|
purposes=("release contract test",),
|
||||||
|
retention="The owning package retention policy applies.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
architecture = ModuleArchitectureDeclaration(
|
||||||
|
layer="data_reporting_integration",
|
||||||
|
kind="integration",
|
||||||
|
maturity="vertical_slice",
|
||||||
|
evidence=(
|
||||||
|
ModuleMaturityEvidence(
|
||||||
|
kind="test",
|
||||||
|
reference="tests/test_release_catalog_entry_synthesis.py",
|
||||||
|
summary="Proves catalog metadata preservation.",
|
||||||
|
),
|
||||||
|
ModuleMaturityEvidence(
|
||||||
|
kind="documentation",
|
||||||
|
reference="docs/INSTITUTIONAL_GOVERNANCE_TARGET_ARCHITECTURE.md",
|
||||||
|
summary="Defines the provider declaration contract.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
known_limits=("Test-only provider.",),
|
||||||
|
supported_authority_modes=("external_mirror",),
|
||||||
|
owned_concepts=("example transport",),
|
||||||
|
target_tested_providers=(provider.id,),
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = manifest_catalog_entry(
|
||||||
|
manifest=ModuleManifest(
|
||||||
|
id="example",
|
||||||
|
name="Example",
|
||||||
|
version="1.2.3",
|
||||||
|
architecture=architecture,
|
||||||
|
external_providers=(provider,),
|
||||||
|
),
|
||||||
|
repo="govoplan-example",
|
||||||
|
package="govoplan-example",
|
||||||
|
version="1.2.3",
|
||||||
|
description=None,
|
||||||
|
root=META_ROOT,
|
||||||
|
repository_base="git+ssh://git@git.add-ideas.de/GovOPlaN",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("vertical_slice", entry["architecture"]["maturity"])
|
||||||
|
self.assertEqual(
|
||||||
|
"external_mirror",
|
||||||
|
entry["external_providers"][0]["objects"][0][
|
||||||
|
"default_authority_mode"
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
def test_selective_update_synthesizes_initial_entries_from_package_manifests(self) -> None:
|
def test_selective_update_synthesizes_initial_entries_from_package_manifests(self) -> None:
|
||||||
payload: dict[str, object] = {
|
payload: dict[str, object] = {
|
||||||
"core_release": {},
|
"core_release": {},
|
||||||
@@ -24,7 +108,7 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
|
|||||||
"module_id": "access",
|
"module_id": "access",
|
||||||
"version": "0.1.11",
|
"version": "0.1.11",
|
||||||
"python_package": "govoplan-access",
|
"python_package": "govoplan-access",
|
||||||
"python_ref": "govoplan-access @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-access.git@v0.1.11",
|
"python_ref": "govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.11",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -37,7 +121,7 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
|
|||||||
"govoplan-scheduling": "0.1.11",
|
"govoplan-scheduling": "0.1.11",
|
||||||
},
|
},
|
||||||
repo_contracts={},
|
repo_contracts={},
|
||||||
repository_base="git+ssh://git@git.add-ideas.de/add-ideas",
|
repository_base="git+ssh://git@git.add-ideas.de/GovOPlaN",
|
||||||
workspace=META_ROOT.parent,
|
workspace=META_ROOT.parent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -29,17 +29,72 @@ class ReleaseEntrypointGateTests(unittest.TestCase):
|
|||||||
workflow = script[confirm:]
|
workflow = script[confirm:]
|
||||||
|
|
||||||
source_gate = workflow.index("run_version_alignment_gate source")
|
source_gate = workflow.index("run_version_alignment_gate source")
|
||||||
|
baseline = workflow.index("record_migration_release_baseline")
|
||||||
first_commit = workflow.index('run git -C "$repo" commit')
|
first_commit = workflow.index('run git -C "$repo" commit')
|
||||||
lock_generation = workflow.index("generate_release_lock")
|
lock_generation = workflow.index("generate_release_lock")
|
||||||
full_gate = workflow.index("run_version_alignment_gate", source_gate + 1)
|
full_gate = workflow.index("run_version_alignment_gate", source_gate + 1)
|
||||||
first_push = workflow.index('run git -C "$repo" push')
|
first_push = workflow.index('run git -C "$repo" push')
|
||||||
|
|
||||||
|
self.assertLess(baseline, source_gate)
|
||||||
self.assertLess(source_gate, first_commit)
|
self.assertLess(source_gate, first_commit)
|
||||||
self.assertLess(first_commit, lock_generation)
|
self.assertLess(first_commit, lock_generation)
|
||||||
self.assertLess(lock_generation, full_gate)
|
self.assertLess(lock_generation, full_gate)
|
||||||
self.assertLess(full_gate, first_push)
|
self.assertLess(full_gate, first_push)
|
||||||
self.assertLess(manifest_gate, confirm)
|
self.assertLess(manifest_gate, confirm)
|
||||||
|
|
||||||
|
def test_lockstep_release_pushes_meta_package_after_core(self) -> None:
|
||||||
|
script = (META_ROOT / "tools" / "release" / "push-release-tag.sh").read_text()
|
||||||
|
module_push = script.index('for repo in "${MODULE_REPOS[@]}"; do\n run git -C "$repo" push')
|
||||||
|
core_push = script.index('run git -C "$ROOT" push', module_push)
|
||||||
|
support_push = script.index('for repo in "${SUPPORT_REPOS[@]}"; do\n run git -C "$repo" push', core_push)
|
||||||
|
|
||||||
|
self.assertLess(module_push, core_push)
|
||||||
|
self.assertLess(core_push, support_push)
|
||||||
|
|
||||||
|
def test_default_migration_preflight_accepts_new_release_heads(self) -> None:
|
||||||
|
script = (META_ROOT / "tools" / "release" / "push-release-tag.sh").read_text()
|
||||||
|
audit_function = script[
|
||||||
|
script.index("run_migration_release_audit()") :
|
||||||
|
script.index("record_migration_release_baseline()")
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertNotIn("--strict-if-baseline", audit_function)
|
||||||
|
self.assertIn('command+=("--strict")', audit_function)
|
||||||
|
|
||||||
|
def test_source_gate_does_not_require_tags_before_they_are_created(self) -> None:
|
||||||
|
script = (META_ROOT / "tools" / "release" / "push-release-tag.sh").read_text()
|
||||||
|
gate = script[
|
||||||
|
script.index("run_version_alignment_gate()") :
|
||||||
|
script.index("run_manifest_shape_gate()")
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertIn('command+=(--source-metadata-only)', gate)
|
||||||
|
self.assertIn('else\n command+=(--release-composition)', gate)
|
||||||
|
|
||||||
|
def test_version_updater_targets_canonical_runtime_declarations(self) -> None:
|
||||||
|
script = (META_ROOT / "tools" / "release" / "push-release-tag.sh").read_text()
|
||||||
|
|
||||||
|
self.assertIn("^manifest\\s*=\\s*ModuleManifest", script)
|
||||||
|
self.assertIn("could not update module version declaration", script)
|
||||||
|
self.assertIn("update_package_init_versions", script)
|
||||||
|
self.assertIn("synchronize-webui-package-metadata.py", script)
|
||||||
|
self.assertIn('"peerDependenciesMeta",', script)
|
||||||
|
self.assertLess(
|
||||||
|
script.index('synchronize-webui-package-metadata.py" --repo "$repo"'),
|
||||||
|
script.index('synchronize_lockfile_root "$package_path"', script.index('synchronize-webui-package-metadata.py" --repo "$repo"')),
|
||||||
|
)
|
||||||
|
self.assertNotIn("could not update ModuleManifest.version", script)
|
||||||
|
|
||||||
|
def test_release_lock_refreshes_candidate_govoplan_metadata(self) -> None:
|
||||||
|
script = (META_ROOT / "tools" / "release" / "generate-release-lock.sh").read_text()
|
||||||
|
|
||||||
|
self.assertEqual(2, script.count('"npm_config_cache=$TMP_DIR/npm-cache"'))
|
||||||
|
self.assertNotIn(
|
||||||
|
'cp "$WEBUI/package-lock.release.json" "$TMP_DIR/package-lock.json"',
|
||||||
|
script,
|
||||||
|
)
|
||||||
|
self.assertIn('cp "$WEBUI/package.release.json" "$TMP_DIR/package.json"', script)
|
||||||
|
|
||||||
def test_source_catalog_generator_enforces_explicit_repo_versions(self) -> None:
|
def test_source_catalog_generator_enforces_explicit_repo_versions(self) -> None:
|
||||||
script = (META_ROOT / "tools" / "release" / "generate-release-catalog.py").read_text()
|
script = (META_ROOT / "tools" / "release" / "generate-release-catalog.py").read_text()
|
||||||
|
|
||||||
|
|||||||
+176
-13
@@ -9,6 +9,7 @@ import tempfile
|
|||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
import unittest
|
import unittest
|
||||||
from unittest.mock import patch
|
from unittest.mock import patch
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
META_ROOT = Path(__file__).resolve().parents[1]
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
@@ -17,6 +18,7 @@ if str(RELEASE_ROOT) not in sys.path:
|
|||||||
sys.path.insert(0, str(RELEASE_ROOT))
|
sys.path.insert(0, str(RELEASE_ROOT))
|
||||||
|
|
||||||
from govoplan_release.model import RepositorySpec # noqa: E402
|
from govoplan_release.model import RepositorySpec # noqa: E402
|
||||||
|
from govoplan_release.artifact_identity import inspect_python_wheel # noqa: E402
|
||||||
from govoplan_release.git_state import sanitized_git_environment # noqa: E402
|
from govoplan_release.git_state import sanitized_git_environment # noqa: E402
|
||||||
from govoplan_release.release_execution import ( # noqa: E402
|
from govoplan_release.release_execution import ( # noqa: E402
|
||||||
ExecutorSpec,
|
ExecutorSpec,
|
||||||
@@ -30,6 +32,7 @@ from govoplan_release.release_execution import ( # noqa: E402
|
|||||||
require_trusted_release_runtime,
|
require_trusted_release_runtime,
|
||||||
verify_frozen_repository_tag_receipt,
|
verify_frozen_repository_tag_receipt,
|
||||||
verify_repository_preflight_binding,
|
verify_repository_preflight_binding,
|
||||||
|
verify_candidate_installation,
|
||||||
_clone_catalog_sources,
|
_clone_catalog_sources,
|
||||||
_checkout_frozen_source,
|
_checkout_frozen_source,
|
||||||
_flatpak_proxy_environment,
|
_flatpak_proxy_environment,
|
||||||
@@ -489,20 +492,180 @@ class ReleaseExecutionTests(unittest.TestCase):
|
|||||||
expected_receipt={"kind": "repository_state"},
|
expected_receipt={"kind": "repository_state"},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_commit_step_has_no_durable_executor(self) -> None:
|
def test_commit_step_has_narrow_confirmation(self) -> None:
|
||||||
self.assertIsNone(
|
spec = executor_spec(
|
||||||
executor_spec(
|
{
|
||||||
{
|
"id": "govoplan-files:commit",
|
||||||
"id": "govoplan-files:commit",
|
"status": "planned",
|
||||||
"status": "planned",
|
"mutating": True,
|
||||||
"mutating": True,
|
"repo": "govoplan-files",
|
||||||
"repo": "govoplan-files",
|
"source_binding": {
|
||||||
"source_binding": {
|
"kind": "repository_state",
|
||||||
"kind": "repository_state",
|
},
|
||||||
},
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
self.assertIsNotNone(spec)
|
||||||
|
self.assertEqual("commit", spec.kind)
|
||||||
|
self.assertEqual("COMMIT", spec.confirmation)
|
||||||
|
|
||||||
|
def test_version_and_commit_executors_bind_only_release_metadata(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
workspace = Path(temp_dir)
|
||||||
|
repository = workspace / "govoplan-files"
|
||||||
|
remote = workspace / "files.git"
|
||||||
|
_git(workspace, "init", "--bare", str(remote))
|
||||||
|
_git(workspace, "init", "-b", "main", str(repository))
|
||||||
|
_git(repository, "config", "user.name", "Release Test")
|
||||||
|
_git(repository, "config", "user.email", "release@example.test")
|
||||||
|
(repository / "pyproject.toml").write_text(
|
||||||
|
'[project]\nname = "govoplan-files"\nversion = "1.2.3"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
_git(repository, "add", "pyproject.toml")
|
||||||
|
_git(repository, "commit", "-m", "Initial")
|
||||||
|
_git(repository, "remote", "add", "origin", str(remote))
|
||||||
|
_git(repository, "push", "-u", "origin", "main")
|
||||||
|
repository_spec = _repository_spec(remote)
|
||||||
|
plan_step = {
|
||||||
|
"id": "govoplan-files:version",
|
||||||
|
"repo": "govoplan-files",
|
||||||
|
"source_binding": {"kind": "repository_state"},
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"govoplan_release.release_execution.load_repository_specs",
|
||||||
|
return_value=(repository_spec,),
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"govoplan_release.release_execution.require_trusted_release_runtime"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
before = repository_state_receipt(
|
||||||
|
repo="govoplan-files",
|
||||||
|
target_tag="v1.2.4",
|
||||||
|
workspace_root=workspace,
|
||||||
|
)
|
||||||
|
_result, updated = execute_repository_step(
|
||||||
|
spec=ExecutorSpec(
|
||||||
|
"version",
|
||||||
|
"UPDATE",
|
||||||
|
"version_metadata_updated",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
plan_step=plan_step,
|
||||||
|
repo_versions={"govoplan-files": "1.2.4"},
|
||||||
|
workspace_root=workspace,
|
||||||
|
remote="origin",
|
||||||
|
expected_receipt=before,
|
||||||
|
)
|
||||||
|
self.assertFalse(updated["worktree_clean"])
|
||||||
|
self.assertNotEqual(
|
||||||
|
before["worktree_sha256"],
|
||||||
|
updated["worktree_sha256"],
|
||||||
|
)
|
||||||
|
|
||||||
|
plan_step["id"] = "govoplan-files:commit"
|
||||||
|
_result, committed = execute_repository_step(
|
||||||
|
spec=ExecutorSpec(
|
||||||
|
"commit",
|
||||||
|
"COMMIT",
|
||||||
|
"release_commit_created",
|
||||||
|
True,
|
||||||
|
),
|
||||||
|
plan_step=plan_step,
|
||||||
|
repo_versions={"govoplan-files": "1.2.4"},
|
||||||
|
workspace_root=workspace,
|
||||||
|
remote="origin",
|
||||||
|
expected_receipt=updated,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(committed["worktree_clean"])
|
||||||
|
self.assertNotEqual(before["head"], committed["head"])
|
||||||
|
self.assertIn(
|
||||||
|
'version = "1.2.4"',
|
||||||
|
(repository / "pyproject.toml").read_text(encoding="utf-8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_candidate_installation_verifies_installed_metadata(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
workspace = Path(temp_dir)
|
||||||
|
repository = workspace / "govoplan-files"
|
||||||
|
repository.mkdir()
|
||||||
|
(repository / "pyproject.toml").write_text(
|
||||||
|
'[project]\nname = "govoplan-files"\nversion = "1.2.4"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
artifacts = workspace / "candidate" / "artifacts"
|
||||||
|
artifacts.mkdir(parents=True)
|
||||||
|
wheel = artifacts / "govoplan_files-1.2.4-py3-none-any.whl"
|
||||||
|
members = {
|
||||||
|
"govoplan_files/__init__.py": '__version__ = "1.2.4"\n',
|
||||||
|
"govoplan_files-1.2.4.dist-info/METADATA": (
|
||||||
|
"Metadata-Version: 2.1\n"
|
||||||
|
"Name: govoplan-files\n"
|
||||||
|
"Version: 1.2.4\n"
|
||||||
|
),
|
||||||
|
"govoplan_files-1.2.4.dist-info/WHEEL": (
|
||||||
|
"Wheel-Version: 1.0\n"
|
||||||
|
"Generator: release-test\n"
|
||||||
|
"Root-Is-Purelib: true\n"
|
||||||
|
"Tag: py3-none-any\n"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
record = "\n".join(f"{name},," for name in members)
|
||||||
|
record += "\ngovoplan_files-1.2.4.dist-info/RECORD,,\n"
|
||||||
|
with zipfile.ZipFile(wheel, "w") as archive:
|
||||||
|
for name, content in members.items():
|
||||||
|
archive.writestr(name, content)
|
||||||
|
archive.writestr(
|
||||||
|
"govoplan_files-1.2.4.dist-info/RECORD",
|
||||||
|
record,
|
||||||
|
)
|
||||||
|
identity = inspect_python_wheel(wheel)
|
||||||
|
channels = workspace / "candidate" / "channels"
|
||||||
|
channels.mkdir()
|
||||||
|
(channels / "stable.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"release": {
|
||||||
|
"selected_units": [
|
||||||
|
{
|
||||||
|
"repo": "govoplan-files",
|
||||||
|
"version": "1.2.4",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"package_name": "govoplan-files",
|
||||||
|
"archive_sha256": identity.archive_sha256,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"core_release": {},
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"python_package": "govoplan-files",
|
||||||
|
"python_ref": (
|
||||||
|
"govoplan-files @ "
|
||||||
|
"git+ssh://git@example.test/GovOPlaN/"
|
||||||
|
"govoplan-files.git@v1.2.4"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = verify_candidate_installation(
|
||||||
|
candidate_path=workspace / "candidate",
|
||||||
|
repo_versions={"govoplan-files": "1.2.4"},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("verified", result["status"])
|
||||||
|
self.assertEqual("govoplan-files", result["packages"][0]["repo"])
|
||||||
|
self.assertEqual("1.2.4", result["packages"][0]["version"])
|
||||||
|
|
||||||
def test_frozen_checkout_uses_receipt_commit_not_live_worktree(self) -> None:
|
def test_frozen_checkout_uses_receipt_commit_not_live_worktree(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
RELEASE_TOOLS_ROOT = META_ROOT / "tools" / "release"
|
||||||
|
|
||||||
|
if str(RELEASE_TOOLS_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(RELEASE_TOOLS_ROOT))
|
||||||
|
|
||||||
|
from govoplan_release import git_state # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseGitStateTests(unittest.TestCase):
|
||||||
|
def test_manifest_version_does_not_confuse_interface_versions(self) -> None:
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
with TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory) / "govoplan-example"
|
||||||
|
manifest = root / "src" / "govoplan_example" / "backend" / "manifest.py"
|
||||||
|
manifest.parent.mkdir(parents=True)
|
||||||
|
manifest.write_text(
|
||||||
|
"\n".join(
|
||||||
|
(
|
||||||
|
'MODULE_VERSION = "1.2.3"',
|
||||||
|
"manifest = ModuleManifest(",
|
||||||
|
' id="example",',
|
||||||
|
" version=MODULE_VERSION,",
|
||||||
|
" provides_interfaces=(",
|
||||||
|
' ModuleInterfaceProvider(name="example.items", version="9.8.7"),',
|
||||||
|
" ),",
|
||||||
|
")",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
versions = git_state.read_manifest_versions(root)
|
||||||
|
|
||||||
|
self.assertEqual(("1.2.3",), versions)
|
||||||
|
|
||||||
|
def test_git_trusts_only_the_resolved_repository_for_each_command(self) -> None:
|
||||||
|
repository = Path("/workspace/../workspace/govoplan-core")
|
||||||
|
completed = subprocess.CompletedProcess([], 0, "", "")
|
||||||
|
|
||||||
|
with patch.object(git_state.subprocess, "run", return_value=completed) as run:
|
||||||
|
result = git_state.git(repository, "status", "--porcelain")
|
||||||
|
|
||||||
|
self.assertEqual(result.returncode, 0)
|
||||||
|
command = run.call_args.args[0]
|
||||||
|
self.assertIn(f"safe.directory={repository.resolve()}", command)
|
||||||
|
self.assertNotIn("safe.directory=*", command)
|
||||||
|
self.assertEqual(run.call_args.kwargs["cwd"], repository)
|
||||||
|
self.assertEqual(
|
||||||
|
run.call_args.kwargs["env"]["GIT_CONFIG_GLOBAL"],
|
||||||
|
"/dev/null",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from tools.checks.release_integration import (
|
from tools.checks.release_integration import (
|
||||||
SOURCE_COUPLED_CORE_TESTS,
|
SOURCE_COUPLED_CORE_TESTS,
|
||||||
@@ -10,6 +12,7 @@ from tools.checks.release_integration import (
|
|||||||
artifact_contract_issues,
|
artifact_contract_issues,
|
||||||
filter_test_suite,
|
filter_test_suite,
|
||||||
release_package_names,
|
release_package_names,
|
||||||
|
run_module_release_tests,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -32,9 +35,9 @@ class ReleaseIntegrationTests(unittest.TestCase):
|
|||||||
"\n".join(
|
"\n".join(
|
||||||
(
|
(
|
||||||
"../govoplan-core[server]",
|
"../govoplan-core[server]",
|
||||||
"govoplan-idm @ git+ssh://git@example.test/add-ideas/govoplan-idm.git@v0.1.8",
|
"govoplan-idm @ git+ssh://git@example.test/GovOPlaN/govoplan-idm.git@v0.1.8",
|
||||||
"govoplan-campaign @ git+ssh://git@example.test/add-ideas/govoplan-campaign.git@v0.1.8",
|
"govoplan-campaign @ git+ssh://git@example.test/GovOPlaN/govoplan-campaign.git@v0.1.8",
|
||||||
"govoplan-idm @ git+ssh://git@example.test/add-ideas/govoplan-idm.git@v0.1.8",
|
"govoplan-idm @ git+ssh://git@example.test/GovOPlaN/govoplan-idm.git@v0.1.8",
|
||||||
"other-package==1.0",
|
"other-package==1.0",
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -77,7 +80,7 @@ class ReleaseIntegrationTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(set(removed), set(SOURCE_COUPLED_CORE_TESTS))
|
self.assertEqual(set(removed), set(SOURCE_COUPLED_CORE_TESTS))
|
||||||
self.assertEqual([test.id() for test in filtered], [retained_id])
|
self.assertEqual([test.id() for test in filtered], [retained_id])
|
||||||
self.assertEqual(len(SOURCE_COUPLED_CORE_TESTS), 4)
|
self.assertEqual(len(SOURCE_COUPLED_CORE_TESTS), 5)
|
||||||
|
|
||||||
def test_release_entrypoint_uses_artifact_checks_and_filtered_core_suite(self) -> None:
|
def test_release_entrypoint_uses_artifact_checks_and_filtered_core_suite(self) -> None:
|
||||||
meta_root = Path(__file__).resolve().parents[1]
|
meta_root = Path(__file__).resolve().parents[1]
|
||||||
@@ -89,16 +92,76 @@ class ReleaseIntegrationTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertLess(artifact_check, core_tests)
|
self.assertLess(artifact_check, core_tests)
|
||||||
self.assertLess(core_tests, module_tests)
|
self.assertLess(core_tests, module_tests)
|
||||||
|
self.assertNotIn("unittest discover", script)
|
||||||
self.assertNotIn('"$PYTHON" -m unittest \\\n tests.test_module_system', script)
|
self.assertNotIn('"$PYTHON" -m unittest \\\n tests.test_module_system', script)
|
||||||
|
|
||||||
|
def test_tagged_module_runner_executes_pytest_functions_from_module_root(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-release-module-tests-") as temp_dir:
|
||||||
|
module_root = Path(temp_dir)
|
||||||
|
tests_root = module_root / "tests"
|
||||||
|
tests_root.mkdir()
|
||||||
|
marker = module_root / "pytest-function-ran"
|
||||||
|
(tests_root / "test_probe.py").write_text(
|
||||||
|
"""from pathlib import Path
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def test_pytest_style_function():
|
||||||
|
expected_root = Path(os.environ[\"GOVOPLAN_TEST_MODULE_ROOT\"])
|
||||||
|
assert Path.cwd() == expected_root
|
||||||
|
Path(os.environ[\"GOVOPLAN_TEST_MARKER\"]).write_text(\"ran\", encoding=\"utf-8\")
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"GOVOPLAN_TEST_MODULE_ROOT": str(module_root),
|
||||||
|
"GOVOPLAN_TEST_MARKER": str(marker),
|
||||||
|
},
|
||||||
|
):
|
||||||
|
status = run_module_release_tests(module_root)
|
||||||
|
|
||||||
|
self.assertEqual(status, 0)
|
||||||
|
self.assertEqual(marker.read_text(encoding="utf-8"), "ran")
|
||||||
|
|
||||||
|
def test_tagged_module_runner_skips_missing_test_tree(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-release-module-tests-") as temp_dir:
|
||||||
|
self.assertEqual(run_module_release_tests(Path(temp_dir)), 0)
|
||||||
|
|
||||||
def test_module_matrix_uses_artifact_aware_core_suite(self) -> None:
|
def test_module_matrix_uses_artifact_aware_core_suite(self) -> None:
|
||||||
meta_root = Path(__file__).resolve().parents[1]
|
meta_root = Path(__file__).resolve().parents[1]
|
||||||
script = (meta_root / "tools" / "checks" / "check-module-matrix.sh").read_text(encoding="utf-8")
|
script = (meta_root / "tools" / "checks" / "check-module-matrix.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
self.assertIn('"$META_ROOT/tools/checks/release_integration.py" core-tests', script)
|
artifact_check = script.index('"$META_ROOT/tools/checks/release_integration.py" artifacts')
|
||||||
|
core_tests = script.index('"$META_ROOT/tools/checks/release_integration.py" core-tests')
|
||||||
|
|
||||||
|
self.assertLess(artifact_check, core_tests)
|
||||||
|
self.assertIn('--requirements "$META_ROOT/requirements-release.txt"', script)
|
||||||
self.assertIn('--core-root "$ROOT"', script)
|
self.assertIn('--core-root "$ROOT"', script)
|
||||||
self.assertNotIn('"$PYTHON" -m unittest tests.test_module_system', script)
|
self.assertNotIn('"$PYTHON" -m unittest tests.test_module_system', script)
|
||||||
|
|
||||||
|
def test_module_matrix_installs_search_before_postgres_validation(self) -> None:
|
||||||
|
meta_root = Path(__file__).resolve().parents[1]
|
||||||
|
workflow = (meta_root / ".gitea" / "workflows" / "module-matrix.yml").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
install = workflow.index("pip install --no-deps ../govoplan-search")
|
||||||
|
validation = workflow.index("Validate Search against PostgreSQL")
|
||||||
|
|
||||||
|
self.assertLess(install, validation)
|
||||||
|
|
||||||
|
def test_release_workflow_installs_tagged_source_test_harness(self) -> None:
|
||||||
|
meta_root = Path(__file__).resolve().parents[1]
|
||||||
|
workflow = (meta_root / ".gitea" / "workflows" / "release-integration.yml").read_text(encoding="utf-8")
|
||||||
|
requirements = (meta_root / "requirements-release-tests.txt").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
install = workflow.index("pip install -r requirements-release-tests.txt")
|
||||||
|
checks = workflow.index("bash tools/checks/check-release-integration.sh")
|
||||||
|
|
||||||
|
self.assertLess(install, checks)
|
||||||
|
self.assertIn("pytest>=9.0.3,<10", requirements)
|
||||||
|
self.assertNotIn("requirements-release-tests.txt", (meta_root / "requirements-release.txt").read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -47,6 +47,38 @@ branch_labels: Union[str, Sequence[str], None] = None
|
|||||||
self.assertEqual(migration.depends_on, ("core",))
|
self.assertEqual(migration.depends_on, ("core",))
|
||||||
self.assertEqual(migration.branch_labels, ())
|
self.assertEqual(migration.branch_labels, ())
|
||||||
|
|
||||||
|
def test_development_wrapper_uses_release_revision_metadata(self) -> None:
|
||||||
|
audit = load_audit_module()
|
||||||
|
with tempfile.TemporaryDirectory(prefix="migration-audit-test-") as directory:
|
||||||
|
root = Path(directory)
|
||||||
|
versions = root / "versions"
|
||||||
|
development = root / "dev_versions"
|
||||||
|
versions.mkdir()
|
||||||
|
development.mkdir()
|
||||||
|
release = versions / "1234_example.py"
|
||||||
|
release.write_text(
|
||||||
|
'revision = "1234"\n'
|
||||||
|
'down_revision = "base"\n'
|
||||||
|
'depends_on = "core"\n'
|
||||||
|
"branch_labels = None\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
wrapper = development / release.name
|
||||||
|
wrapper.write_text(
|
||||||
|
"revision = _migration.revision\n"
|
||||||
|
"down_revision = _migration.down_revision\n"
|
||||||
|
"depends_on = _migration.depends_on\n"
|
||||||
|
"branch_labels = _migration.branch_labels\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
migration = audit.parse_migration_file("govoplan-core", wrapper)
|
||||||
|
|
||||||
|
self.assertIsNotNone(migration)
|
||||||
|
self.assertEqual("1234", migration.revision)
|
||||||
|
self.assertEqual(("base",), migration.down_revisions)
|
||||||
|
self.assertEqual(("core",), migration.depends_on)
|
||||||
|
|
||||||
def test_release_baseline_matches_current_heads_in_strict_report(self) -> None:
|
def test_release_baseline_matches_current_heads_in_strict_report(self) -> None:
|
||||||
audit = load_audit_module()
|
audit = load_audit_module()
|
||||||
migrations = [
|
migrations = [
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from govoplan_release.selective_planner import build_selective_release_plan # n
|
|||||||
|
|
||||||
|
|
||||||
class ReleasePlanGuidanceTests(unittest.TestCase):
|
class ReleasePlanGuidanceTests(unittest.TestCase):
|
||||||
def test_unprepared_target_has_structured_remediation_and_recommendation(
|
def test_unprepared_target_gets_bounded_version_and_commit_steps(
|
||||||
self,
|
self,
|
||||||
) -> None:
|
) -> None:
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
@@ -40,20 +40,25 @@ class ReleasePlanGuidanceTests(unittest.TestCase):
|
|||||||
repo_versions={"govoplan-files": "1.2.4"},
|
repo_versions={"govoplan-files": "1.2.4"},
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual("blocked", plan.status)
|
self.assertEqual("attention", plan.status)
|
||||||
self.assertFalse(plan.source_preflight_ready)
|
self.assertTrue(plan.source_preflight_ready)
|
||||||
finding = next(
|
self.assertFalse(
|
||||||
item
|
any(
|
||||||
for item in plan.gate_findings
|
item.code == "repository_version_alignment"
|
||||||
if item.code == "repository_version_alignment"
|
for item in plan.gate_findings
|
||||||
|
)
|
||||||
)
|
)
|
||||||
self.assertEqual("govoplan-files", finding.repo)
|
steps = {step.id: step for step in plan.dry_run_steps}
|
||||||
self.assertEqual("pyproject.toml", finding.source)
|
self.assertEqual("planned", steps["govoplan-files:version"].status)
|
||||||
self.assertEqual("1.2.4", finding.expected)
|
self.assertEqual("planned", steps["govoplan-files:commit"].status)
|
||||||
self.assertEqual("1.2.3", finding.actual)
|
self.assertIn("pyproject.toml", steps["govoplan-files:version"].detail)
|
||||||
self.assertIn("Regenerate lockfiles", finding.remediation)
|
self.assertTrue(
|
||||||
self.assertEqual("resolve_release_gate", plan.recommended_action.id)
|
any(
|
||||||
self.assertEqual("govoplan-files", plan.recommended_action.repo)
|
"version metadata will be updated" in warning
|
||||||
|
for warning in plan.units[0].warnings
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertIn("python-package", plan.units[0].capabilities)
|
||||||
|
|
||||||
def test_aligned_target_recommends_non_mutating_tag_preview(self) -> None:
|
def test_aligned_target_recommends_non_mutating_tag_preview(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as temp_dir:
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
@@ -74,6 +79,91 @@ class ReleasePlanGuidanceTests(unittest.TestCase):
|
|||||||
self.assertEqual("preview_source_release", plan.recommended_action.id)
|
self.assertEqual("preview_source_release", plan.recommended_action.id)
|
||||||
self.assertIn("Preview Tag + Publish", plan.recommended_action.remediation)
|
self.assertIn("Preview Tag + Publish", plan.recommended_action.remediation)
|
||||||
|
|
||||||
|
def test_mixed_plan_tags_modules_before_core_and_aligns_before_push(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
workspace = Path(temp_dir)
|
||||||
|
for repo_name in ("govoplan-core", "govoplan-files"):
|
||||||
|
repo_path = workspace / repo_name
|
||||||
|
repo_path.mkdir()
|
||||||
|
(repo_path / "pyproject.toml").write_text(
|
||||||
|
f'[project]\nname = "{repo_name}"\nversion = "1.2.3"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
repositories = tuple(
|
||||||
|
RepositorySnapshot(
|
||||||
|
spec=RepositorySpec(
|
||||||
|
name=repo_name,
|
||||||
|
category="core" if repo_name == "govoplan-core" else "module",
|
||||||
|
subtype="platform" if repo_name == "govoplan-core" else "infrastructure",
|
||||||
|
remote=f"git@example.test:GovOPlaN/{repo_name}.git",
|
||||||
|
path=repo_name,
|
||||||
|
),
|
||||||
|
absolute_path=str(workspace / repo_name),
|
||||||
|
exists=True,
|
||||||
|
is_git=True,
|
||||||
|
has_head=True,
|
||||||
|
branch="main",
|
||||||
|
versions=VersionSnapshot(pyproject="1.2.3"),
|
||||||
|
local_target_tag_exists=False,
|
||||||
|
)
|
||||||
|
for repo_name in ("govoplan-core", "govoplan-files")
|
||||||
|
)
|
||||||
|
base = dashboard(workspace=workspace, version="1.2.3")
|
||||||
|
mixed = ReleaseDashboard(
|
||||||
|
generated_at=base.generated_at,
|
||||||
|
meta_root=base.meta_root,
|
||||||
|
workspace_root=base.workspace_root,
|
||||||
|
target_version=None,
|
||||||
|
target_tag=None,
|
||||||
|
online=False,
|
||||||
|
include_migrations=False,
|
||||||
|
summary=DashboardSummary(
|
||||||
|
repository_count=2,
|
||||||
|
missing_count=0,
|
||||||
|
dirty_count=0,
|
||||||
|
ahead_count=0,
|
||||||
|
behind_count=0,
|
||||||
|
no_head_count=0,
|
||||||
|
error_count=0,
|
||||||
|
safe_directory_count=0,
|
||||||
|
local_target_tag_missing_count=0,
|
||||||
|
status="ready",
|
||||||
|
),
|
||||||
|
repositories=repositories,
|
||||||
|
catalog=base.catalog,
|
||||||
|
)
|
||||||
|
plan = build_selective_release_plan(
|
||||||
|
mixed,
|
||||||
|
selected_repos=("govoplan-core", "govoplan-files"),
|
||||||
|
repo_versions={
|
||||||
|
"govoplan-core": "1.2.3",
|
||||||
|
"govoplan-files": "1.2.3",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
["govoplan-files", "govoplan-core"],
|
||||||
|
[unit.repo for unit in plan.units],
|
||||||
|
)
|
||||||
|
step_ids = [step.id for step in plan.dry_run_steps]
|
||||||
|
self.assertLess(
|
||||||
|
step_ids.index("govoplan-files:tag"),
|
||||||
|
step_ids.index("govoplan-core:tag"),
|
||||||
|
)
|
||||||
|
self.assertLess(
|
||||||
|
step_ids.index("govoplan-core:tag"),
|
||||||
|
step_ids.index("release:alignment"),
|
||||||
|
)
|
||||||
|
self.assertLess(
|
||||||
|
step_ids.index("release:alignment"),
|
||||||
|
step_ids.index("govoplan-files:push"),
|
||||||
|
)
|
||||||
|
self.assertEqual("release:install-verify", step_ids[-1])
|
||||||
|
core = next(unit for unit in plan.units if unit.repo == "govoplan-core")
|
||||||
|
self.assertIn("core-release-bundle", core.capabilities)
|
||||||
|
|
||||||
def test_malformed_version_metadata_is_a_structured_blocker(self) -> None:
|
def test_malformed_version_metadata_is_a_structured_blocker(self) -> None:
|
||||||
cases = (
|
cases = (
|
||||||
("package.json", "{not-json\n", "JSONDecodeError"),
|
("package.json", "{not-json\n", "JSONDecodeError"),
|
||||||
@@ -149,6 +239,29 @@ class ReleasePlanGuidanceTests(unittest.TestCase):
|
|||||||
self.assertIn('pill("recommended next", recommendationKind)', webui)
|
self.assertIn('pill("recommended next", recommendationKind)', webui)
|
||||||
self.assertIn("<strong>Remediation:</strong>", webui)
|
self.assertIn("<strong>Remediation:</strong>", webui)
|
||||||
|
|
||||||
|
def test_webui_projects_release_state_into_a_guided_workflow(self) -> None:
|
||||||
|
webui = (RELEASE_ROOT / "webui" / "index.html").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
for phase in (
|
||||||
|
"Inspect",
|
||||||
|
"Targets",
|
||||||
|
"Validate",
|
||||||
|
"Source",
|
||||||
|
"Package",
|
||||||
|
"Publish",
|
||||||
|
"Verify",
|
||||||
|
):
|
||||||
|
self.assertIn(f'label: "{phase}"', webui)
|
||||||
|
self.assertIn("releaseWorkflowPhases", webui)
|
||||||
|
self.assertIn("workflowPrimaryAction", webui)
|
||||||
|
self.assertIn("data-run-step-id", webui)
|
||||||
|
self.assertIn("invalidateReleaseDraft", webui)
|
||||||
|
self.assertIn("Installation Verification", webui)
|
||||||
|
self.assertIn("receipt-bound local gate", webui)
|
||||||
|
self.assertIn('step.id === "release:install-verify"', webui)
|
||||||
|
self.assertIn('const inspectionNotices = (summary.missing_count || 0)', webui)
|
||||||
|
self.assertIn('(summary.repository_count || 0) === 0', webui)
|
||||||
|
|
||||||
|
|
||||||
def dashboard(*, workspace: Path, version: str) -> ReleaseDashboard:
|
def dashboard(*, workspace: Path, version: str) -> ReleaseDashboard:
|
||||||
repo = RepositorySnapshot(
|
repo = RepositorySnapshot(
|
||||||
@@ -156,7 +269,7 @@ def dashboard(*, workspace: Path, version: str) -> ReleaseDashboard:
|
|||||||
name="govoplan-files",
|
name="govoplan-files",
|
||||||
category="module",
|
category="module",
|
||||||
subtype="infrastructure",
|
subtype="infrastructure",
|
||||||
remote="git@example.test:add-ideas/govoplan-files.git",
|
remote="git@example.test:GovOPlaN/govoplan-files.git",
|
||||||
path="govoplan-files",
|
path="govoplan-files",
|
||||||
),
|
),
|
||||||
absolute_path=str(workspace / "govoplan-files"),
|
absolute_path=str(workspace / "govoplan-files"),
|
||||||
|
|||||||
@@ -340,6 +340,7 @@ def add_scoped_workflow_manifest(repo: Path) -> None:
|
|||||||
(backend / "__init__.py").write_text("", encoding="utf-8")
|
(backend / "__init__.py").write_text("", encoding="utf-8")
|
||||||
(backend / "manifest.py").write_text(
|
(backend / "manifest.py").write_text(
|
||||||
"""from govoplan_core.core.modules import DocumentationCondition, DocumentationTopic, ModuleManifest, PermissionDefinition
|
"""from govoplan_core.core.modules import DocumentationCondition, DocumentationTopic, ModuleManifest, PermissionDefinition
|
||||||
|
from govoplan_core.core.provider_governance import declared_module_architecture
|
||||||
|
|
||||||
|
|
||||||
def get_manifest():
|
def get_manifest():
|
||||||
@@ -368,6 +369,20 @@ def get_manifest():
|
|||||||
conditions=(DocumentationCondition(required_scopes=("access:item:read",)),),
|
conditions=(DocumentationCondition(required_scopes=("access:item:read",)),),
|
||||||
metadata={"kind": "workflow"},
|
metadata={"kind": "workflow"},
|
||||||
),
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="access.admin.reference",
|
||||||
|
title="Administer access",
|
||||||
|
summary="Static administrator documentation for the release fixture.",
|
||||||
|
documentation_types=("admin",),
|
||||||
|
metadata={"kind": "reference"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
architecture=declared_module_architecture(
|
||||||
|
layer="institutional_foundation",
|
||||||
|
kind="foundation",
|
||||||
|
maturity="scaffold",
|
||||||
|
documentation_ref="pyproject.toml",
|
||||||
|
known_limits=("Release-test fixture only.",),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
""",
|
""",
|
||||||
@@ -387,6 +402,7 @@ def replace_with_unscoped_workflow_manifest(repo: Path) -> None:
|
|||||||
manifest = repo / "src" / "govoplan_access" / "backend" / "manifest.py"
|
manifest = repo / "src" / "govoplan_access" / "backend" / "manifest.py"
|
||||||
manifest.write_text(
|
manifest.write_text(
|
||||||
"""from govoplan_core.core.modules import DocumentationTopic, ModuleManifest
|
"""from govoplan_core.core.modules import DocumentationTopic, ModuleManifest
|
||||||
|
from govoplan_core.core.provider_governance import declared_module_architecture
|
||||||
|
|
||||||
|
|
||||||
def get_manifest():
|
def get_manifest():
|
||||||
@@ -402,6 +418,20 @@ def get_manifest():
|
|||||||
documentation_types=("user",),
|
documentation_types=("user",),
|
||||||
metadata={"kind": "workflow"},
|
metadata={"kind": "workflow"},
|
||||||
),
|
),
|
||||||
|
DocumentationTopic(
|
||||||
|
id="access.admin.reference",
|
||||||
|
title="Administer access",
|
||||||
|
summary="Static administrator documentation for the release fixture.",
|
||||||
|
documentation_types=("admin",),
|
||||||
|
metadata={"kind": "reference"},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
architecture=declared_module_architecture(
|
||||||
|
layer="institutional_foundation",
|
||||||
|
kind="foundation",
|
||||||
|
maturity="scaffold",
|
||||||
|
documentation_ref="pyproject.toml",
|
||||||
|
known_limits=("Release-test fixture only.",),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
""",
|
""",
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
RELEASE_ROOT = META_ROOT / "tools" / "release"
|
||||||
|
if str(RELEASE_ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(RELEASE_ROOT))
|
||||||
|
|
||||||
|
from govoplan_release.version_metadata import ( # noqa: E402
|
||||||
|
apply_version_metadata_mutations,
|
||||||
|
version_metadata_mutations,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ReleaseVersionMetadataTests(unittest.TestCase):
|
||||||
|
def test_updates_recognized_metadata_without_changing_interface_versions(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
root = Path(temp_dir)
|
||||||
|
package = root / "src" / "govoplan_example"
|
||||||
|
backend = package / "backend"
|
||||||
|
webui = root / "webui"
|
||||||
|
backend.mkdir(parents=True)
|
||||||
|
webui.mkdir()
|
||||||
|
(root / "pyproject.toml").write_text(
|
||||||
|
'[project]\nname = "govoplan-example"\nversion = "1.2.3"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(root / "package.json").write_text(
|
||||||
|
'{"name":"root","version":"1.2.3"}\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(webui / "package.json").write_text(
|
||||||
|
'{"name":"@govoplan/example-webui","version":"1.2.3"}\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(webui / "package-lock.json").write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"name": "@govoplan/example-webui",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "@govoplan/example-webui",
|
||||||
|
"version": "1.2.3",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
+ "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(backend / "manifest.py").write_text(
|
||||||
|
"def get_manifest():\n"
|
||||||
|
" return ModuleManifest(\n"
|
||||||
|
' id="example",\n'
|
||||||
|
' version="1.2.3",\n'
|
||||||
|
" provides_interfaces=(\n"
|
||||||
|
' ModuleInterfaceProvider(name="example.api", version="4.0"),\n'
|
||||||
|
" ),\n"
|
||||||
|
" )\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
(package / "__init__.py").write_text(
|
||||||
|
'__version__ = "1.2.3"\n',
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
preview = version_metadata_mutations(
|
||||||
|
root,
|
||||||
|
target_version="1.2.4",
|
||||||
|
)
|
||||||
|
changed = apply_version_metadata_mutations(
|
||||||
|
root,
|
||||||
|
target_version="1.2.4",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
{mutation.path for mutation in preview},
|
||||||
|
set(changed),
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"1.2.4",
|
||||||
|
json.loads((webui / "package-lock.json").read_text())[
|
||||||
|
"packages"
|
||||||
|
][""]["version"],
|
||||||
|
)
|
||||||
|
manifest = (backend / "manifest.py").read_text(encoding="utf-8")
|
||||||
|
self.assertIn('version="1.2.4"', manifest)
|
||||||
|
self.assertIn('version="4.0"', manifest)
|
||||||
|
self.assertEqual(
|
||||||
|
'__version__ = "1.2.4"\n',
|
||||||
|
(package / "__init__.py").read_text(encoding="utf-8"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -28,23 +28,23 @@ class RepositoryBootstrapTests(unittest.TestCase):
|
|||||||
bootstrap = load_bootstrap_module()
|
bootstrap = load_bootstrap_module()
|
||||||
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"https://git.add-ideas.de/add-ideas/govoplan-core.git",
|
"https://git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||||
bootstrap.clone_remote(
|
bootstrap.clone_remote(
|
||||||
"git@git.add-ideas.de:add-ideas/govoplan-core.git",
|
"git@git.add-ideas.de:GovOPlaN/govoplan-core.git",
|
||||||
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"https://git.add-ideas.de/add-ideas/govoplan-core.git",
|
"https://git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||||
bootstrap.clone_remote(
|
bootstrap.clone_remote(
|
||||||
"ssh://git@git.add-ideas.de/add-ideas/govoplan-core.git",
|
"ssh://git@git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||||
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"https://git.add-ideas.de/add-ideas/govoplan-core.git",
|
"https://git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||||
bootstrap.clone_remote(
|
bootstrap.clone_remote(
|
||||||
"https://git.add-ideas.de/add-ideas/govoplan-core.git",
|
"https://git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||||
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -65,12 +65,12 @@ class RepositoryBootstrapTests(unittest.TestCase):
|
|||||||
bootstrap = load_bootstrap_module()
|
bootstrap = load_bootstrap_module()
|
||||||
|
|
||||||
unsafe_remotes = (
|
unsafe_remotes = (
|
||||||
"git@example.test:add-ideas/govoplan-core.git",
|
"git@example.test:GovOPlaN/govoplan-core.git",
|
||||||
"https://token@git.add-ideas.de/add-ideas/govoplan-core.git",
|
"https://token@git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||||
"https://git.add-ideas.de:443/add-ideas/govoplan-core.git",
|
"https://git.add-ideas.de:443/GovOPlaN/govoplan-core.git",
|
||||||
"https://git.add-ideas.de/add-ideas/../govoplan-core.git",
|
"https://git.add-ideas.de/add-ideas/../govoplan-core.git",
|
||||||
"https://git.add-ideas.de/add-ideas/govoplan-core.git?ref=main",
|
"https://git.add-ideas.de/GovOPlaN/govoplan-core.git?ref=main",
|
||||||
"ssh://root@git.add-ideas.de/add-ideas/govoplan-core.git",
|
"ssh://root@git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||||
)
|
)
|
||||||
for remote in unsafe_remotes:
|
for remote in unsafe_remotes:
|
||||||
with self.subTest(remote=remote), self.assertRaises(ValueError):
|
with self.subTest(remote=remote), self.assertRaises(ValueError):
|
||||||
@@ -94,8 +94,7 @@ class RepositoryBootstrapTests(unittest.TestCase):
|
|||||||
"name": "govoplan-core",
|
"name": "govoplan-core",
|
||||||
"path": "govoplan-core",
|
"path": "govoplan-core",
|
||||||
"remote": (
|
"remote": (
|
||||||
"git@git.add-ideas.de:"
|
"git@git.add-ideas.de:GovOPlaN/govoplan-core.git"
|
||||||
"add-ideas/govoplan-core.git"
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -124,7 +123,7 @@ class RepositoryBootstrapTests(unittest.TestCase):
|
|||||||
"credential.helper=",
|
"credential.helper=",
|
||||||
"clone",
|
"clone",
|
||||||
"--",
|
"--",
|
||||||
"https://git.add-ideas.de/add-ideas/govoplan-core.git",
|
"https://git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||||
str(parent / "govoplan-core"),
|
str(parent / "govoplan-core"),
|
||||||
],
|
],
|
||||||
check=True,
|
check=True,
|
||||||
@@ -134,6 +133,66 @@ class RepositoryBootstrapTests(unittest.TestCase):
|
|||||||
self.assertEqual("/bin/false", environment["GIT_ASKPASS"])
|
self.assertEqual("/bin/false", environment["GIT_ASKPASS"])
|
||||||
self.assertEqual("0", environment["GIT_TERMINAL_PROMPT"])
|
self.assertEqual("0", environment["GIT_TERMINAL_PROMPT"])
|
||||||
|
|
||||||
|
def test_checkout_auth_is_forwarded_without_entering_clone_arguments(self) -> None:
|
||||||
|
bootstrap = load_bootstrap_module()
|
||||||
|
environment = {
|
||||||
|
"GIT_CONFIG_COUNT": "1",
|
||||||
|
"GIT_CONFIG_KEY_0": "url.https://example.test/.insteadOf",
|
||||||
|
"GIT_CONFIG_VALUE_0": "ssh://example.test/",
|
||||||
|
}
|
||||||
|
auth_header = "AUTHORIZATION: basic c2hvcnQtbGl2ZWQtam9iLXRva2Vu"
|
||||||
|
|
||||||
|
with patch.object(
|
||||||
|
bootstrap.subprocess,
|
||||||
|
"run",
|
||||||
|
return_value=bootstrap.subprocess.CompletedProcess(
|
||||||
|
args=[],
|
||||||
|
returncode=0,
|
||||||
|
stdout=auth_header + "\n",
|
||||||
|
),
|
||||||
|
) as runner:
|
||||||
|
bootstrap._add_checkout_auth(environment, root=Path("/workspace/meta"))
|
||||||
|
|
||||||
|
runner.assert_called_once_with(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"-C",
|
||||||
|
"/workspace/meta",
|
||||||
|
"config",
|
||||||
|
"--local",
|
||||||
|
"--get",
|
||||||
|
bootstrap.GITEA_CHECKOUT_AUTH_KEY,
|
||||||
|
],
|
||||||
|
check=False,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
self.assertEqual("2", environment["GIT_CONFIG_COUNT"])
|
||||||
|
self.assertEqual(
|
||||||
|
bootstrap.GITEA_CHECKOUT_AUTH_KEY,
|
||||||
|
environment["GIT_CONFIG_KEY_1"],
|
||||||
|
)
|
||||||
|
self.assertEqual(auth_header, environment["GIT_CONFIG_VALUE_1"])
|
||||||
|
|
||||||
|
def test_checkout_auth_fails_closed_when_checkout_did_not_persist_it(self) -> None:
|
||||||
|
bootstrap = load_bootstrap_module()
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
bootstrap.subprocess,
|
||||||
|
"run",
|
||||||
|
return_value=bootstrap.subprocess.CompletedProcess(
|
||||||
|
args=[],
|
||||||
|
returncode=1,
|
||||||
|
stdout="",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
self.assertRaisesRegex(
|
||||||
|
ValueError, "checkout authentication is unavailable"
|
||||||
|
),
|
||||||
|
):
|
||||||
|
bootstrap._add_checkout_auth({}, root=Path("/workspace/meta"))
|
||||||
|
|
||||||
def test_main_validates_every_remote_before_cloning(self) -> None:
|
def test_main_validates_every_remote_before_cloning(self) -> None:
|
||||||
bootstrap = load_bootstrap_module()
|
bootstrap = load_bootstrap_module()
|
||||||
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
||||||
@@ -149,8 +208,7 @@ class RepositoryBootstrapTests(unittest.TestCase):
|
|||||||
"name": "govoplan-core",
|
"name": "govoplan-core",
|
||||||
"path": "govoplan-core",
|
"path": "govoplan-core",
|
||||||
"remote": (
|
"remote": (
|
||||||
"git@git.add-ideas.de:"
|
"git@git.add-ideas.de:GovOPlaN/govoplan-core.git"
|
||||||
"add-ideas/govoplan-core.git"
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -197,9 +255,7 @@ class RepositoryBootstrapTests(unittest.TestCase):
|
|||||||
"git@git.add-ideas.de:"
|
"git@git.add-ideas.de:"
|
||||||
"add-ideas/addideas-govoplan-website.git"
|
"add-ideas/addideas-govoplan-website.git"
|
||||||
),
|
),
|
||||||
"bootstrap_transport": (
|
"bootstrap_transport": (bootstrap.REGISTERED_TRANSPORT),
|
||||||
bootstrap.REGISTERED_TRANSPORT
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -240,10 +296,7 @@ class RepositoryBootstrapTests(unittest.TestCase):
|
|||||||
{
|
{
|
||||||
"name": name,
|
"name": name,
|
||||||
"path": name,
|
"path": name,
|
||||||
"remote": (
|
"remote": (f"git@git.add-ideas.de:GovOPlaN/{name}.git"),
|
||||||
"git@git.add-ideas.de:add-ideas/"
|
|
||||||
f"{name}.git"
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
for name in ("govoplan-core", "govoplan-poll")
|
for name in ("govoplan-core", "govoplan-poll")
|
||||||
],
|
],
|
||||||
@@ -272,7 +325,7 @@ class RepositoryBootstrapTests(unittest.TestCase):
|
|||||||
self.assertEqual(0, status)
|
self.assertEqual(0, status)
|
||||||
runner.assert_called_once()
|
runner.assert_called_once()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
"https://git.add-ideas.de/add-ideas/govoplan-core.git",
|
"https://git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||||
runner.call_args.args[0][-2],
|
runner.call_args.args[0][-2],
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -291,8 +344,7 @@ class RepositoryBootstrapTests(unittest.TestCase):
|
|||||||
"name": "govoplan-core",
|
"name": "govoplan-core",
|
||||||
"path": "govoplan-core",
|
"path": "govoplan-core",
|
||||||
"remote": (
|
"remote": (
|
||||||
"git@git.add-ideas.de:"
|
"git@git.add-ideas.de:GovOPlaN/govoplan-core.git"
|
||||||
"add-ideas/govoplan-core.git"
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -333,8 +385,7 @@ class RepositoryBootstrapTests(unittest.TestCase):
|
|||||||
"name": "govoplan-core",
|
"name": "govoplan-core",
|
||||||
"path": "govoplan-core",
|
"path": "govoplan-core",
|
||||||
"remote": (
|
"remote": (
|
||||||
"git@git.add-ideas.de:"
|
"git@git.add-ideas.de:GovOPlaN/govoplan-core.git"
|
||||||
"add-ideas/govoplan-core.git"
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -359,6 +410,34 @@ class RepositoryBootstrapTests(unittest.TestCase):
|
|||||||
self.assertEqual(1, status)
|
self.assertEqual(1, status)
|
||||||
runner.assert_not_called()
|
runner.assert_not_called()
|
||||||
|
|
||||||
|
def test_ci_bootstrap_reuses_checkout_auth_and_excludes_registered_transport_repositories(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
manifest = json.loads(
|
||||||
|
(META_ROOT / "repositories.json").read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
registered_only = {
|
||||||
|
entry["name"]
|
||||||
|
for entry in manifest["repositories"]
|
||||||
|
if entry.get("bootstrap_transport") == "registered"
|
||||||
|
}
|
||||||
|
self.assertTrue(registered_only)
|
||||||
|
|
||||||
|
for workflow in sorted((META_ROOT / ".gitea" / "workflows").glob("*.yml")):
|
||||||
|
contents = workflow.read_text(encoding="utf-8")
|
||||||
|
if (
|
||||||
|
"bootstrap-repositories.py" not in contents
|
||||||
|
or "--transport public-https" not in contents
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
with self.subTest(workflow=workflow.name):
|
||||||
|
self.assertIn("--reuse-checkout-auth", contents)
|
||||||
|
for repository in registered_only:
|
||||||
|
self.assertIn(
|
||||||
|
f"--exclude-repo {repository}",
|
||||||
|
contents,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from datetime import UTC, datetime, timedelta
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives import serialization
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
||||||
|
|
||||||
|
|
||||||
|
META_ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(META_ROOT / "tools" / "deployment"))
|
||||||
|
sys.path.insert(0, str(META_ROOT / "tools" / "release"))
|
||||||
|
|
||||||
|
from govoplan_deploy.distribution import ( # noqa: E402
|
||||||
|
DistributionError,
|
||||||
|
canonical_signed_payload,
|
||||||
|
verify_manifest,
|
||||||
|
verify_manifest_binding,
|
||||||
|
verify_offline_image_index,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RuntimeDistributionTests(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.now = datetime(2026, 8, 3, tzinfo=UTC)
|
||||||
|
self.private = Ed25519PrivateKey.generate()
|
||||||
|
public = self.private.public_key().public_bytes(
|
||||||
|
serialization.Encoding.PEM,
|
||||||
|
serialization.PublicFormat.SubjectPublicKeyInfo,
|
||||||
|
).decode("ascii")
|
||||||
|
self.keyring = {
|
||||||
|
"schema_version": "1",
|
||||||
|
"purpose": "govoplan-runtime-distribution",
|
||||||
|
"keys": [
|
||||||
|
{
|
||||||
|
"key_id": "release-1",
|
||||||
|
"algorithm": "ed25519",
|
||||||
|
"status": "active",
|
||||||
|
"public_key_pem": public,
|
||||||
|
"not_before": (self.now - timedelta(days=1)).isoformat(),
|
||||||
|
"expires_at": (self.now + timedelta(days=365)).isoformat(),
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_verifies_signature_and_exact_runtime_binding(self) -> None:
|
||||||
|
payload = self._manifest()
|
||||||
|
|
||||||
|
key_id = verify_manifest(
|
||||||
|
payload,
|
||||||
|
self.keyring,
|
||||||
|
expected_channel="stable",
|
||||||
|
now=self.now,
|
||||||
|
)
|
||||||
|
verify_manifest_binding(
|
||||||
|
payload,
|
||||||
|
channel="stable",
|
||||||
|
version="1.2.3",
|
||||||
|
api_image=payload["images"]["api"]["index"],
|
||||||
|
web_image=payload["images"]["web"]["index"],
|
||||||
|
enabled_modules=("access", "files"),
|
||||||
|
composition_sha256="c" * 64,
|
||||||
|
dependencies=payload["dependencies"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual("release-1", key_id)
|
||||||
|
|
||||||
|
def test_tamper_expiry_revocation_and_unknown_key_fail_closed(self) -> None:
|
||||||
|
payload = self._manifest()
|
||||||
|
payload["composition"]["module_ids"].append("mail")
|
||||||
|
with self.assertRaisesRegex(DistributionError, "signature verification"):
|
||||||
|
verify_manifest(payload, self.keyring, now=self.now)
|
||||||
|
|
||||||
|
expired = self._manifest()
|
||||||
|
expired["expires_at"] = (self.now - timedelta(seconds=1)).isoformat()
|
||||||
|
expired["signatures"] = [self._signature(expired)]
|
||||||
|
with self.assertRaisesRegex(DistributionError, "expired"):
|
||||||
|
verify_manifest(expired, self.keyring, now=self.now)
|
||||||
|
|
||||||
|
revoked = self._manifest()
|
||||||
|
revoked["revoked"] = True
|
||||||
|
revoked["signatures"] = [self._signature(revoked)]
|
||||||
|
with self.assertRaisesRegex(DistributionError, "revoked"):
|
||||||
|
verify_manifest(revoked, self.keyring, now=self.now)
|
||||||
|
|
||||||
|
unknown = self._manifest()
|
||||||
|
unknown["signatures"][0]["key_id"] = "other-key"
|
||||||
|
with self.assertRaisesRegex(DistributionError, "active trusted key"):
|
||||||
|
verify_manifest(unknown, self.keyring, now=self.now)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(DistributionError, "expected 'candidate'"):
|
||||||
|
verify_manifest(
|
||||||
|
self._manifest(),
|
||||||
|
self.keyring,
|
||||||
|
expected_channel="candidate",
|
||||||
|
now=self.now,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_offline_image_index_is_complete_and_digest_bound(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-offline-images-") as value:
|
||||||
|
root = Path(value)
|
||||||
|
api = root / "api.oci.tar"
|
||||||
|
web = root / "web.oci.tar"
|
||||||
|
api.write_bytes(b"api archive")
|
||||||
|
web.write_bytes(b"web archive")
|
||||||
|
api_ref = "registry.example/govoplan/api@sha256:" + "a" * 64
|
||||||
|
web_ref = "registry.example/govoplan/web@sha256:" + "b" * 64
|
||||||
|
index = {
|
||||||
|
"schema_version": "1",
|
||||||
|
"images": [
|
||||||
|
{
|
||||||
|
"reference": api_ref,
|
||||||
|
"archive": api.name,
|
||||||
|
"sha256": hashlib.sha256(api.read_bytes()).hexdigest(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"reference": web_ref,
|
||||||
|
"archive": web.name,
|
||||||
|
"sha256": hashlib.sha256(web.read_bytes()).hexdigest(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
paths = verify_offline_image_index(
|
||||||
|
index,
|
||||||
|
root=root,
|
||||||
|
expected_references=(api_ref, web_ref),
|
||||||
|
)
|
||||||
|
self.assertEqual((api, web), paths)
|
||||||
|
|
||||||
|
index["images"][1]["sha256"] = "0" * 64
|
||||||
|
with self.assertRaisesRegex(DistributionError, "digest mismatch"):
|
||||||
|
verify_offline_image_index(
|
||||||
|
index,
|
||||||
|
root=root,
|
||||||
|
expected_references=(api_ref, web_ref),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _manifest(self) -> dict[str, object]:
|
||||||
|
artifact = {"url": "https://downloads.example.test/artifact.json", "sha256": "d" * 64}
|
||||||
|
manifest: dict[str, object] = {
|
||||||
|
"schema_version": "1",
|
||||||
|
"channel": "stable",
|
||||||
|
"sequence": 1,
|
||||||
|
"version": "1.2.3",
|
||||||
|
"issued_at": (self.now - timedelta(minutes=1)).isoformat(),
|
||||||
|
"expires_at": (self.now + timedelta(days=30)).isoformat(),
|
||||||
|
"revoked": False,
|
||||||
|
"deployer": {
|
||||||
|
"url": "https://downloads.example.test/govoplan-deploy.pyz",
|
||||||
|
"sha256": "e" * 64,
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"api": {
|
||||||
|
"index": "registry.example/govoplan/api@sha256:" + "a" * 64,
|
||||||
|
"platforms": {
|
||||||
|
"linux/amd64": "registry.example/govoplan/api@sha256:" + "1" * 64,
|
||||||
|
"linux/arm64": "registry.example/govoplan/api@sha256:" + "2" * 64,
|
||||||
|
},
|
||||||
|
"sbom": dict(artifact),
|
||||||
|
"provenance": dict(artifact),
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"index": "registry.example/govoplan/web@sha256:" + "b" * 64,
|
||||||
|
"platforms": {
|
||||||
|
"linux/amd64": "registry.example/govoplan/web@sha256:" + "3" * 64,
|
||||||
|
"linux/arm64": "registry.example/govoplan/web@sha256:" + "4" * 64,
|
||||||
|
},
|
||||||
|
"sbom": dict(artifact),
|
||||||
|
"provenance": dict(artifact),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"postgres": "docker.io/library/postgres@sha256:" + "5" * 64,
|
||||||
|
"redis": "docker.io/library/redis@sha256:" + "6" * 64,
|
||||||
|
"load_balancer": "docker.io/library/haproxy@sha256:" + "7" * 64,
|
||||||
|
},
|
||||||
|
"composition": {
|
||||||
|
"sha256": "c" * 64,
|
||||||
|
"module_ids": ["access", "files"],
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "govoplan-core",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"wheel_sha256": "8" * 64,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
manifest["signatures"] = [self._signature(manifest)]
|
||||||
|
return manifest
|
||||||
|
|
||||||
|
def _signature(self, payload: dict[str, object]) -> dict[str, str]:
|
||||||
|
return {
|
||||||
|
"key_id": "release-1",
|
||||||
|
"algorithm": "ed25519",
|
||||||
|
"value": base64.b64encode(
|
||||||
|
self.private.sign(canonical_signed_payload(payload))
|
||||||
|
).decode("ascii"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
from urllib.error import HTTPError
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def _load(name: str, path: Path):
|
||||||
|
spec = importlib.util.spec_from_file_location(name, path)
|
||||||
|
assert spec is not None and spec.loader is not None
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
sys.modules[name] = module
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
OCI = _load("resolve_oci_platforms", ROOT / "tools/release/resolve-oci-platforms.py")
|
||||||
|
FINALIZE = _load(
|
||||||
|
"finalize_runtime_distribution",
|
||||||
|
ROOT / "tools/release/finalize-runtime-distribution.py",
|
||||||
|
)
|
||||||
|
DEPLOYER_BUILD = _load(
|
||||||
|
"build_deployer_zipapp",
|
||||||
|
ROOT / "tools/deployment/build-deployer-zipapp.py",
|
||||||
|
)
|
||||||
|
PUBLISH = _load(
|
||||||
|
"publish_runtime_release",
|
||||||
|
ROOT / "tools/release/publish-runtime-release.py",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RuntimeDistributionBuildTests(unittest.TestCase):
|
||||||
|
def test_deployment_zipapp_is_reproducible_across_source_mtimes(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-reproducible-zipapp-") as value:
|
||||||
|
root = Path(value)
|
||||||
|
source = root / "source"
|
||||||
|
shutil.copytree(ROOT / "tools/deployment", source)
|
||||||
|
first = root / "first.pyz"
|
||||||
|
second = root / "second.pyz"
|
||||||
|
original_root = DEPLOYER_BUILD.ROOT
|
||||||
|
try:
|
||||||
|
DEPLOYER_BUILD.ROOT = source
|
||||||
|
self.assertEqual(0, DEPLOYER_BUILD.main(["--output", str(first)]))
|
||||||
|
for path in source.rglob("*.py"):
|
||||||
|
os.utime(path, (2_000_000_000, 2_000_000_000))
|
||||||
|
self.assertEqual(0, DEPLOYER_BUILD.main(["--output", str(second)]))
|
||||||
|
finally:
|
||||||
|
DEPLOYER_BUILD.ROOT = original_root
|
||||||
|
|
||||||
|
self.assertEqual(first.read_bytes(), second.read_bytes())
|
||||||
|
|
||||||
|
def test_workflow_signs_with_the_release_environment(self) -> None:
|
||||||
|
workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
".runtime-build/bin/python tools/release/generate-runtime-distribution.py",
|
||||||
|
workflow,
|
||||||
|
)
|
||||||
|
self.assertNotIn(
|
||||||
|
"\n python tools/release/generate-runtime-distribution.py",
|
||||||
|
workflow,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_workflow_rejects_missing_or_mutable_image_inputs_before_build(self) -> None:
|
||||||
|
workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
validation = workflow.index("- name: Validate immutable release inputs")
|
||||||
|
bootstrap = workflow.index("- name: Bootstrap release sources")
|
||||||
|
self.assertLess(validation, bootstrap)
|
||||||
|
self.assertIn('image_pattern = re.compile(r"^[^@\\s]+@sha256:', workflow)
|
||||||
|
for input_name in (
|
||||||
|
"python_image",
|
||||||
|
"nginx_image",
|
||||||
|
"postgres_image",
|
||||||
|
"redis_image",
|
||||||
|
"load_balancer_image",
|
||||||
|
"managed_ingress_image",
|
||||||
|
"garage_image",
|
||||||
|
"test_mail_image",
|
||||||
|
"binfmt_image",
|
||||||
|
):
|
||||||
|
self.assertIn(f"inputs.{input_name}", workflow)
|
||||||
|
|
||||||
|
def test_api_runtime_points_core_at_packaged_migration_scripts(self) -> None:
|
||||||
|
dockerfile = (ROOT / "tools/release/runtime/Dockerfile.api").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"GOVOPLAN_CORE_SOURCE_ROOT=/opt/govoplan/runtime/govoplan_core_runtime",
|
||||||
|
dockerfile,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_web_runtime_uses_only_writable_tmpfs_for_nginx_temp_files(self) -> None:
|
||||||
|
nginx = (ROOT / "tools/release/runtime/nginx.conf").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
for temporary_path in (
|
||||||
|
"client_body_temp_path /tmp/client_temp;",
|
||||||
|
"fastcgi_temp_path /tmp/fastcgi_temp;",
|
||||||
|
"proxy_temp_path /tmp/proxy_temp;",
|
||||||
|
"scgi_temp_path /tmp/scgi_temp;",
|
||||||
|
"uwsgi_temp_path /tmp/uwsgi_temp;",
|
||||||
|
):
|
||||||
|
self.assertIn(temporary_path, nginx)
|
||||||
|
|
||||||
|
def test_workflow_verifies_portable_bootstrap_artifacts_before_execution(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn(
|
||||||
|
"(cd runtime-output && sha256sum govoplan-deploy.pyz > "
|
||||||
|
"govoplan-deploy.pyz.sha256)",
|
||||||
|
workflow,
|
||||||
|
)
|
||||||
|
self.assertIn("openssl pkeyutl -verify -pubin", workflow)
|
||||||
|
self.assertIn("govoplan-deploy.tampered.pyz", workflow)
|
||||||
|
self.assertLess(
|
||||||
|
workflow.index("openssl pkeyutl -verify -pubin"),
|
||||||
|
workflow.index("python runtime-output/govoplan-deploy.pyz init"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_workflow_retains_both_platform_runtime_smoke_receipts(self) -> None:
|
||||||
|
workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn('for ARCH in amd64 arm64; do', workflow)
|
||||||
|
self.assertIn("tools/checks/runtime-image-smoke.py", workflow)
|
||||||
|
self.assertIn("Resolve managed dependency platform images", workflow)
|
||||||
|
self.assertIn("--postgres-metadata", workflow)
|
||||||
|
self.assertIn("--redis-metadata", workflow)
|
||||||
|
self.assertIn("Register arm64 execution for runtime smoke", workflow)
|
||||||
|
self.assertIn(
|
||||||
|
'docker run --privileged --rm "$BINFMT_IMAGE" --install arm64',
|
||||||
|
workflow,
|
||||||
|
)
|
||||||
|
self.assertIn("runtime-smoke-amd64.json", workflow)
|
||||||
|
self.assertIn("runtime-smoke-arm64.json", workflow)
|
||||||
|
self.assertIn(
|
||||||
|
"jq -r '.platforms[\"linux/amd64\"]' runtime-output/api-metadata.json",
|
||||||
|
workflow,
|
||||||
|
)
|
||||||
|
self.assertNotIn(".platforms[\\\"linux/amd64\\\"]", workflow)
|
||||||
|
|
||||||
|
def test_workflow_binds_the_release_tag_to_the_workflow_commit(self) -> None:
|
||||||
|
workflow = (ROOT / ".gitea/workflows/runtime-distribution.yml").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
publisher = (ROOT / "tools/release/publish-runtime-release.py").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertIn("SOURCE_COMMIT: ${{ gitea.sha }}", workflow)
|
||||||
|
self.assertIn('--target-commit "$SOURCE_COMMIT"', workflow)
|
||||||
|
self.assertIn('"target_commitish": target_commit', publisher)
|
||||||
|
self.assertIn("self._resolve_commit(tag) != target_commit", publisher)
|
||||||
|
|
||||||
|
def test_runtime_publisher_rejects_a_tag_on_another_commit(self) -> None:
|
||||||
|
publisher = PUBLISH.GiteaReleasePublisher(
|
||||||
|
base_url="https://git.example.test",
|
||||||
|
owner="GovOPlaN",
|
||||||
|
repo="govoplan",
|
||||||
|
token="secret",
|
||||||
|
)
|
||||||
|
target = "1" * 40
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
publisher,
|
||||||
|
"_resolve_commit",
|
||||||
|
side_effect=(target, "2" * 40),
|
||||||
|
),
|
||||||
|
self.assertRaisesRegex(PUBLISH.PublishError, "another commit"),
|
||||||
|
):
|
||||||
|
publisher.release(
|
||||||
|
tag="v1.2.3",
|
||||||
|
target_commit=target,
|
||||||
|
title="Release",
|
||||||
|
body="Body",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_runtime_publisher_creates_the_tag_at_the_exact_commit(self) -> None:
|
||||||
|
publisher = PUBLISH.GiteaReleasePublisher(
|
||||||
|
base_url="https://git.example.test",
|
||||||
|
owner="GovOPlaN",
|
||||||
|
repo="govoplan",
|
||||||
|
token="secret",
|
||||||
|
)
|
||||||
|
target = "1" * 40
|
||||||
|
requests: list[tuple[str, dict[str, object] | None]] = []
|
||||||
|
|
||||||
|
def request(method: str, _url: str, **kwargs):
|
||||||
|
payload = kwargs.get("payload")
|
||||||
|
requests.append((method, payload))
|
||||||
|
if method == "GET":
|
||||||
|
raise HTTPError(_url, 404, "not found", {}, None)
|
||||||
|
return {"id": 1}
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch.object(
|
||||||
|
publisher,
|
||||||
|
"_resolve_commit",
|
||||||
|
side_effect=(target, None, target),
|
||||||
|
),
|
||||||
|
patch.object(publisher, "_json", side_effect=request),
|
||||||
|
):
|
||||||
|
release = publisher.release(
|
||||||
|
tag="v1.2.3",
|
||||||
|
target_commit=target,
|
||||||
|
title="Release",
|
||||||
|
body="Body",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual({"id": 1}, release)
|
||||||
|
self.assertEqual("POST", requests[-1][0])
|
||||||
|
assert requests[-1][1] is not None
|
||||||
|
self.assertEqual(target, requests[-1][1]["target_commitish"])
|
||||||
|
|
||||||
|
def test_resolves_platforms_and_builds_evidence_descriptor(self) -> None:
|
||||||
|
index = {
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"manifests": [
|
||||||
|
{
|
||||||
|
"digest": "sha256:" + "1" * 64,
|
||||||
|
"platform": {"os": "linux", "architecture": "amd64"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"digest": "sha256:" + "2" * 64,
|
||||||
|
"platform": {"os": "linux", "architecture": "arm64"},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
metadata = OCI.resolve_platforms(
|
||||||
|
index,
|
||||||
|
repository="registry.example/govoplan/api",
|
||||||
|
index_digest="sha256:" + "a" * 64,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"registry.example/govoplan/api@sha256:" + "1" * 64,
|
||||||
|
metadata["platforms"]["linux/amd64"],
|
||||||
|
)
|
||||||
|
dependency_metadata = OCI.resolve_platforms(
|
||||||
|
index,
|
||||||
|
repository="registry.example:5000/library/postgres:16-alpine",
|
||||||
|
index_digest="sha256:" + "a" * 64,
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
"registry.example:5000/library/postgres@sha256:" + "2" * 64,
|
||||||
|
dependency_metadata["platforms"]["linux/arm64"],
|
||||||
|
)
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-runtime-finalize-") as value:
|
||||||
|
root = Path(value)
|
||||||
|
composition = {
|
||||||
|
"schema_version": "1",
|
||||||
|
"python": {
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"package": "govoplan-core",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"sha256": "8" * 64,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"module_ids": ["access"],
|
||||||
|
"wheelhouse_sha256": "9" * 64,
|
||||||
|
"wheel_count": 1,
|
||||||
|
},
|
||||||
|
"web": {"sha256": "7" * 64, "file_count": 4},
|
||||||
|
}
|
||||||
|
(root / "composition.json").write_text(json.dumps(composition))
|
||||||
|
(root / "api.json").write_text(json.dumps(metadata))
|
||||||
|
web_metadata = {
|
||||||
|
"index": "registry.example/govoplan/web@sha256:" + "b" * 64,
|
||||||
|
"platforms": {
|
||||||
|
"linux/amd64": "registry.example/govoplan/web@sha256:" + "3" * 64,
|
||||||
|
"linux/arm64": "registry.example/govoplan/web@sha256:" + "4" * 64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
(root / "web.json").write_text(json.dumps(web_metadata))
|
||||||
|
deployer = root / "govoplan-deploy.pyz"
|
||||||
|
deployer.write_bytes(b"zipapp")
|
||||||
|
package_lock = root / "package-artifacts.lock.json"
|
||||||
|
package_lock_value = {
|
||||||
|
"schema_version": "1",
|
||||||
|
"release_version": "1.2.3",
|
||||||
|
"python": [
|
||||||
|
{
|
||||||
|
"name": "govoplan-core",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"sha256": "8" * 64,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"webui": [],
|
||||||
|
}
|
||||||
|
package_lock_value["lock_sha256"] = hashlib.sha256(
|
||||||
|
json.dumps(
|
||||||
|
package_lock_value,
|
||||||
|
sort_keys=True,
|
||||||
|
separators=(",", ":"),
|
||||||
|
).encode("utf-8")
|
||||||
|
).hexdigest()
|
||||||
|
package_lock.write_text(
|
||||||
|
json.dumps(package_lock_value) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
args = argparse.Namespace(
|
||||||
|
composition=root / "composition.json",
|
||||||
|
api_metadata=root / "api.json",
|
||||||
|
web_metadata=root / "web.json",
|
||||||
|
deployer=deployer,
|
||||||
|
deployer_url="https://downloads.example/govoplan-deploy.pyz",
|
||||||
|
package_lock=package_lock,
|
||||||
|
artifact_base_url="https://downloads.example/runtime/v1.2.3",
|
||||||
|
source_commit="f" * 40,
|
||||||
|
version="1.2.3",
|
||||||
|
channel="stable",
|
||||||
|
sequence=1,
|
||||||
|
expires_days=30,
|
||||||
|
dependency=[
|
||||||
|
"postgres=docker.io/library/postgres@sha256:" + "5" * 64,
|
||||||
|
"redis=docker.io/library/redis@sha256:" + "6" * 64,
|
||||||
|
],
|
||||||
|
output_directory=root / "evidence",
|
||||||
|
descriptor=root / "descriptor.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
descriptor = FINALIZE.finalize(args)
|
||||||
|
|
||||||
|
self.assertEqual(["access"], descriptor["composition"]["module_ids"])
|
||||||
|
self.assertEqual(
|
||||||
|
"registry.example/govoplan/api@sha256:" + "a" * 64,
|
||||||
|
descriptor["images"]["api"]["index"],
|
||||||
|
)
|
||||||
|
self.assertTrue((root / "evidence/api-sbom.cdx.json").is_file())
|
||||||
|
self.assertTrue((root / "evidence/web-provenance.json").is_file())
|
||||||
|
self.assertEqual(
|
||||||
|
hashlib.sha256(package_lock.read_bytes()).hexdigest(),
|
||||||
|
descriptor["package_lock"]["sha256"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rejects_incomplete_oci_index(self) -> None:
|
||||||
|
with self.assertRaisesRegex(ValueError, "linux/amd64 and linux/arm64"):
|
||||||
|
OCI.resolve_platforms(
|
||||||
|
{
|
||||||
|
"manifests": [
|
||||||
|
{
|
||||||
|
"digest": "sha256:" + "1" * 64,
|
||||||
|
"platform": {"os": "linux", "architecture": "amd64"},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
repository="registry.example/govoplan/api",
|
||||||
|
index_digest="sha256:" + "a" * 64,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
|
||||||
|
SCRIPT = (
|
||||||
|
Path(__file__).resolve().parents[1]
|
||||||
|
/ "tools"
|
||||||
|
/ "release"
|
||||||
|
/ "prepare-runtime-context.py"
|
||||||
|
)
|
||||||
|
SPEC = importlib.util.spec_from_file_location("prepare_runtime_context", SCRIPT)
|
||||||
|
assert SPEC is not None and SPEC.loader is not None
|
||||||
|
MODULE = importlib.util.module_from_spec(SPEC)
|
||||||
|
sys.modules[SPEC.name] = MODULE
|
||||||
|
SPEC.loader.exec_module(MODULE)
|
||||||
|
|
||||||
|
|
||||||
|
class RuntimeImageContextTests(unittest.TestCase):
|
||||||
|
def test_builds_deterministic_network_free_context(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-runtime-context-") as value:
|
||||||
|
root = Path(value)
|
||||||
|
wheelhouse = root / "input-wheels"
|
||||||
|
web = root / "web"
|
||||||
|
wheelhouse.mkdir()
|
||||||
|
web.mkdir()
|
||||||
|
self._wheel(
|
||||||
|
wheelhouse / "govoplan_core-1.2.3-py3-none-any.whl",
|
||||||
|
package="govoplan-core",
|
||||||
|
version="1.2.3",
|
||||||
|
module_ids=(),
|
||||||
|
)
|
||||||
|
self._wheel(
|
||||||
|
wheelhouse / "govoplan_files-1.2.3-py3-none-any.whl",
|
||||||
|
package="govoplan-files",
|
||||||
|
version="1.2.3",
|
||||||
|
module_ids=("files",),
|
||||||
|
)
|
||||||
|
self._wheel(
|
||||||
|
wheelhouse / "sqlalchemy-2.0.0-py3-none-any.whl",
|
||||||
|
package="SQLAlchemy",
|
||||||
|
version="2.0.0",
|
||||||
|
module_ids=(),
|
||||||
|
)
|
||||||
|
(web / "index.html").write_text("<main>GovOPlaN</main>\n", encoding="utf-8")
|
||||||
|
|
||||||
|
composition = MODULE.prepare_context(
|
||||||
|
wheelhouse=wheelhouse,
|
||||||
|
web_dist=web,
|
||||||
|
output=root / "context",
|
||||||
|
required_modules=("files",),
|
||||||
|
source_date_epoch=1_700_000_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(["files"], composition["python"]["module_ids"])
|
||||||
|
self.assertEqual(2, composition["python"]["wheel_count"])
|
||||||
|
requirements = (root / "context" / "requirements-runtime.txt").read_text()
|
||||||
|
self.assertEqual(
|
||||||
|
"govoplan-core[server]==1.2.3\ngovoplan-files==1.2.3\n",
|
||||||
|
requirements,
|
||||||
|
)
|
||||||
|
published = json.loads(
|
||||||
|
(
|
||||||
|
root
|
||||||
|
/ "context"
|
||||||
|
/ "web-dist"
|
||||||
|
/ ".well-known"
|
||||||
|
/ "govoplan-composition.json"
|
||||||
|
).read_text()
|
||||||
|
)
|
||||||
|
self.assertEqual(composition, published)
|
||||||
|
|
||||||
|
def test_rejects_missing_required_module(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-runtime-context-") as value:
|
||||||
|
root = Path(value)
|
||||||
|
wheelhouse = root / "wheels"
|
||||||
|
web = root / "web"
|
||||||
|
wheelhouse.mkdir()
|
||||||
|
web.mkdir()
|
||||||
|
self._wheel(
|
||||||
|
wheelhouse / "govoplan_core-1.0.0-py3-none-any.whl",
|
||||||
|
package="govoplan-core",
|
||||||
|
version="1.0.0",
|
||||||
|
module_ids=(),
|
||||||
|
)
|
||||||
|
(web / "index.html").write_text("ok", encoding="utf-8")
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(MODULE.ContextError, "missing required"):
|
||||||
|
MODULE.prepare_context(
|
||||||
|
wheelhouse=wheelhouse,
|
||||||
|
web_dist=web,
|
||||||
|
output=root / "context",
|
||||||
|
required_modules=("mail",),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rejects_symlinked_web_payload(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-runtime-context-") as value:
|
||||||
|
root = Path(value)
|
||||||
|
wheelhouse = root / "wheels"
|
||||||
|
web = root / "web"
|
||||||
|
wheelhouse.mkdir()
|
||||||
|
web.mkdir()
|
||||||
|
self._wheel(
|
||||||
|
wheelhouse / "govoplan_core-1.0.0-py3-none-any.whl",
|
||||||
|
package="govoplan-core",
|
||||||
|
version="1.0.0",
|
||||||
|
module_ids=(),
|
||||||
|
)
|
||||||
|
outside = root / "outside"
|
||||||
|
outside.write_text("not part of dist", encoding="utf-8")
|
||||||
|
(web / "index.html").symlink_to(outside)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(MODULE.ContextError, "symlink"):
|
||||||
|
MODULE.prepare_context(
|
||||||
|
wheelhouse=wheelhouse,
|
||||||
|
web_dist=web,
|
||||||
|
output=root / "context",
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _wheel(
|
||||||
|
path: Path,
|
||||||
|
*,
|
||||||
|
package: str,
|
||||||
|
version: str,
|
||||||
|
module_ids: tuple[str, ...],
|
||||||
|
) -> None:
|
||||||
|
dist_info = package.replace("-", "_") + f"-{version}.dist-info"
|
||||||
|
with zipfile.ZipFile(path, "w") as archive:
|
||||||
|
archive.writestr(
|
||||||
|
f"{dist_info}/METADATA",
|
||||||
|
f"Metadata-Version: 2.1\nName: {package}\nVersion: {version}\n",
|
||||||
|
)
|
||||||
|
if module_ids:
|
||||||
|
rows = "\n".join(
|
||||||
|
f"{module_id} = example.module:manifest"
|
||||||
|
for module_id in module_ids
|
||||||
|
)
|
||||||
|
archive.writestr(
|
||||||
|
f"{dist_info}/entry_points.txt",
|
||||||
|
f"[govoplan.modules]\n{rows}\n",
|
||||||
|
)
|
||||||
|
archive.writestr(f"{package.replace('-', '_')}/__init__.py", "")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
SCRIPT = ROOT / "tools/checks/runtime-image-smoke.py"
|
||||||
|
SPEC = importlib.util.spec_from_file_location("runtime_image_smoke", SCRIPT)
|
||||||
|
assert SPEC is not None and SPEC.loader is not None
|
||||||
|
MODULE = importlib.util.module_from_spec(SPEC)
|
||||||
|
sys.modules[SPEC.name] = MODULE
|
||||||
|
SPEC.loader.exec_module(MODULE)
|
||||||
|
|
||||||
|
|
||||||
|
class RuntimeImageSmokeTests(unittest.TestCase):
|
||||||
|
def test_selects_the_exact_platform_digest(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-runtime-smoke-") as value:
|
||||||
|
path = Path(value) / "metadata.json"
|
||||||
|
path.write_text(
|
||||||
|
json.dumps(
|
||||||
|
{
|
||||||
|
"index": "registry.example/api@sha256:" + "a" * 64,
|
||||||
|
"platforms": {
|
||||||
|
"linux/amd64": "registry.example/api@sha256:" + "1" * 64,
|
||||||
|
"linux/arm64": "registry.example/api@sha256:" + "2" * 64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
"registry.example/api@sha256:" + "2" * 64,
|
||||||
|
MODULE.platform_image(path, "linux/arm64", "API"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_rejects_mutable_or_missing_platform_images(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory(prefix="govoplan-runtime-smoke-") as value:
|
||||||
|
path = Path(value) / "metadata.json"
|
||||||
|
path.write_text(
|
||||||
|
json.dumps({"platforms": {"linux/amd64": "registry.example/api:latest"}}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(MODULE.SmokeError, "exact sha256"):
|
||||||
|
MODULE.platform_image(path, "linux/amd64", "API")
|
||||||
|
with self.assertRaisesRegex(MODULE.SmokeError, "exact sha256"):
|
||||||
|
MODULE.platform_image(path, "linux/arm64", "API")
|
||||||
|
|
||||||
|
def test_readiness_fails_immediately_when_container_exits(self) -> None:
|
||||||
|
exited = subprocess.CompletedProcess([], 0, "false\n", "")
|
||||||
|
logs = subprocess.CompletedProcess(
|
||||||
|
[], 0, "fatal startup error db-secret\n", ""
|
||||||
|
)
|
||||||
|
with patch.object(MODULE, "_run", side_effect=(exited, logs)):
|
||||||
|
with self.assertRaisesRegex(
|
||||||
|
MODULE.SmokeError,
|
||||||
|
r"container exited before readiness: fatal startup error \[redacted\]",
|
||||||
|
):
|
||||||
|
MODULE._wait_for(
|
||||||
|
"WebUI",
|
||||||
|
lambda: self.fail("probe must not run for an exited container"),
|
||||||
|
timeout=60,
|
||||||
|
container="web",
|
||||||
|
redactions=("db-secret",),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_smoke_supplies_the_packaged_web_upstream_and_schema_contract(self) -> None:
|
||||||
|
source = SCRIPT.read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
self.assertIn('"--network-alias",\n "load-balancer"', source)
|
||||||
|
self.assertIn("'core_system_settings'", source)
|
||||||
|
self.assertIn("'core_runtime_nodes'", source)
|
||||||
|
self.assertIn('if platform == "linux/arm64"', source)
|
||||||
|
self.assertIn('"ARM64-COW-BUG"', source)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -28,8 +28,8 @@ def load_resolver_module():
|
|||||||
class SecurityAuditMountResolverTests(unittest.TestCase):
|
class SecurityAuditMountResolverTests(unittest.TestCase):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
self.resolver = load_resolver_module()
|
self.resolver = load_resolver_module()
|
||||||
self.root = "/workspace/add-ideas/govoplan/govoplan"
|
self.root = "/workspace/GovOPlaN/govoplan/govoplan"
|
||||||
self.workspace = "/workspace/add-ideas/govoplan"
|
self.workspace = "/workspace/GovOPlaN/govoplan"
|
||||||
|
|
||||||
def test_resolves_only_the_named_workspace_volume_for_both_scopes(self) -> None:
|
def test_resolves_only_the_named_workspace_volume_for_both_scopes(self) -> None:
|
||||||
mounts = [
|
mounts = [
|
||||||
@@ -136,7 +136,7 @@ class SecurityAuditMountResolverTests(unittest.TestCase):
|
|||||||
{
|
{
|
||||||
"Type": "volume",
|
"Type": "volume",
|
||||||
"Name": "wrong-workspace",
|
"Name": "wrong-workspace",
|
||||||
"Destination": "/workspace/add-ideas/govoplan-other",
|
"Destination": "/workspace/GovOPlaN/govoplan-other",
|
||||||
"RW": True,
|
"RW": True,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user