Compare commits
26 Commits
a10a01c903
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 1aea3e7c4f | |||
| 3f9567af18 | |||
| de16f11ce8 | |||
| 9d6cdff4b8 | |||
| aa4050c0ca | |||
| d3cdbd8c7a | |||
| 7115c4711d | |||
| a3beca6fc5 | |||
| 11d45bce25 | |||
| 7b6135b89b | |||
| 5b79e7d377 | |||
| 6afb8fea76 | |||
| 163b35c0af | |||
| 603e07cec5 | |||
| 97dfd333c6 | |||
| ba88c574b9 | |||
| 8d292184d4 | |||
| 52bd3527cd | |||
| ff49dabf8f | |||
| cd15aa514e | |||
| a042baa1d3 | |||
| e80de00f29 | |||
| c69acf0dee | |||
| 8b93bbc6b6 | |||
| 3fc17701df | |||
| 9788bdde0c |
15
.env.example
15
.env.example
@@ -2,17 +2,24 @@
|
||||
# Copy to a deployment-local .env or secret store. Do not commit populated secrets.
|
||||
|
||||
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
|
||||
MASTER_KEY_B64=<generate-with-govoplan-config-env-template-generate-secrets>
|
||||
|
||||
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_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,mail,campaigns,calendar,poll,scheduling,connectors,datasources,dataflow,workflow,views,search,risk_compliance,postbox,notifications,docs,ops
|
||||
|
||||
CELERY_ENABLED=true
|
||||
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
|
||||
|
||||
GOVOPLAN_CONNECTOR_ALLOW_PRIVATE_NETWORKS=false
|
||||
@@ -24,10 +31,14 @@ GOVOPLAN_HTTP_MAX_REQUEST_BODY_BYTES=536870912
|
||||
GOVOPLAN_HTTP_HSTS_SECONDS=31536000
|
||||
|
||||
AUTH_LOGIN_THROTTLE_ENABLED=true
|
||||
AUTH_ACTIVITY_TOUCH_INTERVAL_SECONDS=300
|
||||
AUTH_LOGIN_THROTTLE_IDENTITY_LIMIT=10
|
||||
AUTH_LOGIN_THROTTLE_CLIENT_LIMIT=100
|
||||
AUTH_LOGIN_THROTTLE_WINDOW_SECONDS=900
|
||||
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
|
||||
GOVOPLAN_TRUSTED_HOSTS=govoplan.example.org
|
||||
|
||||
@@ -21,23 +21,13 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: Configure SSH for release dependencies
|
||||
env:
|
||||
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
|
||||
- name: Use anonymous HTTPS for public GovOPlaN repositories
|
||||
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 release dependencies."
|
||||
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
|
||||
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 GovOPlaN repositories
|
||||
working-directory: govoplan
|
||||
run: python tools/repo/bootstrap-repositories.py --parent ..
|
||||
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --exclude-repo addideas-govoplan-website
|
||||
- name: Install backend dev audit dependencies
|
||||
working-directory: govoplan
|
||||
run: |
|
||||
|
||||
@@ -7,6 +7,18 @@ on:
|
||||
jobs:
|
||||
module-matrix:
|
||||
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
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
@@ -17,23 +29,13 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: Configure SSH for release dependencies
|
||||
env:
|
||||
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
|
||||
- name: Use anonymous HTTPS for public GovOPlaN repositories
|
||||
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 release dependencies."
|
||||
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
|
||||
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 GovOPlaN repositories
|
||||
working-directory: govoplan
|
||||
run: python tools/repo/bootstrap-repositories.py --parent ..
|
||||
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --exclude-repo addideas-govoplan-website
|
||||
- name: Install backend release dependencies
|
||||
working-directory: govoplan
|
||||
run: |
|
||||
@@ -43,6 +45,21 @@ jobs:
|
||||
- name: Install WebUI release dependencies with test scripts
|
||||
working-directory: govoplan
|
||||
run: bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
|
||||
- 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: Run module matrix and contract tests
|
||||
working-directory: govoplan
|
||||
run: GOVOPLAN_CORE_ROOT="$PWD/../govoplan-core" PYTHON="$PWD/.venv/bin/python" bash tools/checks/check-module-matrix.sh
|
||||
|
||||
@@ -16,29 +16,19 @@ jobs:
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020
|
||||
with:
|
||||
node-version: "22"
|
||||
- name: Configure SSH for release dependencies
|
||||
env:
|
||||
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
|
||||
- name: Use anonymous HTTPS for public GovOPlaN repositories
|
||||
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 release dependencies."
|
||||
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
|
||||
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 GovOPlaN repositories
|
||||
working-directory: govoplan
|
||||
run: python tools/repo/bootstrap-repositories.py --parent ..
|
||||
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https --exclude-repo addideas-govoplan-website
|
||||
- name: Install backend release integration dependencies
|
||||
working-directory: govoplan
|
||||
run: |
|
||||
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 -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
|
||||
working-directory: govoplan
|
||||
run: bash tools/release/install-webui-release-dependencies.sh ../govoplan-core/webui
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
name: Security Audit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
@@ -21,6 +20,10 @@ jobs:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5
|
||||
with:
|
||||
path: govoplan
|
||||
- name: Use anonymous HTTPS for public 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: Configure SSH for repository bootstrap
|
||||
env:
|
||||
GOVOPLAN_RELEASE_SSH_KEY_B64: ${{ secrets.GOVOPLAN_RELEASE_SSH_KEY_B64 }}
|
||||
@@ -37,13 +40,13 @@ jobs:
|
||||
chmod 600 ~/.ssh/known_hosts
|
||||
- name: Bootstrap GovOPlaN repositories
|
||||
working-directory: govoplan
|
||||
run: python tools/repo/bootstrap-repositories.py --parent ..
|
||||
- name: Run security audit
|
||||
run: python tools/repo/bootstrap-repositories.py --parent .. --transport public-https
|
||||
- name: Run whole-system security audit
|
||||
working-directory: govoplan
|
||||
run: tools/checks/security-audit/run.sh --mode "$SECURITY_AUDIT_MODE" --scope "$SECURITY_AUDIT_SCOPE" --reports-dir audit-reports
|
||||
- name: Upload audit reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32
|
||||
with:
|
||||
name: security-audit-reports
|
||||
path: govoplan/audit-reports
|
||||
|
||||
21
README.md
21
README.md
@@ -4,10 +4,10 @@
|
||||
**Repository type:** system (meta).
|
||||
<!-- 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/add-ideas/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/add-ideas/govoplan/actions?workflow=security-audit.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/GovOPlaN/govoplan/actions?workflow=release-integration.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
|
||||
whole-product development, release orchestration, repository bootstrap, and
|
||||
@@ -41,6 +41,16 @@ Open the WebUI in a browser after launch only when explicitly requested:
|
||||
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:
|
||||
|
||||
```sh
|
||||
@@ -151,6 +161,9 @@ Meta ownership and module install/contract boundaries are documented in
|
||||
`docs/META_REPO_SCAN.md` and `docs/MODULE_CONTRACTS_AND_INSTALLS.md`.
|
||||
Frontend layout principles for module pages are documented in
|
||||
`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,
|
||||
connected outcome stories, and capability horizons are documented in
|
||||
the [Connected Governance Platform Roadmap](docs/CONNECTED_GOVERNANCE_PLATFORM_ROADMAP.md).
|
||||
|
||||
@@ -57,7 +57,7 @@ profile intentionally runs only PostgreSQL and Redis in containers; API,
|
||||
WebUI, worker, and scheduler processes still run from editable source trees. A
|
||||
target deployment must supply TLS termination, process supervision, secret
|
||||
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
|
||||
disaster-recovery plan are also not complete.
|
||||
|
||||
@@ -134,7 +134,7 @@ persistence, and durable shared or object storage. Run one scheduler only when a
|
||||
selected module needs scheduled recovery. Multiple scheduler replicas require
|
||||
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.
|
||||
|
||||
## Functional capability matrix
|
||||
@@ -154,7 +154,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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
||||
## Infrastructure matrix
|
||||
@@ -177,13 +177,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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
||||
The scalability and sizing documentation delivered the documentation portions
|
||||
of [Core #217](https://git.add-ideas.de/add-ideas/govoplan-core/issues/217) and
|
||||
[Core #219](https://git.add-ideas.de/add-ideas/govoplan-core/issues/219).
|
||||
[Core #28](https://git.add-ideas.de/add-ideas/govoplan-core/issues/28) records the
|
||||
of [Core #217](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/217) and
|
||||
[Core #219](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/219).
|
||||
[Core #28](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/28) records the
|
||||
operator-documentation slice. These closed tickets are evidence of documented
|
||||
models, not evidence that an organization's production environment has passed
|
||||
them.
|
||||
@@ -584,13 +584,13 @@ source tests or signed release metadata.
|
||||
- [Production-like profile](../dev/production-like/README.md) and
|
||||
[Compose dependencies](../dev/production-like/docker-compose.yml)
|
||||
- [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)
|
||||
- [Ops scalability profiles](https://git.add-ideas.de/add-ideas/govoplan-ops/src/branch/main/docs/SCALABILITY_PROFILES.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/GovOPlaN/govoplan-ops/src/branch/main/docs/SCALABILITY_PROFILES.md)
|
||||
- Actual module manifests in the pinned repositories and the static contract
|
||||
checker in this meta repository
|
||||
- Core module-system/API smoke/auth/install-config tests, plus focused Campaign,
|
||||
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)
|
||||
and [published keyring](https://govoplan.add-ideas.de/catalogs/v1/keyring.json),
|
||||
verified against a separately provisioned local trust keyring
|
||||
|
||||
@@ -10,7 +10,7 @@ baseline to that destination.
|
||||
|
||||
It is a durable direction, not a release promise or a substitute for issue
|
||||
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
|
||||
cross-product vision that sequence serves.
|
||||
|
||||
@@ -448,7 +448,7 @@ without creating disconnected ticket systems.
|
||||
- As a manager, I can distinguish demand, backlog, SLA risk, recurring cause,
|
||||
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,
|
||||
reporting, policy, and audit.
|
||||
|
||||
@@ -530,7 +530,7 @@ access, no hidden cross-module coupling, correction rather than fictional undo,
|
||||
and a durable action/effect trail.
|
||||
|
||||
**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
|
||||
not a claim that screening is implemented today.
|
||||
|
||||
@@ -1279,8 +1279,9 @@ them; they do not block the product vision today.
|
||||
- Which exact HIS/CampusOnline interfaces, student-statistics fields and
|
||||
official keys, accepted calculation, freeze/correction policy, privacy
|
||||
profile, and drill-down level should define the first analytical data product?
|
||||
- After the first source-to-report proof, do repeated source/dataflow contracts
|
||||
justify separate `govoplan-datasources` and `govoplan-dataflow` modules?
|
||||
- Which database, REST, directory, and managed-file providers should follow the
|
||||
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
|
||||
accepted experience emphasize concurrent editing, controlled check-out, or
|
||||
both?
|
||||
@@ -1381,7 +1382,7 @@ integration. Conversely, 30 repositories had no open issue; for many
|
||||
later-wave modules this meant no implementation program had been opened, not
|
||||
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
|
||||
matrix/history, idempotent keyed retries, re-decision audit, archive/unarchive,
|
||||
and preservation behavior were implemented and pushed while the issue still
|
||||
@@ -1389,27 +1390,27 @@ reported `needs-info`.
|
||||
|
||||
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;
|
||||
- [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;
|
||||
- [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;
|
||||
- [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;
|
||||
- [Core #263](https://git.add-ideas.de/add-ideas/govoplan-core/issues/263) and
|
||||
[Campaign #63](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/63),
|
||||
[#62](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/62),
|
||||
[#65](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/65), and
|
||||
[#69](https://git.add-ideas.de/add-ideas/govoplan-campaign/issues/69) for the
|
||||
- [Core #263](https://git.add-ideas.de/GovOPlaN/govoplan-core/issues/263) and
|
||||
[Campaign #63](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/63),
|
||||
[#62](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/62),
|
||||
[#65](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/65), and
|
||||
[#69](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/69) for the
|
||||
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;
|
||||
- [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;
|
||||
- [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
|
||||
- [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.
|
||||
|
||||
Live Gitea issue state remains canonical. These dated facts explain the roadmap
|
||||
|
||||
82
docs/DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md
Normal file
82
docs/DATASOURCE_AND_DEFINITION_GRAPH_ARCHITECTURE.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Datasource And Definition Graph Architecture
|
||||
|
||||
## Two-Layer Data Boundary
|
||||
|
||||
GovOPlaN separates governed data identity from external acquisition:
|
||||
|
||||
| Layer | Owner | Responsibilities |
|
||||
| --- | --- | --- |
|
||||
| Datasource layer | `govoplan-datasources` | Catalogue, tenant visibility, live/cached/static mode, staging, immutable materializations, frozen states, schema, fingerprints, provenance, and bounded reads |
|
||||
| Connector layer | `govoplan-connectors` and protocol/provider modules | External protocols, endpoints, connection profiles, credentials, discovery, provider health, source-side filtering, and query pushdown |
|
||||
|
||||
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 provides trigger, activity, review, decision, wait, module-action,
|
||||
Dataflow, and outcome nodes. It permits governed loops and has exactly one
|
||||
trigger plus one or more outcomes.
|
||||
|
||||
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 exposes a reusable graph editor, node-library discovery, validation,
|
||||
tenant-isolated definitions, immutable revisions, and activation pinning.
|
||||
- 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 a pinned run-lifecycle capability and a Run/Publish surface.
|
||||
Its first synchronous runner records lineage and terminal state, publishes
|
||||
only complete bounded results, and retains output datasource/materialization
|
||||
references.
|
||||
- The focused composition check proves Connector origin -> Datasource ->
|
||||
pinned Dataflow run -> frozen published materialization, including replay.
|
||||
|
||||
## Next Slices
|
||||
|
||||
1. Add persisted Workflow instances, resumable transitions, human activities,
|
||||
retry policy, and event subscriptions against pinned definition revisions.
|
||||
2. Add SQL database and governed REST origin providers with credential-envelope
|
||||
references and bounded pushdown.
|
||||
3. Add managed-file and directory origins.
|
||||
4. Add datasource quality rules, schema compatibility policy, retention, and
|
||||
promotion approvals.
|
||||
5. Add asynchronous Dataflow workers and durable artifact-backed outputs for
|
||||
runs that exceed the synchronous row/time/byte limits.
|
||||
@@ -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 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
|
||||
export GITEA_URL=https://git.add-ideas.de
|
||||
export GITEA_OWNER=add-ideas
|
||||
export GITEA_OWNER=GovOPlaN
|
||||
export GITEA_REPO=govoplan
|
||||
export GITEA_TOKEN=...
|
||||
```
|
||||
@@ -23,7 +23,7 @@ The API scripts also read `GITEA_*` values from the target repository's `.env` f
|
||||
GITEA_TOKEN=...
|
||||
# Optional if origin inference is not enough:
|
||||
GITEA_URL=https://git.add-ideas.de
|
||||
GITEA_OWNER=add-ideas
|
||||
GITEA_OWNER=GovOPlaN
|
||||
GITEA_REPO=govoplan
|
||||
```
|
||||
|
||||
|
||||
@@ -444,25 +444,25 @@ reason to infer that a pattern is satisfied.
|
||||
and does not duplicate a central component.
|
||||
- Behavioral/accessibility evidence is linked from the rollout matrix and issue.
|
||||
- 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.
|
||||
|
||||
## 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
|
||||
generic extraction:
|
||||
|
||||
- [#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): stable,
|
||||
- [#59](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/59) and
|
||||
[#73](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/73): stable,
|
||||
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
|
||||
- [#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
|
||||
- [#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
|
||||
- [#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
|
||||
|
||||
These slices do not depend on the Workflow runtime. Campaign's current
|
||||
|
||||
@@ -59,16 +59,16 @@ The access guard column reports only the route-level declaration in
|
||||
|
||||
| Route | Owner / render evidence | Route-level access evidence | Primary task | Target archetype | Status / priority |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `/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` | `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/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 |
|
||||
| `/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 |
|
||||
| `/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) |
|
||||
| `/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/GovOPlaN/govoplan-campaign/issues/74) |
|
||||
| `/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 |
|
||||
| `/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 |
|
||||
| `/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 |
|
||||
| `/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/GovOPlaN/govoplan-campaign/issues/78); #74 audit remains |
|
||||
| `/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/GovOPlaN/govoplan-campaign/issues/80); #74 audit remains |
|
||||
| `/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 |
|
||||
| `/dashboard` | `govoplan-dashboard` `DashboardPage` | No route-specific scope | Assemble module-provided actionable widgets | Dashboard | Contributed; unreviewed; P2 |
|
||||
| `/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 |
|
||||
| `/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/GovOPlaN/govoplan-docs/issues/15) after initial pattern content |
|
||||
| `/files` | `govoplan-files` `FilesPage` | `files:file:read` | Browse folders/files and perform managed-file work | Directory/explorer | Contributed; metadata gap; unreviewed; P2 after Campaign |
|
||||
| `/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 |
|
||||
| `/mail` | `govoplan-mail` `MailboxPage` | `mail:mailbox:read` | Browse mailboxes and messages | Directory/list-detail | Contributed; metadata gap; unreviewed; P2 after Campaign |
|
||||
@@ -138,7 +138,7 @@ above; they are not independent routes.
|
||||
|
||||
Campaign is detailed first because it exercises almost every archetype. The
|
||||
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.
|
||||
|
||||
Campaign already consumes core primitives including `ModuleSubnav`, `Card`,
|
||||
@@ -150,25 +150,25 @@ prove that the composition or states satisfy the pattern.
|
||||
|
||||
| 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 | Audit in [#74](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/74); guided entry [#35](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/35) |
|
||||
| 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 |
|
||||
| Fields (`CampaignFieldsPage`) | Define recipient/template field schema | Structured editor | Schema changes can invalidate recipient/template data | #74 audit |
|
||||
| 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; attachment-detail [#59](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/59) |
|
||||
| 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 |
|
||||
| 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; stable overlay [#73](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/73) |
|
||||
| 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 |
|
||||
| Campaign settings (`GlobalSettingsPage` settings view) | Configure campaign behavior | Adaptive configuration | Can alter validation/build/send behavior | #74 audit |
|
||||
| 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 |
|
||||
| Campaign policy (`GlobalSettingsPage` policy view) | Inspect/override campaign policy | Effective policy/provenance editor | Inheritance, actor authority, and blocked edits | #74; Core #225 policy pattern |
|
||||
| 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 |
|
||||
| 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) |
|
||||
| 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) |
|
||||
| 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/GovOPlaN/govoplan-campaign/issues/62) and [#79](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/79); [#63](https://git.add-ideas.de/GovOPlaN/govoplan-campaign/issues/63) wording and #74 audit remain |
|
||||
| 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/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 |
|
||||
| JSON (`CampaignJsonView`) | Inspect expert representation | Advanced diagnostics/reference | Raw data may contain personal/configuration values; not a primary editor | #74 privacy/redaction audit |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| 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/GovOPlaN/govoplan-campaign/issues/78); #74 wording/accessibility audit remains |
|
||||
| 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`,
|
||||
@@ -186,7 +186,7 @@ The following local repositories contain a backend manifest but no
|
||||
`govoplan-contracts`, `govoplan-dist-lists`, `govoplan-evaluation`,
|
||||
`govoplan-facilities`, `govoplan-forms-runtime`, `govoplan-grants`,
|
||||
`govoplan-helpdesk`, `govoplan-identity`, `govoplan-inspections`,
|
||||
`govoplan-issue-reporting`, `govoplan-learning`, `govoplan-permits`,
|
||||
`govoplan-tickets`, `govoplan-learning`, `govoplan-permits`,
|
||||
`govoplan-poll`, `govoplan-procurement`, `govoplan-records`,
|
||||
`govoplan-resources`, `govoplan-rest`, `govoplan-risk-compliance`,
|
||||
`govoplan-soap`, `govoplan-tenancy`, and `govoplan-transparency`.
|
||||
@@ -199,7 +199,7 @@ backend-only modules may remain intentionally headless.
|
||||
|
||||
| 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) | Docs links/diff checks; issue/wiki sync after integration | Initial slice in this document |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
||||
@@ -114,7 +114,10 @@ For release validation:
|
||||
|
||||
```sh
|
||||
./.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
|
||||
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.
|
||||
|
||||
@@ -445,8 +445,9 @@ that first needs them:
|
||||
- exact HIS/CampusOnline source endpoints, student-statistics keys and accepted
|
||||
calculation, freeze/correction policy, privacy profile, and permitted
|
||||
drill-down level;
|
||||
- whether repeated data-source and dataflow contracts justify separate
|
||||
`govoplan-datasources` and `govoplan-dataflow` modules after the Stage 3 proof;
|
||||
- datasource provider selection and quality/promotion policy; the architecture
|
||||
now separates `govoplan-datasources` lifecycle from `govoplan-connectors`
|
||||
acquisition and `govoplan-dataflow` transformation;
|
||||
- first collaborative editor/provider and whether the first UX is concurrent
|
||||
editing, controlled check-out, or both; and
|
||||
- first Records/archive target and approval/signature assurance level.
|
||||
|
||||
@@ -6,81 +6,86 @@ Generated from `repositories.json`. Use that JSON file as the machine-readable s
|
||||
|
||||
| Repository | Subtype | Local path | Gitea |
|
||||
| --- | --- | --- | --- |
|
||||
| `govoplan` | `meta` | `../govoplan` | [govoplan](https://git.add-ideas.de/add-ideas/govoplan) |
|
||||
| `govoplan-core` | `kernel` | `../govoplan-core` | [govoplan-core](https://git.add-ideas.de/add-ideas/govoplan-core) |
|
||||
| `govoplan` | `meta` | `../govoplan` | [govoplan](https://git.add-ideas.de/GovOPlaN/govoplan) |
|
||||
| `govoplan-core` | `kernel` | `../govoplan-core` | [govoplan-core](https://git.add-ideas.de/GovOPlaN/govoplan-core) |
|
||||
|
||||
## Module
|
||||
|
||||
| Repository | Subtype | Local path | Gitea |
|
||||
| --- | --- | --- | --- |
|
||||
| `govoplan-access` | `platform` | `../govoplan-access` | [govoplan-access](https://git.add-ideas.de/add-ideas/govoplan-access) |
|
||||
| `govoplan-addresses` | `domain` | `../govoplan-addresses` | [govoplan-addresses](https://git.add-ideas.de/add-ideas/govoplan-addresses) |
|
||||
| `govoplan-admin` | `platform` | `../govoplan-admin` | [govoplan-admin](https://git.add-ideas.de/add-ideas/govoplan-admin) |
|
||||
| `govoplan-appointments` | `domain` | `../govoplan-appointments` | [govoplan-appointments](https://git.add-ideas.de/add-ideas/govoplan-appointments) |
|
||||
| `govoplan-approvals` | `domain` | `../govoplan-approvals` | [govoplan-approvals](https://git.add-ideas.de/add-ideas/govoplan-approvals) |
|
||||
| `govoplan-assets` | `domain` | `../govoplan-assets` | [govoplan-assets](https://git.add-ideas.de/add-ideas/govoplan-assets) |
|
||||
| `govoplan-audit` | `platform` | `../govoplan-audit` | [govoplan-audit](https://git.add-ideas.de/add-ideas/govoplan-audit) |
|
||||
| `govoplan-booking` | `domain` | `../govoplan-booking` | [govoplan-booking](https://git.add-ideas.de/add-ideas/govoplan-booking) |
|
||||
| `govoplan-calendar` | `domain` | `../govoplan-calendar` | [govoplan-calendar](https://git.add-ideas.de/add-ideas/govoplan-calendar) |
|
||||
| `govoplan-campaign` | `domain` | `../govoplan-campaign` | [govoplan-campaign](https://git.add-ideas.de/add-ideas/govoplan-campaign) |
|
||||
| `govoplan-cases` | `domain` | `../govoplan-cases` | [govoplan-cases](https://git.add-ideas.de/add-ideas/govoplan-cases) |
|
||||
| `govoplan-certificates` | `domain` | `../govoplan-certificates` | [govoplan-certificates](https://git.add-ideas.de/add-ideas/govoplan-certificates) |
|
||||
| `govoplan-committee` | `domain` | `../govoplan-committee` | [govoplan-committee](https://git.add-ideas.de/add-ideas/govoplan-committee) |
|
||||
| `govoplan-consultation` | `domain` | `../govoplan-consultation` | [govoplan-consultation](https://git.add-ideas.de/add-ideas/govoplan-consultation) |
|
||||
| `govoplan-contracts` | `domain` | `../govoplan-contracts` | [govoplan-contracts](https://git.add-ideas.de/add-ideas/govoplan-contracts) |
|
||||
| `govoplan-dashboard` | `platform` | `../govoplan-dashboard` | [govoplan-dashboard](https://git.add-ideas.de/add-ideas/govoplan-dashboard) |
|
||||
| `govoplan-dms` | `domain` | `../govoplan-dms` | [govoplan-dms](https://git.add-ideas.de/add-ideas/govoplan-dms) |
|
||||
| `govoplan-dist-lists` | `domain` | `../govoplan-dist-lists` | [govoplan-dist-lists](https://git.add-ideas.de/add-ideas/govoplan-dist-lists) |
|
||||
| `govoplan-docs` | `platform` | `../govoplan-docs` | [govoplan-docs](https://git.add-ideas.de/add-ideas/govoplan-docs) |
|
||||
| `govoplan-erp` | `domain` | `../govoplan-erp` | [govoplan-erp](https://git.add-ideas.de/add-ideas/govoplan-erp) |
|
||||
| `govoplan-evaluation` | `domain` | `../govoplan-evaluation` | [govoplan-evaluation](https://git.add-ideas.de/add-ideas/govoplan-evaluation) |
|
||||
| `govoplan-facilities` | `domain` | `../govoplan-facilities` | [govoplan-facilities](https://git.add-ideas.de/add-ideas/govoplan-facilities) |
|
||||
| `govoplan-files` | `domain` | `../govoplan-files` | [govoplan-files](https://git.add-ideas.de/add-ideas/govoplan-files) |
|
||||
| `govoplan-forms` | `domain` | `../govoplan-forms` | [govoplan-forms](https://git.add-ideas.de/add-ideas/govoplan-forms) |
|
||||
| `govoplan-forms-runtime` | `platform` | `../govoplan-forms-runtime` | [govoplan-forms-runtime](https://git.add-ideas.de/add-ideas/govoplan-forms-runtime) |
|
||||
| `govoplan-grants` | `domain` | `../govoplan-grants` | [govoplan-grants](https://git.add-ideas.de/add-ideas/govoplan-grants) |
|
||||
| `govoplan-helpdesk` | `domain` | `../govoplan-helpdesk` | [govoplan-helpdesk](https://git.add-ideas.de/add-ideas/govoplan-helpdesk) |
|
||||
| `govoplan-identity` | `platform` | `../govoplan-identity` | [govoplan-identity](https://git.add-ideas.de/add-ideas/govoplan-identity) |
|
||||
| `govoplan-identity-trust` | `platform` | `../govoplan-identity-trust` | [govoplan-identity-trust](https://git.add-ideas.de/add-ideas/govoplan-identity-trust) |
|
||||
| `govoplan-idm` | `platform` | `../govoplan-idm` | [govoplan-idm](https://git.add-ideas.de/add-ideas/govoplan-idm) |
|
||||
| `govoplan-inspections` | `domain` | `../govoplan-inspections` | [govoplan-inspections](https://git.add-ideas.de/add-ideas/govoplan-inspections) |
|
||||
| `govoplan-issue-reporting` | `domain` | `../govoplan-issue-reporting` | [govoplan-issue-reporting](https://git.add-ideas.de/add-ideas/govoplan-issue-reporting) |
|
||||
| `govoplan-learning` | `domain` | `../govoplan-learning` | [govoplan-learning](https://git.add-ideas.de/add-ideas/govoplan-learning) |
|
||||
| `govoplan-ledger` | `domain` | `../govoplan-ledger` | [govoplan-ledger](https://git.add-ideas.de/add-ideas/govoplan-ledger) |
|
||||
| `govoplan-mail` | `domain` | `../govoplan-mail` | [govoplan-mail](https://git.add-ideas.de/add-ideas/govoplan-mail) |
|
||||
| `govoplan-notifications` | `platform` | `../govoplan-notifications` | [govoplan-notifications](https://git.add-ideas.de/add-ideas/govoplan-notifications) |
|
||||
| `govoplan-ops` | `platform` | `../govoplan-ops` | [govoplan-ops](https://git.add-ideas.de/add-ideas/govoplan-ops) |
|
||||
| `govoplan-organizations` | `platform` | `../govoplan-organizations` | [govoplan-organizations](https://git.add-ideas.de/add-ideas/govoplan-organizations) |
|
||||
| `govoplan-payments` | `domain` | `../govoplan-payments` | [govoplan-payments](https://git.add-ideas.de/add-ideas/govoplan-payments) |
|
||||
| `govoplan-permits` | `domain` | `../govoplan-permits` | [govoplan-permits](https://git.add-ideas.de/add-ideas/govoplan-permits) |
|
||||
| `govoplan-policy` | `platform` | `../govoplan-policy` | [govoplan-policy](https://git.add-ideas.de/add-ideas/govoplan-policy) |
|
||||
| `govoplan-poll` | `domain` | `../govoplan-poll` | [govoplan-poll](https://git.add-ideas.de/add-ideas/govoplan-poll) |
|
||||
| `govoplan-portal` | `domain` | `../govoplan-portal` | [govoplan-portal](https://git.add-ideas.de/add-ideas/govoplan-portal) |
|
||||
| `govoplan-postbox` | `domain` | `../govoplan-postbox` | [govoplan-postbox](https://git.add-ideas.de/add-ideas/govoplan-postbox) |
|
||||
| `govoplan-procurement` | `domain` | `../govoplan-procurement` | [govoplan-procurement](https://git.add-ideas.de/add-ideas/govoplan-procurement) |
|
||||
| `govoplan-records` | `domain` | `../govoplan-records` | [govoplan-records](https://git.add-ideas.de/add-ideas/govoplan-records) |
|
||||
| `govoplan-reporting` | `domain` | `../govoplan-reporting` | [govoplan-reporting](https://git.add-ideas.de/add-ideas/govoplan-reporting) |
|
||||
| `govoplan-resources` | `domain` | `../govoplan-resources` | [govoplan-resources](https://git.add-ideas.de/add-ideas/govoplan-resources) |
|
||||
| `govoplan-risk-compliance` | `domain` | `../govoplan-risk-compliance` | [govoplan-risk-compliance](https://git.add-ideas.de/add-ideas/govoplan-risk-compliance) |
|
||||
| `govoplan-scheduling` | `domain` | `../govoplan-scheduling` | [govoplan-scheduling](https://git.add-ideas.de/add-ideas/govoplan-scheduling) |
|
||||
| `govoplan-search` | `platform` | `../govoplan-search` | [govoplan-search](https://git.add-ideas.de/add-ideas/govoplan-search) |
|
||||
| `govoplan-tasks` | `domain` | `../govoplan-tasks` | [govoplan-tasks](https://git.add-ideas.de/add-ideas/govoplan-tasks) |
|
||||
| `govoplan-templates` | `domain` | `../govoplan-templates` | [govoplan-templates](https://git.add-ideas.de/add-ideas/govoplan-templates) |
|
||||
| `govoplan-tenancy` | `platform` | `../govoplan-tenancy` | [govoplan-tenancy](https://git.add-ideas.de/add-ideas/govoplan-tenancy) |
|
||||
| `govoplan-transparency` | `domain` | `../govoplan-transparency` | [govoplan-transparency](https://git.add-ideas.de/add-ideas/govoplan-transparency) |
|
||||
| `govoplan-workflow` | `platform` | `../govoplan-workflow` | [govoplan-workflow](https://git.add-ideas.de/add-ideas/govoplan-workflow) |
|
||||
| `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/GovOPlaN/govoplan-addresses) |
|
||||
| `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/GovOPlaN/govoplan-appointments) |
|
||||
| `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/GovOPlaN/govoplan-assets) |
|
||||
| `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/GovOPlaN/govoplan-booking) |
|
||||
| `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/GovOPlaN/govoplan-campaign) |
|
||||
| `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/GovOPlaN/govoplan-certificates) |
|
||||
| `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/GovOPlaN/govoplan-consultation) |
|
||||
| `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/GovOPlaN/govoplan-dashboard) |
|
||||
| `govoplan-dataflow` | `platform` | `../govoplan-dataflow` | [govoplan-dataflow](https://git.add-ideas.de/GovOPlaN/govoplan-dataflow) |
|
||||
| `govoplan-datasources` | `platform` | `../govoplan-datasources` | [govoplan-datasources](https://git.add-ideas.de/GovOPlaN/govoplan-datasources) |
|
||||
| `govoplan-dms` | `domain` | `../govoplan-dms` | [govoplan-dms](https://git.add-ideas.de/GovOPlaN/govoplan-dms) |
|
||||
| `govoplan-dist-lists` | `domain` | `../govoplan-dist-lists` | [govoplan-dist-lists](https://git.add-ideas.de/GovOPlaN/govoplan-dist-lists) |
|
||||
| `govoplan-docs` | `platform` | `../govoplan-docs` | [govoplan-docs](https://git.add-ideas.de/GovOPlaN/govoplan-docs) |
|
||||
| `govoplan-erp` | `domain` | `../govoplan-erp` | [govoplan-erp](https://git.add-ideas.de/GovOPlaN/govoplan-erp) |
|
||||
| `govoplan-evaluation` | `domain` | `../govoplan-evaluation` | [govoplan-evaluation](https://git.add-ideas.de/GovOPlaN/govoplan-evaluation) |
|
||||
| `govoplan-facilities` | `domain` | `../govoplan-facilities` | [govoplan-facilities](https://git.add-ideas.de/GovOPlaN/govoplan-facilities) |
|
||||
| `govoplan-files` | `domain` | `../govoplan-files` | [govoplan-files](https://git.add-ideas.de/GovOPlaN/govoplan-files) |
|
||||
| `govoplan-forms` | `domain` | `../govoplan-forms` | [govoplan-forms](https://git.add-ideas.de/GovOPlaN/govoplan-forms) |
|
||||
| `govoplan-forms-runtime` | `platform` | `../govoplan-forms-runtime` | [govoplan-forms-runtime](https://git.add-ideas.de/GovOPlaN/govoplan-forms-runtime) |
|
||||
| `govoplan-grants` | `domain` | `../govoplan-grants` | [govoplan-grants](https://git.add-ideas.de/GovOPlaN/govoplan-grants) |
|
||||
| `govoplan-helpdesk` | `domain` | `../govoplan-helpdesk` | [govoplan-helpdesk](https://git.add-ideas.de/GovOPlaN/govoplan-helpdesk) |
|
||||
| `govoplan-identity` | `platform` | `../govoplan-identity` | [govoplan-identity](https://git.add-ideas.de/GovOPlaN/govoplan-identity) |
|
||||
| `govoplan-identity-trust` | `platform` | `../govoplan-identity-trust` | [govoplan-identity-trust](https://git.add-ideas.de/GovOPlaN/govoplan-identity-trust) |
|
||||
| `govoplan-idm` | `platform` | `../govoplan-idm` | [govoplan-idm](https://git.add-ideas.de/GovOPlaN/govoplan-idm) |
|
||||
| `govoplan-inspections` | `domain` | `../govoplan-inspections` | [govoplan-inspections](https://git.add-ideas.de/GovOPlaN/govoplan-inspections) |
|
||||
| `govoplan-learning` | `domain` | `../govoplan-learning` | [govoplan-learning](https://git.add-ideas.de/GovOPlaN/govoplan-learning) |
|
||||
| `govoplan-ledger` | `domain` | `../govoplan-ledger` | [govoplan-ledger](https://git.add-ideas.de/GovOPlaN/govoplan-ledger) |
|
||||
| `govoplan-mail` | `domain` | `../govoplan-mail` | [govoplan-mail](https://git.add-ideas.de/GovOPlaN/govoplan-mail) |
|
||||
| `govoplan-notifications` | `platform` | `../govoplan-notifications` | [govoplan-notifications](https://git.add-ideas.de/GovOPlaN/govoplan-notifications) |
|
||||
| `govoplan-ops` | `platform` | `../govoplan-ops` | [govoplan-ops](https://git.add-ideas.de/GovOPlaN/govoplan-ops) |
|
||||
| `govoplan-organizations` | `platform` | `../govoplan-organizations` | [govoplan-organizations](https://git.add-ideas.de/GovOPlaN/govoplan-organizations) |
|
||||
| `govoplan-payments` | `domain` | `../govoplan-payments` | [govoplan-payments](https://git.add-ideas.de/GovOPlaN/govoplan-payments) |
|
||||
| `govoplan-permits` | `domain` | `../govoplan-permits` | [govoplan-permits](https://git.add-ideas.de/GovOPlaN/govoplan-permits) |
|
||||
| `govoplan-policy` | `platform` | `../govoplan-policy` | [govoplan-policy](https://git.add-ideas.de/GovOPlaN/govoplan-policy) |
|
||||
| `govoplan-poll` | `domain` | `../govoplan-poll` | [govoplan-poll](https://git.add-ideas.de/GovOPlaN/govoplan-poll) |
|
||||
| `govoplan-portal` | `domain` | `../govoplan-portal` | [govoplan-portal](https://git.add-ideas.de/GovOPlaN/govoplan-portal) |
|
||||
| `govoplan-postbox` | `domain` | `../govoplan-postbox` | [govoplan-postbox](https://git.add-ideas.de/GovOPlaN/govoplan-postbox) |
|
||||
| `govoplan-procurement` | `domain` | `../govoplan-procurement` | [govoplan-procurement](https://git.add-ideas.de/GovOPlaN/govoplan-procurement) |
|
||||
| `govoplan-projects` | `domain` | `../govoplan-projects` | [govoplan-projects](https://git.add-ideas.de/GovOPlaN/govoplan-projects) |
|
||||
| `govoplan-records` | `domain` | `../govoplan-records` | [govoplan-records](https://git.add-ideas.de/GovOPlaN/govoplan-records) |
|
||||
| `govoplan-reporting` | `domain` | `../govoplan-reporting` | [govoplan-reporting](https://git.add-ideas.de/GovOPlaN/govoplan-reporting) |
|
||||
| `govoplan-resources` | `domain` | `../govoplan-resources` | [govoplan-resources](https://git.add-ideas.de/GovOPlaN/govoplan-resources) |
|
||||
| `govoplan-risk-compliance` | `domain` | `../govoplan-risk-compliance` | [govoplan-risk-compliance](https://git.add-ideas.de/GovOPlaN/govoplan-risk-compliance) |
|
||||
| `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-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-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) |
|
||||
|
||||
## Connector
|
||||
|
||||
| Repository | Subtype | Local path | Gitea |
|
||||
| --- | --- | --- | --- |
|
||||
| `govoplan-connectors` | `connector-hub` | `../govoplan-connectors` | [govoplan-connectors](https://git.add-ideas.de/add-ideas/govoplan-connectors) |
|
||||
| `govoplan-fit-connect` | `standard` | `../govoplan-fit-connect` | [govoplan-fit-connect](https://git.add-ideas.de/add-ideas/govoplan-fit-connect) |
|
||||
| `govoplan-rest` | `protocol` | `../govoplan-rest` | [govoplan-rest](https://git.add-ideas.de/add-ideas/govoplan-rest) |
|
||||
| `govoplan-soap` | `protocol` | `../govoplan-soap` | [govoplan-soap](https://git.add-ideas.de/add-ideas/govoplan-soap) |
|
||||
| `govoplan-xoev` | `standard` | `../govoplan-xoev` | [govoplan-xoev](https://git.add-ideas.de/add-ideas/govoplan-xoev) |
|
||||
| `govoplan-xrechnung` | `standard` | `../govoplan-xrechnung` | [govoplan-xrechnung](https://git.add-ideas.de/add-ideas/govoplan-xrechnung) |
|
||||
| `govoplan-xta-osci` | `standard` | `../govoplan-xta-osci` | [govoplan-xta-osci](https://git.add-ideas.de/add-ideas/govoplan-xta-osci) |
|
||||
| `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/GovOPlaN/govoplan-fit-connect) |
|
||||
| `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/GovOPlaN/govoplan-soap) |
|
||||
| `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/GovOPlaN/govoplan-xrechnung) |
|
||||
| `govoplan-xta-osci` | `standard` | `../govoplan-xta-osci` | [govoplan-xta-osci](https://git.add-ideas.de/GovOPlaN/govoplan-xta-osci) |
|
||||
|
||||
## Website
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ tools/checks/security-audit/run.sh --mode ci --scope current --strict
|
||||
## Audit Burndown Workflow
|
||||
|
||||
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.
|
||||
|
||||
Use the tracker issue for:
|
||||
@@ -162,7 +162,14 @@ clusters that cross module ownership or make behavior harder to change safely.
|
||||
|
||||
The regular `Security Audit` workflow reuses the fingerprinted toolbox image
|
||||
when the Docker daemon is persistent, which is the normal case for the
|
||||
self-hosted Gitea runner using the host Docker socket. The separate
|
||||
self-hosted Gitea runner using the host Docker socket. Trusted push, schedule,
|
||||
and manual runs scan all registered repositories; authenticated SSH is used
|
||||
only for the private website repository. The wrapper inspects the Actions job
|
||||
mount table and forwards only the narrowest writable mount covering the audit
|
||||
scope; it never inherits the job's Docker socket or unrelated runner mounts.
|
||||
Pull-request audit runs stay disabled while the audit runner exposes its host
|
||||
Docker socket: PR-controlled audit code must run on a disposable or rootless
|
||||
runner without host-socket access. The separate
|
||||
`Security Audit Toolbox Update` workflow runs weekly with
|
||||
`SECURITY_AUDIT_UPDATE=1`; it pulls current base images and re-resolves the
|
||||
allowed tool version ranges into a refreshed local image.
|
||||
|
||||
@@ -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,
|
||||
configuration packages, and release provenance into one administrator journey.
|
||||
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
|
||||
|
||||
|
||||
137
docs/VIEWS_ARCHITECTURE.md
Normal file
137
docs/VIEWS_ARCHITECTURE.md
Normal file
@@ -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
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$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",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -507,7 +507,7 @@
|
||||
{
|
||||
"kind": "issue",
|
||||
"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": [],
|
||||
@@ -793,7 +793,7 @@
|
||||
{
|
||||
"kind": "issue",
|
||||
"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": [],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$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",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$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",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -149,12 +149,36 @@
|
||||
"description": "GovOPlaN core runner, shared primitives, shell, or extension points.",
|
||||
"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",
|
||||
"color": "c5def5",
|
||||
"description": "GovOPlaN Dms module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/docs",
|
||||
"color": "c5def5",
|
||||
"description": "GovOPlaN Docs module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/dist-lists",
|
||||
"color": "0e8a16",
|
||||
@@ -191,6 +215,12 @@
|
||||
"description": "GovOPlaN Forms module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/helpdesk",
|
||||
"color": "c2e0c6",
|
||||
"description": "GovOPlaN Helpdesk module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/identity-trust",
|
||||
"color": "d4c5f9",
|
||||
@@ -275,12 +305,24 @@
|
||||
"description": "GovOPlaN Postbox module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/projects",
|
||||
"color": "5319e7",
|
||||
"description": "GovOPlaN Projects module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/reporting",
|
||||
"color": "c2e0c6",
|
||||
"description": "GovOPlaN Reporting module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/risk-compliance",
|
||||
"color": "b60205",
|
||||
"description": "GovOPlaN Risk Compliance module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/search",
|
||||
"color": "bfdadc",
|
||||
@@ -311,6 +353,24 @@
|
||||
"description": "GovOPlaN Tenancy module behavior or integration.",
|
||||
"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",
|
||||
"color": "f9d0c4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$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",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$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",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$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",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
|
||||
@@ -1,73 +1,78 @@
|
||||
{
|
||||
"version": 1,
|
||||
"organization": "add-ideas",
|
||||
"organization": "GovOPlaN",
|
||||
"default_parent": "/mnt/DATA/git",
|
||||
"repositories": [
|
||||
{"name": "govoplan", "category": "system", "subtype": "meta", "remote": "git@git.add-ideas.de:add-ideas/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-access", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/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-admin", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/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-approvals", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/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-audit", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/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-calendar", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/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-cases", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/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-committee", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/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-consultation", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/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-dashboard", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/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-dist-lists", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-dist-lists.git", "path": "govoplan-dist-lists"},
|
||||
{"name": "govoplan-docs", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-docs.git", "path": "govoplan-docs"},
|
||||
{"name": "govoplan-erp", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-erp.git", "path": "govoplan-erp"},
|
||||
{"name": "govoplan-evaluation", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-evaluation.git", "path": "govoplan-evaluation"},
|
||||
{"name": "govoplan-facilities", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-facilities.git", "path": "govoplan-facilities"},
|
||||
{"name": "govoplan-files", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-files.git", "path": "govoplan-files"},
|
||||
{"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-forms", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-forms.git", "path": "govoplan-forms"},
|
||||
{"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-grants", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-grants.git", "path": "govoplan-grants"},
|
||||
{"name": "govoplan-helpdesk", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-helpdesk.git", "path": "govoplan-helpdesk"},
|
||||
{"name": "govoplan-identity", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-identity.git", "path": "govoplan-identity"},
|
||||
{"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-idm", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-idm.git", "path": "govoplan-idm"},
|
||||
{"name": "govoplan-inspections", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-inspections.git", "path": "govoplan-inspections"},
|
||||
{"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-learning", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-learning.git", "path": "govoplan-learning"},
|
||||
{"name": "govoplan-ledger", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-ledger.git", "path": "govoplan-ledger"},
|
||||
{"name": "govoplan-mail", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-mail.git", "path": "govoplan-mail"},
|
||||
{"name": "govoplan-notifications", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-notifications.git", "path": "govoplan-notifications"},
|
||||
{"name": "govoplan-ops", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-ops.git", "path": "govoplan-ops"},
|
||||
{"name": "govoplan-organizations", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-organizations.git", "path": "govoplan-organizations"},
|
||||
{"name": "govoplan-payments", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-payments.git", "path": "govoplan-payments"},
|
||||
{"name": "govoplan-permits", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-permits.git", "path": "govoplan-permits"},
|
||||
{"name": "govoplan-policy", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-policy.git", "path": "govoplan-policy"},
|
||||
{"name": "govoplan-poll", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-poll.git", "path": "govoplan-poll"},
|
||||
{"name": "govoplan-portal", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-portal.git", "path": "govoplan-portal"},
|
||||
{"name": "govoplan-postbox", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-postbox.git", "path": "govoplan-postbox"},
|
||||
{"name": "govoplan-procurement", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-procurement.git", "path": "govoplan-procurement"},
|
||||
{"name": "govoplan-records", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-records.git", "path": "govoplan-records"},
|
||||
{"name": "govoplan-reporting", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-reporting.git", "path": "govoplan-reporting"},
|
||||
{"name": "govoplan-resources", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-resources.git", "path": "govoplan-resources"},
|
||||
{"name": "govoplan-rest", "category": "connector", "subtype": "protocol", "remote": "git@git.add-ideas.de:add-ideas/govoplan-rest.git", "path": "govoplan-rest"},
|
||||
{"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-scheduling", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-scheduling.git", "path": "govoplan-scheduling"},
|
||||
{"name": "govoplan-search", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-search.git", "path": "govoplan-search"},
|
||||
{"name": "govoplan-soap", "category": "connector", "subtype": "protocol", "remote": "git@git.add-ideas.de:add-ideas/govoplan-soap.git", "path": "govoplan-soap"},
|
||||
{"name": "govoplan-tasks", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-tasks.git", "path": "govoplan-tasks"},
|
||||
{"name": "govoplan-templates", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-templates.git", "path": "govoplan-templates"},
|
||||
{"name": "govoplan-tenancy", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/govoplan-tenancy.git", "path": "govoplan-tenancy"},
|
||||
{"name": "govoplan-transparency", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:add-ideas/govoplan-transparency.git", "path": "govoplan-transparency"},
|
||||
{"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"},
|
||||
{"name": "govoplan-workflow", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:add-ideas/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-xrechnung", "category": "connector", "subtype": "standard", "remote": "git@git.add-ideas.de:add-ideas/govoplan-xrechnung.git", "path": "govoplan-xrechnung"},
|
||||
{"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", "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:GovOPlaN/govoplan-core.git", "path": "govoplan-core"},
|
||||
{"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:GovOPlaN/govoplan-addresses.git", "path": "govoplan-addresses"},
|
||||
{"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:GovOPlaN/govoplan-appointments.git", "path": "govoplan-appointments"},
|
||||
{"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:GovOPlaN/govoplan-assets.git", "path": "govoplan-assets"},
|
||||
{"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:GovOPlaN/govoplan-booking.git", "path": "govoplan-booking"},
|
||||
{"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:GovOPlaN/govoplan-campaign.git", "path": "govoplan-campaign"},
|
||||
{"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:GovOPlaN/govoplan-certificates.git", "path": "govoplan-certificates"},
|
||||
{"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:GovOPlaN/govoplan-connectors.git", "path": "govoplan-connectors"},
|
||||
{"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:GovOPlaN/govoplan-contracts.git", "path": "govoplan-contracts"},
|
||||
{"name": "govoplan-dashboard", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-dashboard.git", "path": "govoplan-dashboard"},
|
||||
{"name": "govoplan-dataflow", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-dataflow.git", "path": "govoplan-dataflow"},
|
||||
{"name": "govoplan-datasources", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-datasources.git", "path": "govoplan-datasources"},
|
||||
{"name": "govoplan-dms", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-dms.git", "path": "govoplan-dms"},
|
||||
{"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-docs", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-docs.git", "path": "govoplan-docs"},
|
||||
{"name": "govoplan-erp", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-erp.git", "path": "govoplan-erp"},
|
||||
{"name": "govoplan-evaluation", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-evaluation.git", "path": "govoplan-evaluation"},
|
||||
{"name": "govoplan-facilities", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-facilities.git", "path": "govoplan-facilities"},
|
||||
{"name": "govoplan-files", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-files.git", "path": "govoplan-files"},
|
||||
{"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-forms", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-forms.git", "path": "govoplan-forms"},
|
||||
{"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-grants", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-grants.git", "path": "govoplan-grants"},
|
||||
{"name": "govoplan-helpdesk", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-helpdesk.git", "path": "govoplan-helpdesk"},
|
||||
{"name": "govoplan-identity", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-identity.git", "path": "govoplan-identity"},
|
||||
{"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-idm", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-idm.git", "path": "govoplan-idm"},
|
||||
{"name": "govoplan-inspections", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-inspections.git", "path": "govoplan-inspections"},
|
||||
{"name": "govoplan-learning", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-learning.git", "path": "govoplan-learning"},
|
||||
{"name": "govoplan-ledger", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-ledger.git", "path": "govoplan-ledger"},
|
||||
{"name": "govoplan-mail", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-mail.git", "path": "govoplan-mail"},
|
||||
{"name": "govoplan-notifications", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-notifications.git", "path": "govoplan-notifications"},
|
||||
{"name": "govoplan-ops", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-ops.git", "path": "govoplan-ops"},
|
||||
{"name": "govoplan-organizations", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-organizations.git", "path": "govoplan-organizations"},
|
||||
{"name": "govoplan-payments", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-payments.git", "path": "govoplan-payments"},
|
||||
{"name": "govoplan-permits", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-permits.git", "path": "govoplan-permits"},
|
||||
{"name": "govoplan-policy", "category": "module", "subtype": "platform", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-policy.git", "path": "govoplan-policy"},
|
||||
{"name": "govoplan-poll", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-poll.git", "path": "govoplan-poll"},
|
||||
{"name": "govoplan-portal", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-portal.git", "path": "govoplan-portal"},
|
||||
{"name": "govoplan-postbox", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-postbox.git", "path": "govoplan-postbox"},
|
||||
{"name": "govoplan-procurement", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-procurement.git", "path": "govoplan-procurement"},
|
||||
{"name": "govoplan-projects", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-projects.git", "path": "govoplan-projects"},
|
||||
{"name": "govoplan-records", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-records.git", "path": "govoplan-records"},
|
||||
{"name": "govoplan-reporting", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-reporting.git", "path": "govoplan-reporting"},
|
||||
{"name": "govoplan-resources", "category": "module", "subtype": "domain", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-resources.git", "path": "govoplan-resources"},
|
||||
{"name": "govoplan-rest", "category": "connector", "subtype": "protocol", "remote": "git@git.add-ideas.de:GovOPlaN/govoplan-rest.git", "path": "govoplan-rest"},
|
||||
{"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-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-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-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": "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:GovOPlaN/govoplan-xoev.git", "path": "govoplan-xoev"},
|
||||
{"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"}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -19,6 +19,14 @@
|
||||
-e ../govoplan-mail
|
||||
-e ../govoplan-campaign
|
||||
-e ../govoplan-calendar
|
||||
-e ../govoplan-connectors
|
||||
-e ../govoplan-datasources
|
||||
-e ../govoplan-dataflow
|
||||
-e ../govoplan-workflow
|
||||
-e ../govoplan-views
|
||||
-e ../govoplan-search
|
||||
-e ../govoplan-risk-compliance
|
||||
-e ../govoplan-postbox
|
||||
-e ../govoplan-poll
|
||||
-e ../govoplan-scheduling
|
||||
-e ../govoplan-notifications
|
||||
@@ -32,5 +40,7 @@ idna>=3.15
|
||||
jsonschema>=4,<5
|
||||
pip>=26.1.2
|
||||
pip-audit>=2.9,<3
|
||||
pytest>=9.0.3,<10
|
||||
pygments>=2.20,<3
|
||||
python-multipart>=0.0.31
|
||||
ruff>=0.14,<1
|
||||
|
||||
5
requirements-release-tests.txt
Normal file
5
requirements-release-tests.txt
Normal file
@@ -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
|
||||
@@ -1,18 +1,18 @@
|
||||
# Whole-product release install from immutable, independently versioned module tags.
|
||||
# Only add a module after its referenced tag has been published.
|
||||
../govoplan-core[server]
|
||||
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-tenancy.git@v0.1.8
|
||||
govoplan-organizations @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-organizations.git@v0.1.8
|
||||
govoplan-identity @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-identity.git@v0.1.8
|
||||
govoplan-idm @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-idm.git@v0.1.8
|
||||
govoplan-access @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-access.git@v0.1.8
|
||||
govoplan-admin @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-admin.git@v0.1.8
|
||||
govoplan-policy @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-policy.git@v0.1.8
|
||||
govoplan-audit @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-audit.git@v0.1.8
|
||||
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-dashboard.git@v0.1.8
|
||||
govoplan-files @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git@v0.1.8
|
||||
govoplan-mail @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-mail.git@v0.1.8
|
||||
govoplan-campaign @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-campaign.git@v0.1.11
|
||||
govoplan-calendar @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-calendar.git@v0.1.8
|
||||
govoplan-docs @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-docs.git@v0.1.8
|
||||
govoplan-ops @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-ops.git@v0.1.8
|
||||
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.8
|
||||
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.8
|
||||
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.8
|
||||
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.8
|
||||
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.8
|
||||
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.8
|
||||
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.8
|
||||
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.8
|
||||
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.8
|
||||
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.8
|
||||
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.10
|
||||
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.11
|
||||
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.8
|
||||
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.8
|
||||
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.8
|
||||
|
||||
49
tests/test_gitea_sync_wiki.py
Normal file
49
tests/test_gitea_sync_wiki.py
Normal file
@@ -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()
|
||||
@@ -77,6 +77,76 @@ class PythonEnvironmentSyncTests(unittest.TestCase):
|
||||
self.assertIn(str(root / "govoplan-core"), 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_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__":
|
||||
unittest.main()
|
||||
|
||||
@@ -24,7 +24,7 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
|
||||
"module_id": "access",
|
||||
"version": "0.1.11",
|
||||
"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 +37,7 @@ class ReleaseCatalogEntrySynthesisTests(unittest.TestCase):
|
||||
"govoplan-scheduling": "0.1.11",
|
||||
},
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
39
tests/test_release_git_state.py
Normal file
39
tests/test_release_git_state.py
Normal file
@@ -0,0 +1,39 @@
|
||||
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_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
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from tools.checks.release_integration import (
|
||||
SOURCE_COUPLED_CORE_TESTS,
|
||||
@@ -10,6 +12,7 @@ from tools.checks.release_integration import (
|
||||
artifact_contract_issues,
|
||||
filter_test_suite,
|
||||
release_package_names,
|
||||
run_module_release_tests,
|
||||
)
|
||||
|
||||
|
||||
@@ -32,9 +35,9 @@ class ReleaseIntegrationTests(unittest.TestCase):
|
||||
"\n".join(
|
||||
(
|
||||
"../govoplan-core[server]",
|
||||
"govoplan-idm @ git+ssh://git@example.test/add-ideas/govoplan-idm.git@v0.1.8",
|
||||
"govoplan-campaign @ git+ssh://git@example.test/add-ideas/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",
|
||||
"govoplan-campaign @ git+ssh://git@example.test/GovOPlaN/govoplan-campaign.git@v0.1.8",
|
||||
"govoplan-idm @ git+ssh://git@example.test/GovOPlaN/govoplan-idm.git@v0.1.8",
|
||||
"other-package==1.0",
|
||||
)
|
||||
),
|
||||
@@ -89,8 +92,67 @@ class ReleaseIntegrationTests(unittest.TestCase):
|
||||
|
||||
self.assertLess(artifact_check, core_tests)
|
||||
self.assertLess(core_tests, module_tests)
|
||||
self.assertNotIn("unittest discover", 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:
|
||||
meta_root = Path(__file__).resolve().parents[1]
|
||||
script = (meta_root / "tools" / "checks" / "check-module-matrix.sh").read_text(encoding="utf-8")
|
||||
|
||||
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.assertNotIn('"$PYTHON" -m unittest tests.test_module_system', script)
|
||||
|
||||
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__":
|
||||
unittest.main()
|
||||
|
||||
@@ -156,7 +156,7 @@ def dashboard(*, workspace: Path, version: str) -> ReleaseDashboard:
|
||||
name="govoplan-files",
|
||||
category="module",
|
||||
subtype="infrastructure",
|
||||
remote="git@example.test:add-ideas/govoplan-files.git",
|
||||
remote="git@example.test:GovOPlaN/govoplan-files.git",
|
||||
path="govoplan-files",
|
||||
),
|
||||
absolute_path=str(workspace / "govoplan-files"),
|
||||
|
||||
364
tests/test_repository_bootstrap.py
Normal file
364
tests/test_repository_bootstrap.py
Normal file
@@ -0,0 +1,364 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
SCRIPT = META_ROOT / "tools" / "repo" / "bootstrap-repositories.py"
|
||||
|
||||
|
||||
def load_bootstrap_module():
|
||||
spec = importlib.util.spec_from_file_location("bootstrap_repositories", SCRIPT)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Could not load {SCRIPT}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class RepositoryBootstrapTests(unittest.TestCase):
|
||||
def test_public_https_transport_rewrites_registered_gitea_remotes(self) -> None:
|
||||
bootstrap = load_bootstrap_module()
|
||||
|
||||
self.assertEqual(
|
||||
"https://git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||
bootstrap.clone_remote(
|
||||
"git@git.add-ideas.de:GovOPlaN/govoplan-core.git",
|
||||
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
"https://git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||
bootstrap.clone_remote(
|
||||
"ssh://git@git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
"https://git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||
bootstrap.clone_remote(
|
||||
"https://git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||
),
|
||||
)
|
||||
|
||||
def test_registered_transport_preserves_the_manifest_remote(self) -> None:
|
||||
bootstrap = load_bootstrap_module()
|
||||
remote = "git@example.test:private/repository.git"
|
||||
|
||||
self.assertEqual(
|
||||
remote,
|
||||
bootstrap.clone_remote(
|
||||
remote,
|
||||
transport=bootstrap.REGISTERED_TRANSPORT,
|
||||
),
|
||||
)
|
||||
|
||||
def test_public_https_transport_fails_closed_for_other_hosts(self) -> None:
|
||||
bootstrap = load_bootstrap_module()
|
||||
|
||||
unsafe_remotes = (
|
||||
"git@example.test:GovOPlaN/govoplan-core.git",
|
||||
"https://token@git.add-ideas.de/GovOPlaN/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/GovOPlaN/govoplan-core.git?ref=main",
|
||||
"ssh://root@git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||
)
|
||||
for remote in unsafe_remotes:
|
||||
with self.subTest(remote=remote), self.assertRaises(ValueError):
|
||||
bootstrap.clone_remote(
|
||||
remote,
|
||||
transport=bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||
)
|
||||
|
||||
def test_main_clones_missing_repositories_over_public_https(self) -> None:
|
||||
bootstrap = load_bootstrap_module()
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
||||
root = Path(directory)
|
||||
parent = root / "checkouts"
|
||||
parent.mkdir()
|
||||
root.joinpath("repositories.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"default_parent": str(parent),
|
||||
"repositories": [
|
||||
{
|
||||
"name": "govoplan-core",
|
||||
"path": "govoplan-core",
|
||||
"remote": (
|
||||
"git@git.add-ideas.de:"
|
||||
"GovOPlaN/govoplan-core.git"
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with (
|
||||
patch.object(bootstrap, "ROOT", root),
|
||||
patch.object(bootstrap.subprocess, "run") as runner,
|
||||
):
|
||||
status = bootstrap.main(
|
||||
[
|
||||
"--parent",
|
||||
str(parent),
|
||||
"--transport",
|
||||
bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(0, status)
|
||||
runner.assert_called_once_with(
|
||||
[
|
||||
"git",
|
||||
"-c",
|
||||
"credential.helper=",
|
||||
"clone",
|
||||
"--",
|
||||
"https://git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||
str(parent / "govoplan-core"),
|
||||
],
|
||||
check=True,
|
||||
env=runner.call_args.kwargs["env"],
|
||||
)
|
||||
environment = runner.call_args.kwargs["env"]
|
||||
self.assertEqual("/bin/false", environment["GIT_ASKPASS"])
|
||||
self.assertEqual("0", environment["GIT_TERMINAL_PROMPT"])
|
||||
|
||||
def test_main_validates_every_remote_before_cloning(self) -> None:
|
||||
bootstrap = load_bootstrap_module()
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
||||
root = Path(directory)
|
||||
parent = root / "checkouts"
|
||||
parent.mkdir()
|
||||
root.joinpath("repositories.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"default_parent": str(parent),
|
||||
"repositories": [
|
||||
{
|
||||
"name": "govoplan-core",
|
||||
"path": "govoplan-core",
|
||||
"remote": (
|
||||
"git@git.add-ideas.de:"
|
||||
"GovOPlaN/govoplan-core.git"
|
||||
),
|
||||
},
|
||||
{
|
||||
"name": "unsafe",
|
||||
"path": "unsafe",
|
||||
"remote": "git@example.test:private/unsafe.git",
|
||||
},
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with (
|
||||
patch.object(bootstrap, "ROOT", root),
|
||||
patch.object(bootstrap.subprocess, "run") as runner,
|
||||
self.assertRaises(ValueError),
|
||||
):
|
||||
bootstrap.main(
|
||||
[
|
||||
"--parent",
|
||||
str(parent),
|
||||
"--transport",
|
||||
bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||
]
|
||||
)
|
||||
|
||||
runner.assert_not_called()
|
||||
|
||||
def test_main_preserves_an_explicit_private_repository_transport(self) -> None:
|
||||
bootstrap = load_bootstrap_module()
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
||||
root = Path(directory)
|
||||
parent = root / "checkouts"
|
||||
parent.mkdir()
|
||||
root.joinpath("repositories.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"default_parent": str(parent),
|
||||
"repositories": [
|
||||
{
|
||||
"name": "website",
|
||||
"path": "website",
|
||||
"remote": (
|
||||
"git@git.add-ideas.de:"
|
||||
"add-ideas/addideas-govoplan-website.git"
|
||||
),
|
||||
"bootstrap_transport": (
|
||||
bootstrap.REGISTERED_TRANSPORT
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with (
|
||||
patch.object(bootstrap, "ROOT", root),
|
||||
patch.object(bootstrap.subprocess, "run") as runner,
|
||||
):
|
||||
status = bootstrap.main(
|
||||
[
|
||||
"--parent",
|
||||
str(parent),
|
||||
"--transport",
|
||||
bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(0, status)
|
||||
command = runner.call_args.args[0]
|
||||
self.assertEqual(
|
||||
"git@git.add-ideas.de:add-ideas/addideas-govoplan-website.git",
|
||||
command[-2],
|
||||
)
|
||||
|
||||
def test_main_limits_bootstrap_to_selected_repositories(self) -> None:
|
||||
bootstrap = load_bootstrap_module()
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
||||
root = Path(directory)
|
||||
parent = root / "checkouts"
|
||||
parent.mkdir()
|
||||
root.joinpath("repositories.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"default_parent": str(parent),
|
||||
"repositories": [
|
||||
{
|
||||
"name": name,
|
||||
"path": name,
|
||||
"remote": (
|
||||
"git@git.add-ideas.de:GovOPlaN/"
|
||||
f"{name}.git"
|
||||
),
|
||||
}
|
||||
for name in ("govoplan-core", "govoplan-poll")
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
for repository_filter in (
|
||||
["--repo", "govoplan-core"],
|
||||
["--exclude-repo", "govoplan-poll"],
|
||||
):
|
||||
with (
|
||||
self.subTest(repository_filter=repository_filter),
|
||||
patch.object(bootstrap, "ROOT", root),
|
||||
patch.object(bootstrap.subprocess, "run") as runner,
|
||||
):
|
||||
status = bootstrap.main(
|
||||
[
|
||||
"--parent",
|
||||
str(parent),
|
||||
"--transport",
|
||||
bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||
*repository_filter,
|
||||
]
|
||||
)
|
||||
self.assertEqual(0, status)
|
||||
runner.assert_called_once()
|
||||
self.assertEqual(
|
||||
"https://git.add-ideas.de/GovOPlaN/govoplan-core.git",
|
||||
runner.call_args.args[0][-2],
|
||||
)
|
||||
|
||||
def test_main_rejects_unknown_or_conflicting_repository_filters(self) -> None:
|
||||
bootstrap = load_bootstrap_module()
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
||||
root = Path(directory)
|
||||
parent = root / "checkouts"
|
||||
parent.mkdir()
|
||||
root.joinpath("repositories.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"default_parent": str(parent),
|
||||
"repositories": [
|
||||
{
|
||||
"name": "govoplan-core",
|
||||
"path": "govoplan-core",
|
||||
"remote": (
|
||||
"git@git.add-ideas.de:"
|
||||
"GovOPlaN/govoplan-core.git"
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with (
|
||||
patch.object(bootstrap, "ROOT", root),
|
||||
patch.object(bootstrap.subprocess, "run") as runner,
|
||||
):
|
||||
with self.assertRaisesRegex(ValueError, "unknown registered"):
|
||||
bootstrap.main(["--repo", "govoplan-missing"])
|
||||
with self.assertRaisesRegex(ValueError, "selected and excluded"):
|
||||
bootstrap.main(
|
||||
[
|
||||
"--repo",
|
||||
"govoplan-core",
|
||||
"--exclude-repo",
|
||||
"govoplan-core",
|
||||
]
|
||||
)
|
||||
|
||||
runner.assert_not_called()
|
||||
|
||||
def test_check_reports_missing_without_cloning(self) -> None:
|
||||
bootstrap = load_bootstrap_module()
|
||||
with tempfile.TemporaryDirectory(prefix="govoplan-bootstrap-") as directory:
|
||||
root = Path(directory)
|
||||
parent = root / "checkouts"
|
||||
parent.mkdir()
|
||||
root.joinpath("repositories.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"default_parent": str(parent),
|
||||
"repositories": [
|
||||
{
|
||||
"name": "govoplan-core",
|
||||
"path": "govoplan-core",
|
||||
"remote": (
|
||||
"git@git.add-ideas.de:"
|
||||
"GovOPlaN/govoplan-core.git"
|
||||
),
|
||||
}
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
with (
|
||||
patch.object(bootstrap, "ROOT", root),
|
||||
patch.object(bootstrap.subprocess, "run") as runner,
|
||||
):
|
||||
status = bootstrap.main(
|
||||
[
|
||||
"--check",
|
||||
"--parent",
|
||||
str(parent),
|
||||
"--transport",
|
||||
bootstrap.PUBLIC_HTTPS_TRANSPORT,
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(1, status)
|
||||
runner.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
248
tests/test_security_audit_mount_resolver.py
Normal file
248
tests/test_security_audit_mount_resolver.py
Normal file
@@ -0,0 +1,248 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
RESOLVER = (
|
||||
META_ROOT / "tools" / "checks" / "security-audit" / "resolve_workspace_mount.py"
|
||||
)
|
||||
|
||||
|
||||
def load_resolver_module():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"security_audit_mount_resolver",
|
||||
RESOLVER,
|
||||
)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Could not load {RESOLVER}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class SecurityAuditMountResolverTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.resolver = load_resolver_module()
|
||||
self.root = "/workspace/GovOPlaN/govoplan/govoplan"
|
||||
self.workspace = "/workspace/GovOPlaN/govoplan"
|
||||
|
||||
def test_resolves_only_the_named_workspace_volume_for_both_scopes(self) -> None:
|
||||
mounts = [
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "/var/run/docker.sock",
|
||||
"Destination": "/var/run/docker.sock",
|
||||
"RW": True,
|
||||
},
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "actions-workspace",
|
||||
"Source": "/var/lib/docker/volumes/actions-workspace/_data",
|
||||
"Destination": self.workspace,
|
||||
"RW": True,
|
||||
},
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "actions-environment",
|
||||
"Source": "/var/lib/docker/volumes/actions-environment/_data",
|
||||
"Destination": "/var/run/act",
|
||||
"RW": True,
|
||||
},
|
||||
]
|
||||
|
||||
for scope in ("current", "govoplan"):
|
||||
with self.subTest(scope=scope):
|
||||
self.assertEqual(
|
||||
(f"type=volume,source=actions-workspace,target={self.workspace}"),
|
||||
self.resolver.resolve_workspace_mount(
|
||||
mounts,
|
||||
root=self.root,
|
||||
scope=scope,
|
||||
reports_dir="audit-reports",
|
||||
),
|
||||
)
|
||||
|
||||
def test_resolves_a_workspace_bind_without_other_mounts(self) -> None:
|
||||
mounts = [
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "/srv/gitea/actions/task-123",
|
||||
"Destination": self.workspace,
|
||||
"RW": True,
|
||||
},
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "/var/run/docker.sock",
|
||||
"Destination": "/var/run/docker.sock",
|
||||
"RW": True,
|
||||
},
|
||||
]
|
||||
|
||||
self.assertEqual(
|
||||
(f"type=bind,source=/srv/gitea/actions/task-123,target={self.workspace}"),
|
||||
self.resolver.resolve_workspace_mount(
|
||||
mounts,
|
||||
root=self.root,
|
||||
scope="govoplan",
|
||||
reports_dir="audit-reports",
|
||||
),
|
||||
)
|
||||
|
||||
def test_nested_repository_mount_is_valid_only_for_current_scope(self) -> None:
|
||||
mounts = [
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "all-repositories",
|
||||
"Destination": self.workspace,
|
||||
"RW": True,
|
||||
},
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "current-repository",
|
||||
"Destination": self.root,
|
||||
"RW": True,
|
||||
},
|
||||
]
|
||||
|
||||
self.assertIn(
|
||||
"source=current-repository",
|
||||
self.resolver.resolve_workspace_mount(
|
||||
mounts,
|
||||
root=self.root,
|
||||
scope="current",
|
||||
reports_dir="audit-reports",
|
||||
),
|
||||
)
|
||||
with self.assertRaisesRegex(
|
||||
self.resolver.MountResolutionError,
|
||||
"nested job-container mounts",
|
||||
):
|
||||
self.resolver.resolve_workspace_mount(
|
||||
mounts,
|
||||
root=self.root,
|
||||
scope="govoplan",
|
||||
reports_dir="audit-reports",
|
||||
)
|
||||
|
||||
def test_rejects_unsafe_or_unusable_mount_layouts(self) -> None:
|
||||
cases = {
|
||||
"missing": [],
|
||||
"path-boundary": [
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "wrong-workspace",
|
||||
"Destination": "/workspace/GovOPlaN/govoplan-other",
|
||||
"RW": True,
|
||||
}
|
||||
],
|
||||
"read-only": [
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "actions-workspace",
|
||||
"Destination": self.workspace,
|
||||
"RW": False,
|
||||
}
|
||||
],
|
||||
"ambiguous": [
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": name,
|
||||
"Destination": self.workspace,
|
||||
"RW": True,
|
||||
}
|
||||
for name in ("workspace-one", "workspace-two")
|
||||
],
|
||||
"scope-too-narrow": [
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "repository-only",
|
||||
"Destination": self.root,
|
||||
"RW": True,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
for name, mounts in cases.items():
|
||||
with (
|
||||
self.subTest(name=name),
|
||||
self.assertRaises(self.resolver.MountResolutionError),
|
||||
):
|
||||
self.resolver.resolve_workspace_mount(
|
||||
mounts,
|
||||
root=self.root,
|
||||
scope="govoplan",
|
||||
reports_dir="audit-reports",
|
||||
)
|
||||
|
||||
def test_rejects_reports_outside_the_selected_workspace_mount(self) -> None:
|
||||
mounts = [
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "actions-workspace",
|
||||
"Destination": self.workspace,
|
||||
"RW": True,
|
||||
}
|
||||
]
|
||||
|
||||
with self.assertRaisesRegex(
|
||||
self.resolver.MountResolutionError,
|
||||
"reports path .* outside",
|
||||
):
|
||||
self.resolver.resolve_workspace_mount(
|
||||
mounts,
|
||||
root=self.root,
|
||||
scope="current",
|
||||
reports_dir="/tmp/audit-reports",
|
||||
)
|
||||
|
||||
def test_rejects_broad_or_sensitive_workspace_bind_sources(self) -> None:
|
||||
for source in (
|
||||
"/",
|
||||
"/home",
|
||||
"/var/run/docker.sock",
|
||||
"/srv/../etc/shadow",
|
||||
):
|
||||
with (
|
||||
self.subTest(source=source),
|
||||
self.assertRaisesRegex(
|
||||
self.resolver.MountResolutionError,
|
||||
"too broad or sensitive",
|
||||
),
|
||||
):
|
||||
self.resolver.resolve_workspace_mount(
|
||||
[
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": source,
|
||||
"Destination": self.workspace,
|
||||
"RW": True,
|
||||
}
|
||||
],
|
||||
root=self.root,
|
||||
scope="govoplan",
|
||||
reports_dir="audit-reports",
|
||||
)
|
||||
|
||||
with self.assertRaises(self.resolver.MountResolutionError):
|
||||
self.resolver.resolve_workspace_mount(
|
||||
[
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "//var/lib/workspace",
|
||||
"Destination": self.workspace,
|
||||
"RW": True,
|
||||
}
|
||||
],
|
||||
root=self.root,
|
||||
scope="govoplan",
|
||||
reports_dir="audit-reports",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -12,6 +12,7 @@ import unittest
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
AUDIT_SCRIPT = META_ROOT / "tools" / "checks" / "check-security-audit.sh"
|
||||
CONTAINER_RUNNER = META_ROOT / "tools" / "checks" / "security-audit" / "run.sh"
|
||||
|
||||
|
||||
class SecurityAuditWrapperTests(unittest.TestCase):
|
||||
@@ -354,5 +355,162 @@ class SecurityAuditWrapperTests(unittest.TestCase):
|
||||
self.assertEqual(set(manifest["reports"]), checksummed_paths)
|
||||
|
||||
|
||||
class SecurityAuditContainerRunnerTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self._temporary_directory = tempfile.TemporaryDirectory(
|
||||
prefix="govoplan-audit-runner-"
|
||||
)
|
||||
root = Path(self._temporary_directory.name)
|
||||
self.stub_bin = root / "bin"
|
||||
self.stub_bin.mkdir()
|
||||
self.docker_log = root / "docker.jsonl"
|
||||
docker_stub = self.stub_bin / "docker"
|
||||
docker_stub.write_text(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
arguments = sys.argv[1:]
|
||||
with Path(os.environ["DOCKER_STUB_LOG"]).open(
|
||||
"a", encoding="utf-8"
|
||||
) as handle:
|
||||
handle.write(json.dumps(arguments) + "\\n")
|
||||
if arguments and arguments[0] == "version":
|
||||
print("26.1.0")
|
||||
if arguments[:2] == ["container", "inspect"]:
|
||||
print(os.environ["DOCKER_STUB_MOUNTS"])
|
||||
"""
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
docker_stub.chmod(0o755)
|
||||
self.environment = os.environ.copy()
|
||||
self.environment.update(
|
||||
{
|
||||
"DOCKER_STUB_LOG": str(self.docker_log),
|
||||
"DOCKER_STUB_MOUNTS": "[]",
|
||||
"PATH": f"{self.stub_bin}:{self.environment['PATH']}",
|
||||
}
|
||||
)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self._temporary_directory.cleanup()
|
||||
|
||||
def _run(
|
||||
self,
|
||||
*,
|
||||
scope: str,
|
||||
actions_mounts: object | None = None,
|
||||
) -> tuple[subprocess.CompletedProcess[str], list[list[str]]]:
|
||||
self.docker_log.write_text("", encoding="utf-8")
|
||||
environment = self.environment.copy()
|
||||
if actions_mounts is None:
|
||||
environment.pop("GITEA_ACTIONS", None)
|
||||
else:
|
||||
environment["GITEA_ACTIONS"] = "true"
|
||||
environment["DOCKER_STUB_MOUNTS"] = json.dumps(actions_mounts)
|
||||
result = subprocess.run(
|
||||
[
|
||||
"bash",
|
||||
str(CONTAINER_RUNNER),
|
||||
"--mode",
|
||||
"quick",
|
||||
"--scope",
|
||||
scope,
|
||||
],
|
||||
cwd=META_ROOT,
|
||||
env=environment,
|
||||
check=False,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
commands = [
|
||||
json.loads(line)
|
||||
for line in self.docker_log.read_text(encoding="utf-8").splitlines()
|
||||
]
|
||||
return result, commands
|
||||
|
||||
def test_gitea_job_shares_only_its_workspace_mount(self) -> None:
|
||||
mounts = [
|
||||
{
|
||||
"Type": "bind",
|
||||
"Source": "/var/run/docker.sock",
|
||||
"Destination": "/var/run/docker.sock",
|
||||
"RW": True,
|
||||
},
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "govoplan-actions-workspace",
|
||||
"Destination": str(META_ROOT.parent),
|
||||
"RW": True,
|
||||
},
|
||||
{
|
||||
"Type": "volume",
|
||||
"Name": "govoplan-actions-environment",
|
||||
"Destination": "/var/run/act",
|
||||
"RW": True,
|
||||
},
|
||||
]
|
||||
container_id = subprocess.run(
|
||||
["hostname"],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
).stdout.strip()
|
||||
|
||||
for scope in ("current", "govoplan"):
|
||||
with self.subTest(scope=scope):
|
||||
result, commands = self._run(scope=scope, actions_mounts=mounts)
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
run_command = next(
|
||||
command for command in commands if command[0] == "run"
|
||||
)
|
||||
inspect_command = next(
|
||||
command
|
||||
for command in commands
|
||||
if command[:2] == ["container", "inspect"]
|
||||
)
|
||||
self.assertEqual(container_id, inspect_command[-1])
|
||||
mount_index = run_command.index("--mount")
|
||||
self.assertEqual(
|
||||
(
|
||||
"type=volume,source=govoplan-actions-workspace,"
|
||||
f"target={META_ROOT.parent}"
|
||||
),
|
||||
run_command[mount_index + 1],
|
||||
)
|
||||
self.assertNotIn("--volumes-from", run_command)
|
||||
self.assertNotIn("-v", run_command)
|
||||
self.assertNotIn("/var/run/docker.sock", " ".join(run_command))
|
||||
self.assertNotIn("/var/run/act", " ".join(run_command))
|
||||
repository_roots = [
|
||||
value
|
||||
for value in run_command
|
||||
if value.startswith("GOVOPLAN_REPOS_ROOT=")
|
||||
]
|
||||
expected_roots = (
|
||||
[f"GOVOPLAN_REPOS_ROOT={META_ROOT.parent}"]
|
||||
if scope == "govoplan"
|
||||
else []
|
||||
)
|
||||
self.assertEqual(expected_roots, repository_roots)
|
||||
|
||||
def test_non_actions_runner_keeps_the_scoped_bind_mount(self) -> None:
|
||||
result, commands = self._run(scope="govoplan")
|
||||
|
||||
self.assertEqual(0, result.returncode, result.stderr)
|
||||
run_command = next(command for command in commands if command[0] == "run")
|
||||
bind_index = run_command.index("-v")
|
||||
self.assertEqual(
|
||||
f"{META_ROOT.parent}:/workspace",
|
||||
run_command[bind_index + 1],
|
||||
)
|
||||
self.assertNotIn("--mount", run_command)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -6,6 +6,7 @@ import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -20,6 +21,7 @@ from govoplan_release.version_alignment import ( # noqa: E402
|
||||
repository_version_issues,
|
||||
selected_repository_version_issues,
|
||||
)
|
||||
from govoplan_release.git_state import sanitized_git_environment # noqa: E402
|
||||
from govoplan_release.model import ( # noqa: E402
|
||||
CatalogSnapshot,
|
||||
DashboardSummary,
|
||||
@@ -398,6 +400,68 @@ class VersionAlignmentTests(unittest.TestCase):
|
||||
)
|
||||
self.assertTrue(all("must contain" in issue.message for issue in issues))
|
||||
|
||||
def test_annotated_release_tags_work_with_sanitized_git_configuration(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
workspace = Path(tmp)
|
||||
meta = workspace / "govoplan"
|
||||
core = workspace / "govoplan-core"
|
||||
module = workspace / "govoplan-example"
|
||||
meta.mkdir()
|
||||
(core / "webui").mkdir(parents=True)
|
||||
(module / "webui").mkdir(parents=True)
|
||||
backend_ref = "git+ssh://git@example.test/acme/govoplan-example.git@v1.2.3"
|
||||
webui_ref = "git+ssh://git@example.test/acme/govoplan-example.git#v1.2.3"
|
||||
(meta / "requirements-release.txt").write_text(f"govoplan-example @ {backend_ref}\n")
|
||||
(module / "pyproject.toml").write_text('[project]\nname="govoplan-example"\nversion="1.2.3"\n')
|
||||
(module / "webui" / "package.json").write_text(
|
||||
'{"name":"@govoplan/example-webui","version":"1.2.3"}\n'
|
||||
)
|
||||
subprocess.run(["git", "init", "-q", str(module)], check=True)
|
||||
subprocess.run(["git", "-C", str(module), "config", "user.email", "test@example.test"], check=True)
|
||||
subprocess.run(["git", "-C", str(module), "config", "user.name", "Test"], check=True)
|
||||
subprocess.run(["git", "-C", str(module), "add", "pyproject.toml", "webui/package.json"], check=True)
|
||||
subprocess.run(["git", "-C", str(module), "commit", "-qm", "release"], check=True)
|
||||
subprocess.run(
|
||||
["git", "-C", str(module), "tag", "-a", "v1.2.3", "-m", "Release v1.2.3"],
|
||||
check=True,
|
||||
)
|
||||
tagged_commit = subprocess.run(
|
||||
["git", "-C", str(module), "rev-parse", "refs/tags/v1.2.3^{commit}"],
|
||||
check=True,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
).stdout.strip()
|
||||
dependencies = {"@govoplan/example-webui": webui_ref}
|
||||
(core / "pyproject.toml").write_text('[project]\nname="govoplan-core"\nversion="2.0.0"\n')
|
||||
(core / "webui" / "package.release.json").write_text(
|
||||
json.dumps({"version": "2.0.0", "dependencies": dependencies})
|
||||
)
|
||||
(core / "webui" / "package-lock.release.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"packages": {
|
||||
"": {"version": "2.0.0", "dependencies": dependencies},
|
||||
"node_modules/@govoplan/example-webui": {
|
||||
"version": "1.2.3",
|
||||
"resolved": f"git+ssh://git@example.test/acme/govoplan-example.git#{tagged_commit}",
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
git_environment = sanitized_git_environment()
|
||||
git_environment["GIT_TEST_ASSUME_DIFFERENT_OWNER"] = "1"
|
||||
with patch(
|
||||
"govoplan_release.version_alignment.sanitized_git_environment",
|
||||
return_value=git_environment,
|
||||
):
|
||||
composition_issues = release_composition_issues(meta, core_root=core)
|
||||
core_issues = repository_version_issues(core)
|
||||
|
||||
self.assertEqual((), composition_issues)
|
||||
self.assertEqual((), core_issues)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
264
tools/checks/check-datasource-composition.py
Normal file
264
tools/checks/check-datasource-composition.py
Normal file
@@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise connector -> datasource -> dataflow publication capabilities."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from govoplan_connectors.backend.db.models import ConnectorTabularSource
|
||||
from govoplan_core.auth import ApiPrincipal
|
||||
from govoplan_core.core.access import PrincipalRef
|
||||
from govoplan_core.core.dataflows import (
|
||||
DataflowPublicationTarget,
|
||||
DataflowRunRequest,
|
||||
dataflow_run_lifecycle,
|
||||
)
|
||||
from govoplan_core.core.datasources import (
|
||||
DatasourceReadRequest,
|
||||
datasource_catalogue,
|
||||
datasource_lifecycle,
|
||||
datasource_publication,
|
||||
)
|
||||
from govoplan_core.core.modules import ModuleContext
|
||||
from govoplan_core.core.tabular_sources import (
|
||||
TabularSnapshotInput,
|
||||
tabular_snapshot_writer,
|
||||
)
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_core.server.registry import build_platform_registry
|
||||
from govoplan_dataflow.backend.schemas import (
|
||||
GraphEdge,
|
||||
GraphNode,
|
||||
GraphPosition,
|
||||
PipelineGraph,
|
||||
PipelineCreateRequest,
|
||||
PipelinePreviewRequest,
|
||||
)
|
||||
from govoplan_dataflow.backend.db.models import (
|
||||
DataflowPipeline,
|
||||
DataflowPipelineRevision,
|
||||
DataflowRun,
|
||||
)
|
||||
from govoplan_dataflow.backend.service import create_pipeline, preview_pipeline
|
||||
from govoplan_datasources.backend.db.models import (
|
||||
DatasourceMaterializationRecord,
|
||||
DatasourcePayloadRecord,
|
||||
DatasourcePayloadRowRecord,
|
||||
DatasourcePublicationRecord,
|
||||
DatasourceRecord,
|
||||
DatasourceStageRecord,
|
||||
)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
registry = build_platform_registry(
|
||||
("connectors", "datasources", "dataflow", "workflow")
|
||||
)
|
||||
registry.configure_capability_context(
|
||||
ModuleContext(registry=registry, settings=object())
|
||||
)
|
||||
engine = create_engine("sqlite:///:memory:")
|
||||
Base.metadata.create_all(
|
||||
engine,
|
||||
tables=[
|
||||
ConnectorTabularSource.__table__,
|
||||
DatasourceRecord.__table__,
|
||||
DatasourcePayloadRecord.__table__,
|
||||
DatasourcePayloadRowRecord.__table__,
|
||||
DatasourceMaterializationRecord.__table__,
|
||||
DatasourceStageRecord.__table__,
|
||||
DatasourcePublicationRecord.__table__,
|
||||
DataflowPipeline.__table__,
|
||||
DataflowPipelineRevision.__table__,
|
||||
DataflowRun.__table__,
|
||||
],
|
||||
)
|
||||
session_factory = sessionmaker(bind=engine)
|
||||
with session_factory() as session:
|
||||
principal = _principal()
|
||||
writer = tabular_snapshot_writer(registry)
|
||||
lifecycle = datasource_lifecycle(registry)
|
||||
catalogue = datasource_catalogue(registry)
|
||||
publisher = datasource_publication(registry)
|
||||
runner = dataflow_run_lifecycle(registry)
|
||||
if (
|
||||
writer is None
|
||||
or lifecycle is None
|
||||
or catalogue is None
|
||||
or publisher is None
|
||||
or runner is None
|
||||
):
|
||||
raise RuntimeError("Datasource composition capabilities are incomplete.")
|
||||
|
||||
origin = writer.create_snapshot(
|
||||
session,
|
||||
principal,
|
||||
snapshot=TabularSnapshotInput(
|
||||
name="Monthly cases",
|
||||
source_name="connector_monthly_cases",
|
||||
rows=(
|
||||
{"id": 1, "amount": 5},
|
||||
{"id": 2, "amount": 15},
|
||||
),
|
||||
),
|
||||
)
|
||||
datasource = lifecycle.register_origin(
|
||||
session,
|
||||
principal,
|
||||
origin_ref=origin.ref,
|
||||
name="Monthly cases cache",
|
||||
source_name="monthly_cases",
|
||||
mode="cached",
|
||||
)
|
||||
result = preview_pipeline(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
actor_id="account-1",
|
||||
payload=PipelinePreviewRequest(
|
||||
graph=_graph(
|
||||
datasource_ref=datasource.ref,
|
||||
fingerprint=datasource.fingerprint,
|
||||
),
|
||||
row_limit=100,
|
||||
),
|
||||
principal=principal,
|
||||
registry=registry,
|
||||
)
|
||||
expected_rows = [
|
||||
{"id": 1, "amount": 5},
|
||||
{"id": 2, "amount": 15},
|
||||
]
|
||||
if result.status != "succeeded":
|
||||
raise RuntimeError(f"Dataflow preview failed: {result.diagnostics}")
|
||||
if result.rows != expected_rows:
|
||||
raise RuntimeError(f"Unexpected Dataflow rows: {result.rows!r}")
|
||||
if result.source_fingerprints[0]["source_ref"] != datasource.ref:
|
||||
raise RuntimeError("Dataflow lineage did not retain the datasource reference.")
|
||||
pipeline = create_pipeline(
|
||||
session,
|
||||
tenant_id="tenant-1",
|
||||
actor_id="account-1",
|
||||
payload=PipelineCreateRequest(
|
||||
name="Monthly case output",
|
||||
status="active",
|
||||
graph=_graph(
|
||||
datasource_ref=datasource.ref,
|
||||
fingerprint=datasource.fingerprint,
|
||||
),
|
||||
editor_mode="graph",
|
||||
),
|
||||
)
|
||||
run_request = DataflowRunRequest(
|
||||
pipeline_ref=f"pipeline:{pipeline.id}",
|
||||
revision=1,
|
||||
idempotency_key="composition-run-1",
|
||||
publication=DataflowPublicationTarget(
|
||||
name="Monthly case result",
|
||||
source_name="monthly_case_result",
|
||||
freeze=True,
|
||||
frozen_label="Composition evidence",
|
||||
),
|
||||
)
|
||||
published = runner.start_run(
|
||||
session,
|
||||
principal,
|
||||
request=run_request,
|
||||
)
|
||||
replayed = runner.start_run(
|
||||
session,
|
||||
principal,
|
||||
request=run_request,
|
||||
)
|
||||
if published.status != "succeeded":
|
||||
raise RuntimeError(f"Dataflow publication failed: {published.error}")
|
||||
if replayed.ref != published.ref or not replayed.replayed:
|
||||
raise RuntimeError("Dataflow run idempotency did not replay the prior run.")
|
||||
if (
|
||||
not published.output_datasource_ref
|
||||
or not published.output_materialization_ref
|
||||
):
|
||||
raise RuntimeError("Dataflow publication did not retain output references.")
|
||||
output = catalogue.read_datasource(
|
||||
session,
|
||||
principal,
|
||||
request=DatasourceReadRequest(
|
||||
datasource_ref=published.output_datasource_ref,
|
||||
),
|
||||
)
|
||||
if list(output.rows) != expected_rows:
|
||||
raise RuntimeError(
|
||||
f"Unexpected published Dataflow rows: {list(output.rows)!r}"
|
||||
)
|
||||
if (
|
||||
output.materialization is None
|
||||
or output.materialization.ref != published.output_materialization_ref
|
||||
or output.materialization.frozen_at is None
|
||||
):
|
||||
raise RuntimeError(
|
||||
"Published Datasource materialization is not pinned and frozen."
|
||||
)
|
||||
engine.dispose()
|
||||
print(
|
||||
"Connector -> Datasources -> pinned Dataflow publication composition passed."
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def _principal() -> ApiPrincipal:
|
||||
return ApiPrincipal(
|
||||
principal=PrincipalRef(
|
||||
account_id="account-1",
|
||||
membership_id="membership-1",
|
||||
tenant_id="tenant-1",
|
||||
scopes=frozenset(
|
||||
{
|
||||
"connectors:source:read",
|
||||
"connectors:source:write",
|
||||
"datasources:catalogue:read",
|
||||
"datasources:source:write",
|
||||
"datasources:stage:write",
|
||||
"dataflow:pipeline:run",
|
||||
}
|
||||
),
|
||||
),
|
||||
account=object(),
|
||||
user=object(),
|
||||
)
|
||||
|
||||
|
||||
def _graph(*, datasource_ref: str, fingerprint: str) -> PipelineGraph:
|
||||
return PipelineGraph(
|
||||
nodes=[
|
||||
GraphNode(
|
||||
id="source",
|
||||
type="source.reference",
|
||||
label="Cases",
|
||||
position=GraphPosition(x=0, y=0),
|
||||
config={
|
||||
"source_ref": datasource_ref,
|
||||
"source_name": "monthly_cases",
|
||||
"expected_fingerprint": fingerprint,
|
||||
"consistency": "current",
|
||||
},
|
||||
),
|
||||
GraphNode(
|
||||
id="output",
|
||||
type="output",
|
||||
label="Output",
|
||||
position=GraphPosition(x=200, y=0),
|
||||
config={},
|
||||
),
|
||||
],
|
||||
edges=[
|
||||
GraphEdge(
|
||||
id="source-output",
|
||||
source="source",
|
||||
target="output",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -69,6 +69,14 @@ PY
|
||||
"$PYTHON" -c 'import govoplan_core.db.bootstrap; import govoplan_access.backend.admin.service; import govoplan_addresses.backend.manifest; import govoplan_files.backend.router; import govoplan_mail.backend.sending.imap; print("targeted backend imports passed")'
|
||||
"$META_ROOT/tools/checks/check_dependency_boundaries.py"
|
||||
"$PYTHON" -m unittest tests.test_module_system
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-connectors/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-datasources/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-dataflow/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-workflow/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-views/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-dashboard/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-postbox/tests
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-datasource-composition.py"
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-mail/tests
|
||||
"$PYTHON" -m unittest tests.test_api_smoke.ApiSmokeTests.test_mailbox_message_listing_reports_total_count
|
||||
|
||||
@@ -77,6 +85,21 @@ cd "$ROOT/webui"
|
||||
"$NPM" run test:module-capabilities
|
||||
"$NPM" run test:module-permutations
|
||||
|
||||
cd /mnt/DATA/git/govoplan-dataflow/webui
|
||||
"$NPM" run test:structure
|
||||
|
||||
cd /mnt/DATA/git/govoplan-datasources/webui
|
||||
"$NPM" run typecheck
|
||||
|
||||
cd /mnt/DATA/git/govoplan-workflow/webui
|
||||
"$NPM" run typecheck
|
||||
|
||||
cd /mnt/DATA/git/govoplan-dashboard/webui
|
||||
"$NPM" run test:dashboard-layout
|
||||
|
||||
cd /mnt/DATA/git/govoplan-postbox/webui
|
||||
"$NPM" run test:ui-structure
|
||||
|
||||
cd /mnt/DATA/git/govoplan-mail/webui
|
||||
"$NPM" run test:mail-ui
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@ fi
|
||||
|
||||
cd "$ROOT"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-contracts.py" --no-impact
|
||||
"$PYTHON" -m unittest tests.test_module_system tests.test_access_contracts
|
||||
"$PYTHON" "$META_ROOT/tools/checks/release_integration.py" artifacts \
|
||||
--requirements "$META_ROOT/requirements-release.txt"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/release_integration.py" core-tests \
|
||||
--core-root "$ROOT"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check_dependency_boundaries.py"
|
||||
|
||||
cd "$ROOT/webui"
|
||||
|
||||
@@ -60,24 +60,6 @@ retry() {
|
||||
done
|
||||
}
|
||||
|
||||
run_discovered_tests() {
|
||||
local suite_dir="$1"
|
||||
local status
|
||||
if ! find "$suite_dir" -type f -name 'test*.py' -print -quit | grep -q .; then
|
||||
echo "No unittest test files found under $suite_dir; skipping."
|
||||
return 0
|
||||
fi
|
||||
set +e
|
||||
"$PYTHON" -m unittest discover -s "$suite_dir"
|
||||
status=$?
|
||||
set -e
|
||||
if [[ "$status" == 5 ]]; then
|
||||
echo "No unittest tests discovered under $suite_dir; skipping."
|
||||
return 0
|
||||
fi
|
||||
return "$status"
|
||||
}
|
||||
|
||||
install_cloned_webui_dependencies() {
|
||||
local webui_dir="$1"
|
||||
if [[ ! -f "$webui_dir/package.json" ]]; then
|
||||
@@ -244,7 +226,7 @@ PY
|
||||
run_step "Clone release module test sources"
|
||||
for repo in govoplan-mail govoplan-calendar govoplan-campaign; do
|
||||
repo_tag="$(release_tag_for_package "$repo")"
|
||||
git clone --depth 1 --branch "$repo_tag" "git@git.add-ideas.de:add-ideas/${repo}.git" "$WORK_ROOT/$repo"
|
||||
git clone --depth 1 --branch "$repo_tag" "git@git.add-ideas.de:GovOPlaN/${repo}.git" "$WORK_ROOT/$repo"
|
||||
done
|
||||
|
||||
run_step "Run core backend release tests"
|
||||
@@ -253,9 +235,10 @@ run_step "Run core backend release tests"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check_dependency_boundaries.py"
|
||||
|
||||
run_step "Run cloned module backend tests"
|
||||
run_discovered_tests "$WORK_ROOT/govoplan-mail/tests"
|
||||
run_discovered_tests "$WORK_ROOT/govoplan-calendar/tests"
|
||||
run_discovered_tests "$WORK_ROOT/govoplan-campaign/tests"
|
||||
for repo in govoplan-mail govoplan-calendar govoplan-campaign; do
|
||||
"$PYTHON" "$META_ROOT/tools/checks/release_integration.py" module-tests \
|
||||
--module-root "$WORK_ROOT/$repo"
|
||||
done
|
||||
|
||||
run_step "Run core WebUI release tests and build"
|
||||
cd "$ROOT/webui"
|
||||
|
||||
@@ -496,7 +496,12 @@ run_bandit() {
|
||||
fi
|
||||
if [[ "${#PY_TEST_ROOTS[@]}" -gt 0 ]]; then
|
||||
prepare_machine_report "$REPORTS_DIR/bandit-tests.json" || return 2
|
||||
bandit -r "${PY_TEST_ROOTS[@]}" -f json -o "$REPORTS_DIR/bandit-tests.json"
|
||||
# Tests intentionally use assertions and synthetic credentials. Keep the
|
||||
# separate test scan useful by excluding only those test-specific signals.
|
||||
bandit -r "${PY_TEST_ROOTS[@]}" \
|
||||
--skip B101,B105,B106,B107 \
|
||||
-f json \
|
||||
-o "$REPORTS_DIR/bandit-tests.json"
|
||||
local test_status=$?
|
||||
[[ "$test_status" -le 1 ]] || status=2
|
||||
fi
|
||||
@@ -512,7 +517,11 @@ run_ruff_security() {
|
||||
fi
|
||||
if [[ "${#PY_TEST_ROOTS[@]}" -gt 0 ]]; then
|
||||
prepare_machine_report "$REPORTS_DIR/ruff-security-tests.json" || return 2
|
||||
ruff check --select S --output-format json "${PY_TEST_ROOTS[@]}" > "$REPORTS_DIR/ruff-security-tests.json"
|
||||
ruff check \
|
||||
--select S \
|
||||
--ignore S101,S105,S106,S107 \
|
||||
--output-format json \
|
||||
"${PY_TEST_ROOTS[@]}" > "$REPORTS_DIR/ruff-security-tests.json"
|
||||
local test_status=$?
|
||||
[[ "$test_status" -le 1 ]] || status=2
|
||||
fi
|
||||
@@ -582,14 +591,32 @@ run_trivy() {
|
||||
run_pip_audit_manifests() {
|
||||
local status=0
|
||||
for repo in "${REPOS[@]}"; do
|
||||
[[ -f "$repo/requirements.txt" ]] || continue
|
||||
local name
|
||||
name="$(safe_name "$repo")"
|
||||
prepare_machine_report "$REPORTS_DIR/pip-audit-$name.json" || return 2
|
||||
# Requirements may contain project-relative entries such as `.[server]`.
|
||||
# Resolve them from the owning repository instead of the meta-repository.
|
||||
(cd "$repo" && pip-audit -r requirements.txt --progress-spinner off --format json --output "$REPORTS_DIR/pip-audit-$name.json")
|
||||
accumulate_exit_status status "$?"
|
||||
local repo_name
|
||||
repo_name="$(safe_name "$repo")"
|
||||
while IFS= read -r -d '' requirements_file; do
|
||||
local manifest_name report_path
|
||||
manifest_name="$(safe_name "$requirements_file")"
|
||||
report_path="$REPORTS_DIR/pip-audit-$repo_name-$manifest_name.json"
|
||||
prepare_machine_report "$report_path" || return 2
|
||||
# Requirements may contain project-relative entries such as `.[server]`.
|
||||
# Resolve them from the owning repository instead of the meta-repository.
|
||||
(
|
||||
cd "$repo" &&
|
||||
pip-audit \
|
||||
-r "$(basename "$requirements_file")" \
|
||||
--progress-spinner off \
|
||||
--format json \
|
||||
--output "$report_path"
|
||||
)
|
||||
accumulate_exit_status status "$?"
|
||||
done < <(
|
||||
find "$repo" \
|
||||
-maxdepth 1 \
|
||||
-type f \
|
||||
-name 'requirements*.txt' \
|
||||
-print0 |
|
||||
sort -z
|
||||
)
|
||||
done
|
||||
return "$status"
|
||||
}
|
||||
@@ -597,12 +624,38 @@ run_pip_audit_manifests() {
|
||||
run_npm_audit_manifests() {
|
||||
local status=0
|
||||
for repo in "${REPOS[@]}"; do
|
||||
[[ -f "$repo/webui/package-lock.json" ]] || continue
|
||||
local name
|
||||
name="$(safe_name "$repo")"
|
||||
prepare_machine_report "$REPORTS_DIR/npm-audit-$name.json" || return 2
|
||||
(cd "$repo/webui" && npm audit --omit=dev --json > "$REPORTS_DIR/npm-audit-$name.json")
|
||||
accumulate_exit_status status "$?"
|
||||
local repo_name
|
||||
repo_name="$(safe_name "$repo")"
|
||||
while IFS= read -r -d '' lock_file; do
|
||||
local lock_dir lock_name runtime_report all_report
|
||||
lock_dir="$(dirname "$lock_file")"
|
||||
lock_name="$(
|
||||
printf '%s' "${lock_file#"$repo"/}" |
|
||||
tr -c 'A-Za-z0-9_.-' '_'
|
||||
)"
|
||||
runtime_report="$REPORTS_DIR/npm-audit-runtime-$repo_name-$lock_name.json"
|
||||
all_report="$REPORTS_DIR/npm-audit-all-$repo_name-$lock_name.json"
|
||||
prepare_machine_report "$runtime_report" || return 2
|
||||
(
|
||||
cd "$lock_dir" &&
|
||||
npm audit --omit=dev --json > "$runtime_report"
|
||||
)
|
||||
accumulate_exit_status status "$?"
|
||||
prepare_machine_report "$all_report" || return 2
|
||||
(
|
||||
cd "$lock_dir" &&
|
||||
npm audit --json > "$all_report"
|
||||
)
|
||||
accumulate_exit_status status "$?"
|
||||
done < <(
|
||||
find "$repo" \
|
||||
-maxdepth 3 \
|
||||
-type f \
|
||||
-name package-lock.json \
|
||||
-not -path '*/node_modules/*' \
|
||||
-print0 |
|
||||
sort -z
|
||||
)
|
||||
done
|
||||
return "$status"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import argparse
|
||||
import importlib.metadata as metadata
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
@@ -354,6 +355,24 @@ def run_core_release_tests(core_root: Path) -> int:
|
||||
return 0 if result.wasSuccessful() else 1
|
||||
|
||||
|
||||
def run_module_release_tests(module_root: Path) -> int:
|
||||
resolved_root = module_root.resolve()
|
||||
tests_root = resolved_root / "tests"
|
||||
if not tests_root.is_dir() or not any(tests_root.rglob("test*.py")):
|
||||
print(f"No test files found under {tests_root}; skipping.")
|
||||
return 0
|
||||
|
||||
result = subprocess.run(
|
||||
(sys.executable, "-m", "pytest", "-q", "tests"),
|
||||
cwd=resolved_root,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 5:
|
||||
print(f"No tests collected under {tests_root}; skipping.")
|
||||
return 0
|
||||
return result.returncode
|
||||
|
||||
|
||||
def _parser() -> argparse.ArgumentParser:
|
||||
meta_root = Path(__file__).resolve().parents[2]
|
||||
parser = argparse.ArgumentParser(description="Artifact-aware GovOPlaN release integration checks")
|
||||
@@ -366,6 +385,8 @@ def _parser() -> argparse.ArgumentParser:
|
||||
)
|
||||
core_parser = subparsers.add_parser("core-tests", help="Run Core tests that are valid for tagged module artifacts")
|
||||
core_parser.add_argument("--core-root", type=Path, required=True)
|
||||
module_parser = subparsers.add_parser("module-tests", help="Run tests from one tagged module source checkout")
|
||||
module_parser.add_argument("--module-root", type=Path, required=True)
|
||||
return parser
|
||||
|
||||
|
||||
@@ -373,7 +394,9 @@ def main(argv: Sequence[str] | None = None) -> int:
|
||||
args = _parser().parse_args(argv)
|
||||
if args.command == "artifacts":
|
||||
return run_installed_artifact_checks(args.requirements)
|
||||
return run_core_release_tests(args.core_root)
|
||||
if args.command == "core-tests":
|
||||
return run_core_release_tests(args.core_root)
|
||||
return run_module_release_tests(args.module_root)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
202
tools/checks/security-audit/resolve_workspace_mount.py
Normal file
202
tools/checks/security-audit/resolve_workspace_mount.py
Normal file
@@ -0,0 +1,202 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import posixpath
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
VOLUME_NAME = re.compile(r"[A-Za-z0-9][A-Za-z0-9_.-]*")
|
||||
UNSAFE_BIND_SOURCES = {
|
||||
Path("/"),
|
||||
Path("/bin"),
|
||||
Path("/boot"),
|
||||
Path("/dev"),
|
||||
Path("/etc"),
|
||||
Path("/home"),
|
||||
Path("/lib"),
|
||||
Path("/lib64"),
|
||||
Path("/mnt"),
|
||||
Path("/opt"),
|
||||
Path("/proc"),
|
||||
Path("/root"),
|
||||
Path("/run"),
|
||||
Path("/sbin"),
|
||||
Path("/srv"),
|
||||
Path("/sys"),
|
||||
Path("/tmp"),
|
||||
Path("/usr"),
|
||||
Path("/var"),
|
||||
Path("/var/run"),
|
||||
}
|
||||
SENSITIVE_BIND_ROOTS = {
|
||||
Path("/dev"),
|
||||
Path("/etc"),
|
||||
Path("/proc"),
|
||||
Path("/root"),
|
||||
Path("/run"),
|
||||
Path("/sys"),
|
||||
Path("/var/run"),
|
||||
}
|
||||
|
||||
|
||||
class MountResolutionError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def normalized_absolute_path(
|
||||
value: str,
|
||||
*,
|
||||
field: str,
|
||||
resolve_symlinks: bool = True,
|
||||
) -> Path:
|
||||
if not value.startswith("/") or value.startswith("//"):
|
||||
raise MountResolutionError(f"{field} must be an absolute path")
|
||||
path = Path(posixpath.normpath(value))
|
||||
return path.resolve(strict=False) if resolve_symlinks else path
|
||||
|
||||
|
||||
def contains_path(container: Path, path: Path) -> bool:
|
||||
return path == container or container in path.parents
|
||||
|
||||
|
||||
def mount_option_value(value: str, *, field: str) -> str:
|
||||
if not value or any(character in value for character in (",", "\n", "\r")):
|
||||
raise MountResolutionError(f"{field} cannot be represented safely")
|
||||
return value
|
||||
|
||||
|
||||
def resolve_workspace_mount(
|
||||
mounts: Any,
|
||||
*,
|
||||
root: str,
|
||||
scope: str,
|
||||
reports_dir: str,
|
||||
) -> str:
|
||||
if scope not in {"current", "govoplan"}:
|
||||
raise MountResolutionError(f"unsupported audit scope: {scope}")
|
||||
if not isinstance(mounts, list):
|
||||
raise MountResolutionError("Docker mount metadata must be a list")
|
||||
|
||||
root_path = normalized_absolute_path(root, field="audit root")
|
||||
scan_root = root_path if scope == "current" else root_path.parent
|
||||
reports_path = Path(reports_dir)
|
||||
if not reports_path.is_absolute():
|
||||
reports_path = root_path / reports_path
|
||||
reports_path = reports_path.resolve(strict=False)
|
||||
|
||||
parsed_mounts: list[tuple[Path, dict[str, Any]]] = []
|
||||
for mount in mounts:
|
||||
if not isinstance(mount, dict) or not isinstance(mount.get("Destination"), str):
|
||||
continue
|
||||
destination = normalized_absolute_path(
|
||||
mount["Destination"],
|
||||
field="mount destination",
|
||||
)
|
||||
parsed_mounts.append((destination, mount))
|
||||
|
||||
candidates = [
|
||||
(destination, mount)
|
||||
for destination, mount in parsed_mounts
|
||||
if destination != Path("/") and contains_path(destination, scan_root)
|
||||
]
|
||||
|
||||
if not candidates:
|
||||
raise MountResolutionError(
|
||||
f"no job-container mount covers audit scope root {scan_root}"
|
||||
)
|
||||
longest_depth = max(len(destination.parts) for destination, _ in candidates)
|
||||
selected = [
|
||||
candidate
|
||||
for candidate in candidates
|
||||
if len(candidate[0].parts) == longest_depth
|
||||
]
|
||||
if len(selected) != 1:
|
||||
raise MountResolutionError("job-container workspace mount is ambiguous")
|
||||
|
||||
destination, mount = selected[0]
|
||||
nested_mounts = [
|
||||
nested_destination
|
||||
for nested_destination, _ in parsed_mounts
|
||||
if nested_destination != destination
|
||||
and contains_path(scan_root, nested_destination)
|
||||
]
|
||||
if nested_mounts:
|
||||
raise MountResolutionError(
|
||||
"nested job-container mounts inside the audit scope cannot be "
|
||||
"reproduced safely"
|
||||
)
|
||||
if mount.get("RW") is not True:
|
||||
raise MountResolutionError("job-container workspace mount is not writable")
|
||||
if not contains_path(destination, reports_path):
|
||||
raise MountResolutionError(
|
||||
f"audit reports path {reports_path} is outside the workspace mount"
|
||||
)
|
||||
|
||||
destination_value = mount_option_value(
|
||||
str(destination),
|
||||
field="mount destination",
|
||||
)
|
||||
mount_type = mount.get("Type")
|
||||
if mount_type == "volume":
|
||||
name = mount.get("Name")
|
||||
if not isinstance(name, str) or VOLUME_NAME.fullmatch(name) is None:
|
||||
raise MountResolutionError("workspace volume name is missing or malformed")
|
||||
source_value = name
|
||||
elif mount_type == "bind":
|
||||
source = mount.get("Source")
|
||||
if not isinstance(source, str):
|
||||
raise MountResolutionError("workspace bind source is missing")
|
||||
source_path = normalized_absolute_path(
|
||||
source,
|
||||
field="mount source",
|
||||
resolve_symlinks=False,
|
||||
)
|
||||
if source_path in UNSAFE_BIND_SOURCES or any(
|
||||
contains_path(sensitive_root, source_path)
|
||||
for sensitive_root in SENSITIVE_BIND_ROOTS
|
||||
):
|
||||
raise MountResolutionError(
|
||||
"workspace bind source is too broad or sensitive"
|
||||
)
|
||||
source_value = mount_option_value(str(source_path), field="mount source")
|
||||
else:
|
||||
raise MountResolutionError(f"unsupported workspace mount type: {mount_type!r}")
|
||||
|
||||
return f"type={mount_type},source={source_value},target={destination_value}"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Resolve one safe Gitea Actions workspace mount for an audit container."
|
||||
)
|
||||
parser.add_argument("--root", required=True)
|
||||
parser.add_argument("--scope", choices=("current", "govoplan"), required=True)
|
||||
parser.add_argument("--reports-dir", required=True)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
try:
|
||||
mounts = json.load(sys.stdin)
|
||||
print(
|
||||
resolve_workspace_mount(
|
||||
mounts,
|
||||
root=args.root,
|
||||
scope=args.scope,
|
||||
reports_dir=args.reports_dir,
|
||||
)
|
||||
)
|
||||
except (json.JSONDecodeError, MountResolutionError) as exc:
|
||||
print(f"workspace mount error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
44
tools/checks/security-audit/run.sh
Normal file → Executable file
44
tools/checks/security-audit/run.sh
Normal file → Executable file
@@ -139,13 +139,53 @@ if [[ "$BUILD_ONLY" == "1" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$SCOPE" == "govoplan" ]]; then
|
||||
declare -a WORKSPACE_MOUNT=()
|
||||
if [[ "${GITEA_ACTIONS:-}" == "true" ]]; then
|
||||
ACTIONS_CONTAINER_ID="$(hostname)"
|
||||
if ! ACTIONS_MOUNTS_JSON="$(
|
||||
"${DOCKER_CLI[@]}" container inspect \
|
||||
--format '{{json .Mounts}}' \
|
||||
"$ACTIONS_CONTAINER_ID"
|
||||
)"; then
|
||||
echo "Gitea Actions is using a host Docker daemon, but the current job container could not be resolved." >&2
|
||||
echo "Cannot safely share the checked-out workspace with the audit container." >&2
|
||||
exit 1
|
||||
fi
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
MOUNT_PYTHON=python3
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
MOUNT_PYTHON=python
|
||||
else
|
||||
echo "Python is required to validate the Gitea Actions workspace mount." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! ACTIONS_WORKSPACE_MOUNT="$(
|
||||
printf '%s' "$ACTIONS_MOUNTS_JSON" \
|
||||
| "$MOUNT_PYTHON" "$ROOT/tools/checks/security-audit/resolve_workspace_mount.py" \
|
||||
--root "$ROOT" \
|
||||
--scope "$SCOPE" \
|
||||
--reports-dir "$REPORTS_DIR"
|
||||
)"; then
|
||||
echo "Cannot safely share the Gitea Actions workspace with the audit container." >&2
|
||||
exit 1
|
||||
fi
|
||||
WORKSPACE_MOUNT=(--mount "$ACTIONS_WORKSPACE_MOUNT")
|
||||
MOUNT_ROOT="$(dirname "$ROOT")"
|
||||
WORKDIR="$ROOT"
|
||||
if [[ "$SCOPE" == "govoplan" ]]; then
|
||||
EXTRA_ENV=(-e "GOVOPLAN_REPOS_ROOT=$MOUNT_ROOT")
|
||||
else
|
||||
EXTRA_ENV=()
|
||||
fi
|
||||
elif [[ "$SCOPE" == "govoplan" ]]; then
|
||||
MOUNT_ROOT="$(dirname "$ROOT")"
|
||||
WORKDIR="/workspace/$(basename "$ROOT")"
|
||||
WORKSPACE_MOUNT=(-v "$MOUNT_ROOT:/workspace")
|
||||
EXTRA_ENV=(-e "GOVOPLAN_REPOS_ROOT=/workspace")
|
||||
else
|
||||
MOUNT_ROOT="$ROOT"
|
||||
WORKDIR="/workspace"
|
||||
WORKSPACE_MOUNT=(-v "$MOUNT_ROOT:/workspace")
|
||||
EXTRA_ENV=()
|
||||
fi
|
||||
|
||||
@@ -159,7 +199,7 @@ fi
|
||||
-e SECURITY_AUDIT_REQUIRE_TOOLS="${SECURITY_AUDIT_REQUIRE_TOOLS:-0}" \
|
||||
-e SECURITY_AUDIT_TOOLBOX_FINGERPRINT="$IMAGE_FINGERPRINT" \
|
||||
"${EXTRA_ENV[@]}" \
|
||||
-v "$MOUNT_ROOT:/workspace" \
|
||||
"${WORKSPACE_MOUNT[@]}" \
|
||||
-w "$WORKDIR" \
|
||||
"$FINGERPRINT_IMAGE" \
|
||||
bash tools/checks/check-security-audit.sh --mode "$MODE" --scope "$SCOPE" --reports-dir "$REPORTS_DIR" "${SCRIPT_ARGS[@]}"
|
||||
|
||||
@@ -27,12 +27,17 @@ EXCLUDED_PARTS = {
|
||||
".git",
|
||||
".gitea",
|
||||
".venv",
|
||||
".mypy_cache",
|
||||
".pytest_cache",
|
||||
".ruff_cache",
|
||||
"node_modules",
|
||||
"__pycache__",
|
||||
"audit-reports",
|
||||
"dist",
|
||||
"build",
|
||||
".cache",
|
||||
".module-test-build",
|
||||
"runtime",
|
||||
}
|
||||
EXCLUDED_NAMES = {
|
||||
"package-lock.json",
|
||||
@@ -40,6 +45,14 @@ EXCLUDED_NAMES = {
|
||||
"yarn.lock",
|
||||
"uv.lock",
|
||||
}
|
||||
REPO_DOC_NAME_TOKENS = {
|
||||
"architecture",
|
||||
"backlog",
|
||||
"plan",
|
||||
"roadmap",
|
||||
"todo",
|
||||
"workflow",
|
||||
}
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
@@ -216,7 +229,8 @@ def is_repo_doc(repo_root_path: pathlib.Path, path: pathlib.Path) -> bool:
|
||||
return False
|
||||
if rel.parts[0] in {"docs", "doc", "codex"} and path.suffix.lower() in {".md", ".txt", ".csv"}:
|
||||
return True
|
||||
if re.search(r"(backlog|todo|roadmap|plan|architecture|workflow|manifest)", path.name, re.IGNORECASE):
|
||||
name_tokens = set(re.split(r"[^a-z0-9]+", path.stem.lower()))
|
||||
if name_tokens & REPO_DOC_NAME_TOKENS:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ FRONTEND_USE_POLLING="${GOVOPLAN_FRONTEND_USE_POLLING:-1}"
|
||||
FRONTEND_POLLING_INTERVAL="${GOVOPLAN_FRONTEND_POLLING_INTERVAL:-500}"
|
||||
AUTO_SYNC_PYTHON="${GOVOPLAN_AUTO_SYNC_PYTHON:-1}"
|
||||
DEV_DATABASE_BACKEND="${GOVOPLAN_DEV_DATABASE_BACKEND:-postgres}"
|
||||
BACKEND_RELOAD_MODULES="${GOVOPLAN_BACKEND_RELOAD_MODULES:-}"
|
||||
|
||||
LOG_DIR="${GOVOPLAN_DEV_LOG_DIR:-$META_ROOT/runtime/dev-launcher}"
|
||||
BACKEND_LOG="$LOG_DIR/backend.log"
|
||||
@@ -167,7 +168,16 @@ port_is_free "$BACKEND_HOST" "$BACKEND_PORT" || fail "$BACKEND_URL is already in
|
||||
port_is_free "$FRONTEND_HOST" "$FRONTEND_PORT" || fail "$FRONTEND_URL is already in use"
|
||||
|
||||
printf 'Starting GovOPlaN backend at %s\n' "$BACKEND_URL"
|
||||
run_grouped "$ROOT" "$BACKEND_LOG" "$PYTHON" -m govoplan_core.devserver --host "$BACKEND_HOST" --port "$BACKEND_PORT"
|
||||
backend_args=(-m govoplan_core.devserver --host "$BACKEND_HOST" --port "$BACKEND_PORT")
|
||||
if [ "$BACKEND_RELOAD_MODULES" = "none" ]; then
|
||||
backend_args+=(--reload-core-only)
|
||||
elif [ -n "$BACKEND_RELOAD_MODULES" ]; then
|
||||
IFS=',' read -r -a reload_modules <<< "$BACKEND_RELOAD_MODULES"
|
||||
for reload_module in "${reload_modules[@]}"; do
|
||||
[ -n "$reload_module" ] && backend_args+=(--reload-module "$reload_module")
|
||||
done
|
||||
fi
|
||||
run_grouped "$ROOT" "$BACKEND_LOG" "$PYTHON" "${backend_args[@]}"
|
||||
backend_pid="$run_grouped_pid"
|
||||
|
||||
printf 'Waiting for %s/health\n' "$BACKEND_URL"
|
||||
|
||||
@@ -51,7 +51,7 @@ set +a
|
||||
|
||||
export APP_ENV="${APP_ENV:-staging}"
|
||||
export GOVOPLAN_INSTALL_PROFILE="${GOVOPLAN_INSTALL_PROFILE:-production-like}"
|
||||
export ENABLED_MODULES="${ENABLED_MODULES:-tenancy,organizations,identity,access,admin,dashboard,policy,audit,campaigns,files,mail,calendar,poll,scheduling,notifications,docs,ops}"
|
||||
export ENABLED_MODULES="${ENABLED_MODULES:-tenancy,organizations,identity,access,admin,dashboard,policy,audit,campaigns,files,mail,calendar,poll,scheduling,connectors,datasources,dataflow,workflow,views,search,risk_compliance,notifications,docs,ops}"
|
||||
export DATABASE_URL="${DATABASE_URL:-${GOVOPLAN_PRODUCTION_LIKE_DATABASE_URL:-postgresql+psycopg://govoplan:govoplan-dev@127.0.0.1:55433/govoplan}}"
|
||||
export GOVOPLAN_DATABASE_URL_PGTOOLS="${GOVOPLAN_DATABASE_URL_PGTOOLS:-${GOVOPLAN_PRODUCTION_LIKE_DATABASE_URL_PGTOOLS:-postgresql://govoplan:govoplan-dev@127.0.0.1:55433/govoplan}}"
|
||||
export REDIS_URL="${REDIS_URL:-${GOVOPLAN_PRODUCTION_LIKE_REDIS_URL:-redis://127.0.0.1:56379/0}}"
|
||||
|
||||
@@ -66,7 +66,7 @@ export DATABASE_URL="${DATABASE_URL:-${GOVOPLAN_PRODUCTION_LIKE_DATABASE_URL:-po
|
||||
export GOVOPLAN_DATABASE_URL_PGTOOLS="${GOVOPLAN_DATABASE_URL_PGTOOLS:-${GOVOPLAN_PRODUCTION_LIKE_DATABASE_URL_PGTOOLS:-postgresql://govoplan:govoplan-dev@127.0.0.1:55433/govoplan}}"
|
||||
export REDIS_URL="${REDIS_URL:-${GOVOPLAN_PRODUCTION_LIKE_REDIS_URL:-redis://127.0.0.1:56379/0}}"
|
||||
export CELERY_ENABLED="${CELERY_ENABLED:-true}"
|
||||
export ENABLED_MODULES="${ENABLED_MODULES:-tenancy,organizations,identity,access,admin,dashboard,policy,audit,campaigns,files,mail,calendar,poll,scheduling,notifications,docs,ops}"
|
||||
export ENABLED_MODULES="${ENABLED_MODULES:-tenancy,organizations,identity,access,admin,dashboard,policy,audit,campaigns,files,mail,calendar,poll,scheduling,connectors,datasources,dataflow,workflow,views,search,risk_compliance,notifications,docs,ops}"
|
||||
export FILE_STORAGE_BACKEND="${FILE_STORAGE_BACKEND:-local}"
|
||||
export FILE_STORAGE_LOCAL_ROOT="${FILE_STORAGE_LOCAL_ROOT:-$META_ROOT/runtime/production-like/files}"
|
||||
export DEV_AUTO_MIGRATE_ENABLED="${DEV_AUTO_MIGRATE_ENABLED:-false}"
|
||||
|
||||
@@ -26,7 +26,7 @@ from govoplan_core.server.registry import available_module_manifests # noqa: E4
|
||||
from govoplan_release.version_alignment import selected_repository_version_issues # noqa: E402
|
||||
|
||||
|
||||
GITEA_BASE = "git+ssh://git@git.add-ideas.de/add-ideas"
|
||||
GITEA_BASE = "git+ssh://git@git.add-ideas.de/GovOPlaN"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
||||
@@ -164,9 +164,10 @@ def git_success(path: Path, *args: str, timeout: int = 10) -> bool:
|
||||
|
||||
|
||||
def git(path: Path, *args: str, timeout: int = 10) -> subprocess.CompletedProcess[str]:
|
||||
command = scoped_git_command(path, *args)
|
||||
try:
|
||||
return subprocess.run(
|
||||
["/usr/bin/git", "-c", "core.hooksPath=/dev/null", *args],
|
||||
command,
|
||||
cwd=path,
|
||||
check=False,
|
||||
text=True,
|
||||
@@ -176,7 +177,20 @@ def git(path: Path, *args: str, timeout: int = 10) -> subprocess.CompletedProces
|
||||
env=sanitized_git_environment(),
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
return subprocess.CompletedProcess(["/usr/bin/git", *args], 124, exc.stdout or "", exc.stderr or "git command timed out")
|
||||
return subprocess.CompletedProcess(command, 124, exc.stdout or "", exc.stderr or "git command timed out")
|
||||
|
||||
|
||||
def scoped_git_command(path: Path, *args: str) -> tuple[str, ...]:
|
||||
"""Trust exactly one resolved catalog checkout for one Git invocation."""
|
||||
|
||||
return (
|
||||
"/usr/bin/git",
|
||||
"-c",
|
||||
"core.hooksPath=/dev/null",
|
||||
"-c",
|
||||
f"safe.directory={path.resolve()}",
|
||||
*args,
|
||||
)
|
||||
|
||||
|
||||
def sanitized_git_environment(
|
||||
|
||||
@@ -1985,6 +1985,8 @@ def _git_bytes(
|
||||
"-C",
|
||||
str(path),
|
||||
"-c",
|
||||
f"safe.directory={path.resolve()}",
|
||||
"-c",
|
||||
"core.sharedRepository=0600",
|
||||
"-c",
|
||||
"core.fsmonitor=false",
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
from .git_state import collect_repository_snapshot
|
||||
from .git_state import collect_repository_snapshot, scoped_git_command
|
||||
from .repository_push import command_text, compact_output
|
||||
from .workspace import load_repository_specs, resolve_repo_path, resolve_workspace_root
|
||||
|
||||
@@ -131,7 +131,12 @@ def resolve_message(*, repo: str, version: str | None, message: str | None) -> s
|
||||
|
||||
|
||||
def run(command: tuple[str, ...], *, cwd: Path) -> subprocess.CompletedProcess[str]:
|
||||
effective_command = (
|
||||
scoped_git_command(cwd, *command[1:])
|
||||
if command and Path(command[0]).name == "git"
|
||||
else command
|
||||
)
|
||||
try:
|
||||
return subprocess.run(command, cwd=cwd, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
|
||||
return subprocess.run(effective_command, cwd=cwd, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
return subprocess.CompletedProcess(command, 124, exc.stdout or "", exc.stderr or "git command timed out")
|
||||
return subprocess.CompletedProcess(effective_command, 124, exc.stdout or "", exc.stderr or "git command timed out")
|
||||
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
from .git_state import collect_repository_snapshot
|
||||
from .git_state import collect_repository_snapshot, scoped_git_command
|
||||
from .workspace import load_repository_specs, resolve_repo_path, resolve_workspace_root
|
||||
|
||||
|
||||
@@ -138,10 +138,15 @@ def command_text(command: tuple[str, ...]) -> str:
|
||||
|
||||
|
||||
def run(command: tuple[str, ...], *, cwd: Path) -> subprocess.CompletedProcess[str]:
|
||||
effective_command = (
|
||||
scoped_git_command(cwd, *command[1:])
|
||||
if command and Path(command[0]).name == "git"
|
||||
else command
|
||||
)
|
||||
try:
|
||||
return subprocess.run(command, cwd=cwd, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
|
||||
return subprocess.run(effective_command, cwd=cwd, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
return subprocess.CompletedProcess(command, 124, exc.stdout or "", exc.stderr or "git push timed out")
|
||||
return subprocess.CompletedProcess(effective_command, 124, exc.stdout or "", exc.stderr or "git push timed out")
|
||||
|
||||
|
||||
def compact_output(value: str) -> str:
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
from .git_state import collect_repository_snapshot
|
||||
from .git_state import collect_repository_snapshot, scoped_git_command
|
||||
from .repository_push import command_text, compact_output
|
||||
from .workspace import load_repository_specs, resolve_repo_path, resolve_workspace_root
|
||||
|
||||
@@ -93,7 +93,12 @@ def sync_repositories(
|
||||
|
||||
|
||||
def run(command: tuple[str, ...], *, cwd: Path) -> subprocess.CompletedProcess[str]:
|
||||
effective_command = (
|
||||
scoped_git_command(cwd, *command[1:])
|
||||
if command and Path(command[0]).name == "git"
|
||||
else command
|
||||
)
|
||||
try:
|
||||
return subprocess.run(command, cwd=cwd, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
|
||||
return subprocess.run(effective_command, cwd=cwd, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=120)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
return subprocess.CompletedProcess(command, 124, exc.stdout or "", exc.stderr or "git fetch timed out")
|
||||
return subprocess.CompletedProcess(effective_command, 124, exc.stdout or "", exc.stderr or "git fetch timed out")
|
||||
|
||||
@@ -13,6 +13,7 @@ from .git_state import (
|
||||
collect_repository_snapshot,
|
||||
git_text,
|
||||
sanitized_git_environment,
|
||||
scoped_git_command,
|
||||
)
|
||||
from .model import RepositorySnapshot
|
||||
from .repository_push import command_text, compact_output
|
||||
@@ -479,12 +480,7 @@ def run(
|
||||
effective_command = command
|
||||
effective_environment = env
|
||||
if command and Path(command[0]).name == "git":
|
||||
effective_command = (
|
||||
"/usr/bin/git",
|
||||
"-c",
|
||||
"core.hooksPath=/dev/null",
|
||||
*command[1:],
|
||||
)
|
||||
effective_command = scoped_git_command(cwd, *command[1:])
|
||||
effective_environment = sanitized_git_environment(env)
|
||||
try:
|
||||
return subprocess.run(
|
||||
|
||||
@@ -53,7 +53,7 @@ from .workspace import (
|
||||
website_root,
|
||||
)
|
||||
|
||||
GITEA_BASE = "git+ssh://git@git.add-ideas.de/add-ideas"
|
||||
GITEA_BASE = "git+ssh://git@git.add-ideas.de/GovOPlaN"
|
||||
MAX_BASE_JSON_BYTES = 16 * 1024 * 1024
|
||||
_SIGNING_KEY_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$")
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ from .git_state import (
|
||||
collect_repository_snapshot,
|
||||
git_text,
|
||||
sanitized_git_environment,
|
||||
scoped_git_command,
|
||||
)
|
||||
from .repository_tag import RemoteTagResult, ref_commit, remote_tag_commit
|
||||
from .version_alignment import repository_version_issues
|
||||
@@ -417,9 +418,10 @@ def _git_file(path: Path, tag: str, name: str) -> str | None:
|
||||
|
||||
|
||||
def _git(path: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
command = scoped_git_command(path, *args)
|
||||
try:
|
||||
return subprocess.run(
|
||||
("/usr/bin/git", "-c", "core.hooksPath=/dev/null", *args),
|
||||
command,
|
||||
cwd=path,
|
||||
check=False,
|
||||
text=True,
|
||||
@@ -429,7 +431,7 @@ def _git(path: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
||||
env=sanitized_git_environment(),
|
||||
)
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
return subprocess.CompletedProcess(("/usr/bin/git", *args), 124, exc.stdout or "", exc.stderr or "Git command timed out")
|
||||
return subprocess.CompletedProcess(command, 124, exc.stdout or "", exc.stderr or "Git command timed out")
|
||||
|
||||
|
||||
def _deduplicate(issues: list[SourceTagProvenanceIssue]) -> list[SourceTagProvenanceIssue]:
|
||||
|
||||
@@ -681,10 +681,22 @@ def _git_lock_resolution_issues(
|
||||
|
||||
|
||||
def _git_tag_commit(repo_path: Path, tag: str) -> str | None:
|
||||
repo_path = repo_path.resolve()
|
||||
if not (repo_path / ".git").exists():
|
||||
return None
|
||||
result = subprocess.run(
|
||||
["/usr/bin/git", "-C", str(repo_path), "rev-list", "-n", "1", tag],
|
||||
[
|
||||
"/usr/bin/git",
|
||||
"-c",
|
||||
"core.hooksPath=/dev/null",
|
||||
"-c",
|
||||
f"safe.directory={repo_path}",
|
||||
"-C",
|
||||
str(repo_path),
|
||||
"rev-parse",
|
||||
"--verify",
|
||||
f"refs/tags/{tag}^{{commit}}",
|
||||
],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
@@ -697,10 +709,22 @@ def _git_tag_commit(repo_path: Path, tag: str) -> str | None:
|
||||
|
||||
|
||||
def _git_tag_file(repo_path: Path, tag: str, relative_path: str) -> str | None:
|
||||
repo_path = repo_path.resolve()
|
||||
if not (repo_path / ".git").exists():
|
||||
return None
|
||||
result = subprocess.run(
|
||||
["/usr/bin/git", "-C", str(repo_path), "show", f"{tag}:{relative_path}"],
|
||||
[
|
||||
"/usr/bin/git",
|
||||
"-c",
|
||||
"core.hooksPath=/dev/null",
|
||||
"-c",
|
||||
f"safe.directory={repo_path}",
|
||||
"-C",
|
||||
str(repo_path),
|
||||
"cat-file",
|
||||
"blob",
|
||||
f"refs/tags/{tag}^{{commit}}:{relative_path}",
|
||||
],
|
||||
check=False,
|
||||
text=True,
|
||||
stdout=subprocess.PIPE,
|
||||
|
||||
@@ -85,8 +85,11 @@ PACKAGE_MODULE_REPOS=(
|
||||
"$PARENT/govoplan-certificates"
|
||||
"$PARENT/govoplan-committee"
|
||||
"$PARENT/govoplan-consultation"
|
||||
"$PARENT/govoplan-connectors"
|
||||
"$PARENT/govoplan-contracts"
|
||||
"$PARENT/govoplan-dashboard"
|
||||
"$PARENT/govoplan-dataflow"
|
||||
"$PARENT/govoplan-datasources"
|
||||
"$PARENT/govoplan-docs"
|
||||
"$PARENT/govoplan-facilities"
|
||||
"$PARENT/govoplan-files"
|
||||
@@ -96,7 +99,7 @@ PACKAGE_MODULE_REPOS=(
|
||||
"$PARENT/govoplan-identity"
|
||||
"$PARENT/govoplan-idm"
|
||||
"$PARENT/govoplan-inspections"
|
||||
"$PARENT/govoplan-issue-reporting"
|
||||
"$PARENT/govoplan-tickets"
|
||||
"$PARENT/govoplan-learning"
|
||||
"$PARENT/govoplan-mail"
|
||||
"$PARENT/govoplan-ops"
|
||||
@@ -111,12 +114,14 @@ PACKAGE_MODULE_REPOS=(
|
||||
"$PARENT/govoplan-tenancy"
|
||||
"$PARENT/govoplan-transparency"
|
||||
"$PARENT/govoplan-evaluation"
|
||||
"$PARENT/govoplan-views"
|
||||
"$PARENT/govoplan-workflow"
|
||||
)
|
||||
TAG_ONLY_MODULE_REPOS=(
|
||||
"$PARENT/govoplan-addresses"
|
||||
"$PARENT/govoplan-appointments"
|
||||
"$PARENT/govoplan-cases"
|
||||
"$PARENT/govoplan-connectors"
|
||||
"$PARENT/govoplan-projects"
|
||||
"$PARENT/govoplan-dms"
|
||||
"$PARENT/govoplan-dist-lists"
|
||||
"$PARENT/govoplan-erp"
|
||||
@@ -132,10 +137,10 @@ TAG_ONLY_MODULE_REPOS=(
|
||||
"$PARENT/govoplan-rest"
|
||||
"$PARENT/govoplan-scheduling"
|
||||
"$PARENT/govoplan-search"
|
||||
"$PARENT/govoplan-wiki"
|
||||
"$PARENT/govoplan-soap"
|
||||
"$PARENT/govoplan-tasks"
|
||||
"$PARENT/govoplan-templates"
|
||||
"$PARENT/govoplan-workflow"
|
||||
"$PARENT/govoplan-xoev"
|
||||
"$PARENT/govoplan-xrechnung"
|
||||
"$PARENT/govoplan-xta-osci"
|
||||
|
||||
@@ -76,7 +76,7 @@ def main() -> int:
|
||||
)
|
||||
selective.add_argument("--public-base-url", default="https://govoplan.add-ideas.de")
|
||||
selective.add_argument(
|
||||
"--repository-base", default="git+ssh://git@git.add-ideas.de/add-ideas"
|
||||
"--repository-base", default="git+ssh://git@git.add-ideas.de/GovOPlaN"
|
||||
)
|
||||
selective.add_argument(
|
||||
"--source-remote",
|
||||
|
||||
@@ -68,7 +68,7 @@ class CatalogCandidateRequest(BaseModel):
|
||||
expires_days: int = 90
|
||||
sequence: int | None = None
|
||||
public_base_url: str = "https://govoplan.add-ideas.de"
|
||||
repository_base: str = "git+ssh://git@git.add-ideas.de/add-ideas"
|
||||
repository_base: str = "git+ssh://git@git.add-ideas.de/GovOPlaN"
|
||||
source_remote: str = "origin"
|
||||
check_public: bool = True
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
padding: 18px 28px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
@@ -281,7 +282,7 @@
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 1220px;
|
||||
min-width: 960px;
|
||||
}
|
||||
|
||||
th,
|
||||
|
||||
@@ -3,35 +3,138 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
REGISTERED_TRANSPORT = "registered"
|
||||
PUBLIC_HTTPS_TRANSPORT = "public-https"
|
||||
GITEA_SSH_PREFIX = "git@git.add-ideas.de:"
|
||||
GITEA_HTTPS_PREFIX = "https://git.add-ideas.de/"
|
||||
GITEA_REPOSITORY_PATH = re.compile(
|
||||
r"(?:GovOPlaN|add-ideas)/[a-z0-9][a-z0-9-]*[.]git"
|
||||
)
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Clone GovOPlaN repositories listed in repositories.json.")
|
||||
parser.add_argument("--check", action="store_true", help="Only report missing repositories.")
|
||||
parser.add_argument("--parent", type=Path, help="Override checkout parent directory.")
|
||||
return parser.parse_args()
|
||||
parser.add_argument(
|
||||
"--repo",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="NAME",
|
||||
help="Clone only this registered repository; repeat for more than one.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--exclude-repo",
|
||||
action="append",
|
||||
default=[],
|
||||
metavar="NAME",
|
||||
help="Exclude this registered repository; repeat for more than one.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--transport",
|
||||
choices=(REGISTERED_TRANSPORT, PUBLIC_HTTPS_TRANSPORT),
|
||||
default=REGISTERED_TRANSPORT,
|
||||
help=(
|
||||
"Clone registered remotes unchanged, or convert public "
|
||||
"git.add-ideas.de SSH remotes to anonymous HTTPS."
|
||||
),
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
manifest = json.loads((ROOT / "repositories.json").read_text())
|
||||
def clone_remote(remote: str, *, transport: str) -> str:
|
||||
if transport == REGISTERED_TRANSPORT:
|
||||
return remote
|
||||
if transport != PUBLIC_HTTPS_TRANSPORT:
|
||||
raise ValueError(f"unsupported repository transport: {transport}")
|
||||
if remote.startswith(GITEA_SSH_PREFIX):
|
||||
repository_path = remote.removeprefix(GITEA_SSH_PREFIX)
|
||||
else:
|
||||
parsed = urlsplit(remote)
|
||||
if (
|
||||
parsed.scheme not in {"https", "ssh"}
|
||||
or parsed.hostname != "git.add-ideas.de"
|
||||
or parsed.port is not None
|
||||
or parsed.password is not None
|
||||
or parsed.query
|
||||
or parsed.fragment
|
||||
):
|
||||
raise ValueError(
|
||||
"public HTTPS bootstrap only accepts registered "
|
||||
"git.add-ideas.de remotes"
|
||||
)
|
||||
if parsed.scheme == "https" and parsed.username is not None:
|
||||
raise ValueError("public HTTPS bootstrap does not accept credentials")
|
||||
if parsed.scheme == "ssh" and parsed.username != "git":
|
||||
raise ValueError("registered Gitea SSH remotes must use the git account")
|
||||
repository_path = parsed.path.removeprefix("/")
|
||||
if GITEA_REPOSITORY_PATH.fullmatch(repository_path) is None:
|
||||
raise ValueError("registered Gitea repository path is malformed")
|
||||
return GITEA_HTTPS_PREFIX + repository_path
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
manifest = json.loads((ROOT / "repositories.json").read_text(encoding="utf-8"))
|
||||
parent = args.parent or Path(manifest["default_parent"])
|
||||
missing: list[dict[str, str]] = []
|
||||
missing: list[tuple[dict[str, str], Path, str]] = []
|
||||
repositories = manifest["repositories"]
|
||||
names = [entry["name"] for entry in repositories]
|
||||
if len(names) != len(set(names)):
|
||||
raise ValueError("repository registry contains duplicate names")
|
||||
requested = set(args.repo)
|
||||
excluded = set(args.exclude_repo)
|
||||
unknown = (requested | excluded) - set(names)
|
||||
if unknown:
|
||||
raise ValueError(f"unknown registered repositories: {', '.join(sorted(unknown))}")
|
||||
overlap = requested & excluded
|
||||
if overlap:
|
||||
raise ValueError(
|
||||
f"repositories cannot be selected and excluded: {', '.join(sorted(overlap))}"
|
||||
)
|
||||
|
||||
for entry in manifest["repositories"]:
|
||||
for entry in repositories:
|
||||
if (requested and entry["name"] not in requested) or entry["name"] in excluded:
|
||||
continue
|
||||
repo = parent / entry["path"]
|
||||
if repo.exists():
|
||||
continue
|
||||
missing.append(entry)
|
||||
transport = entry.get("bootstrap_transport", args.transport)
|
||||
remote = clone_remote(entry["remote"], transport=transport)
|
||||
missing.append((entry, repo, remote))
|
||||
|
||||
environment = os.environ.copy()
|
||||
environment.update(
|
||||
{
|
||||
"GIT_ASKPASS": "/bin/false",
|
||||
"GIT_TERMINAL_PROMPT": "0",
|
||||
}
|
||||
)
|
||||
for entry, repo, remote in missing:
|
||||
print(f"missing: {entry['name']} -> {repo}")
|
||||
if not args.check:
|
||||
subprocess.run(["git", "clone", entry["remote"], str(repo)], check=True)
|
||||
subprocess.run(
|
||||
[
|
||||
"git",
|
||||
"-c",
|
||||
"credential.helper=",
|
||||
"clone",
|
||||
"--",
|
||||
remote,
|
||||
str(repo),
|
||||
],
|
||||
check=True,
|
||||
env=environment,
|
||||
)
|
||||
|
||||
if args.check:
|
||||
return 1 if missing else 0
|
||||
|
||||
@@ -12,6 +12,7 @@ from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tomllib
|
||||
from typing import Any
|
||||
|
||||
META_ROOT = Path(__file__).resolve().parents[2]
|
||||
@@ -49,6 +50,23 @@ class InstallPlan:
|
||||
warnings: tuple[str, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LocalInstallationExpectation:
|
||||
requirement: RequirementEntry
|
||||
distribution: str
|
||||
entry_points: tuple[tuple[str, str, str], ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EnvironmentValidation:
|
||||
stale_requirements: tuple[RequirementEntry, ...] = ()
|
||||
issues: tuple[str, ...] = ()
|
||||
|
||||
@property
|
||||
def current(self) -> bool:
|
||||
return not self.stale_requirements
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument(
|
||||
@@ -85,8 +103,9 @@ def main() -> int:
|
||||
fingerprint = build_fingerprint(requirements=requirements, python=python, local_requirements=local_requirements)
|
||||
previous = load_stamp(stamp)
|
||||
current = previous.get("digest") == fingerprint["digest"]
|
||||
environment = validate_local_installations(python, local_requirements)
|
||||
|
||||
if current and not args.force:
|
||||
if current and environment.current and not args.force:
|
||||
payload = stamp_payload(
|
||||
requirements=requirements,
|
||||
python=python,
|
||||
@@ -107,20 +126,31 @@ def main() -> int:
|
||||
print(f"Python environment is current for {requirements.name}.")
|
||||
return 0
|
||||
|
||||
print(f"Python environment metadata is stale for {requirements.name}.")
|
||||
if current and not environment.current:
|
||||
print(f"Python environment metadata is current for {requirements.name}, but installed packages need repair.")
|
||||
else:
|
||||
print(f"Python environment metadata is stale for {requirements.name}.")
|
||||
print(f"Tracked inputs: {len(fingerprint['inputs'])}")
|
||||
for issue in environment.issues:
|
||||
print(f"warning: {issue}", file=sys.stderr)
|
||||
|
||||
if args.check:
|
||||
return 1
|
||||
|
||||
plan = build_install_plan(
|
||||
previous=previous,
|
||||
fingerprint=fingerprint,
|
||||
requirements=requirements,
|
||||
python=python,
|
||||
local_requirements=local_requirements,
|
||||
force=args.force,
|
||||
)
|
||||
if current and environment.stale_requirements and not args.force:
|
||||
plan = build_environment_repair_plan(
|
||||
python=python,
|
||||
stale_requirements=environment.stale_requirements,
|
||||
)
|
||||
else:
|
||||
plan = build_install_plan(
|
||||
previous=previous,
|
||||
fingerprint=fingerprint,
|
||||
requirements=requirements,
|
||||
python=python,
|
||||
local_requirements=local_requirements,
|
||||
force=args.force,
|
||||
)
|
||||
|
||||
print(f"{plan.mode}: {plan.reason}")
|
||||
for warning in plan.warnings:
|
||||
@@ -141,6 +171,13 @@ def main() -> int:
|
||||
for command in commands:
|
||||
subprocess.run(command, check=True)
|
||||
|
||||
repaired_environment = validate_local_installations(python, local_requirements)
|
||||
if not repaired_environment.current:
|
||||
for issue in repaired_environment.issues:
|
||||
print(f"error: {issue}", file=sys.stderr)
|
||||
print("Python environment synchronization did not install all local package metadata.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
write_stamp(
|
||||
stamp,
|
||||
stamp_payload(
|
||||
@@ -297,6 +334,127 @@ def build_install_plan(
|
||||
)
|
||||
|
||||
|
||||
def build_environment_repair_plan(
|
||||
*,
|
||||
python: str,
|
||||
stale_requirements: tuple[RequirementEntry, ...],
|
||||
) -> InstallPlan:
|
||||
command = (
|
||||
python,
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
*(
|
||||
argument
|
||||
for requirement in stale_requirements
|
||||
for argument in requirement.install_args
|
||||
),
|
||||
)
|
||||
return InstallPlan(
|
||||
"Selective Python environment repair",
|
||||
f"reinstalling {len(stale_requirements)} missing or stale local distribution(s).",
|
||||
(command,),
|
||||
)
|
||||
|
||||
|
||||
def validate_local_installations(
|
||||
python: str,
|
||||
local_requirements: tuple[RequirementEntry, ...],
|
||||
) -> EnvironmentValidation:
|
||||
expectations = local_installation_expectations(local_requirements)
|
||||
if not expectations:
|
||||
return EnvironmentValidation()
|
||||
|
||||
probe_input = [
|
||||
{
|
||||
"key": expectation.requirement.key,
|
||||
"distribution": expectation.distribution,
|
||||
"entry_points": [
|
||||
{"group": group, "name": name, "value": value}
|
||||
for group, name, value in expectation.entry_points
|
||||
],
|
||||
}
|
||||
for expectation in expectations
|
||||
]
|
||||
try:
|
||||
result = subprocess.run(
|
||||
(python, "-c", _LOCAL_INSTALLATION_PROBE),
|
||||
check=False,
|
||||
text=True,
|
||||
input=json.dumps(probe_input),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=20,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired) as exc:
|
||||
return EnvironmentValidation(
|
||||
stale_requirements=tuple(item.requirement for item in expectations),
|
||||
issues=(f"could not inspect installed local distributions ({type(exc).__name__})",),
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
detail = result.stderr.strip() or result.stdout.strip() or "installation probe failed"
|
||||
return EnvironmentValidation(
|
||||
stale_requirements=tuple(item.requirement for item in expectations),
|
||||
issues=(f"could not inspect installed local distributions: {detail}",),
|
||||
)
|
||||
try:
|
||||
payload = json.loads(result.stdout)
|
||||
except json.JSONDecodeError:
|
||||
return EnvironmentValidation(
|
||||
stale_requirements=tuple(item.requirement for item in expectations),
|
||||
issues=("installed local distribution probe returned invalid JSON",),
|
||||
)
|
||||
|
||||
raw_issues = payload.get("issues") if isinstance(payload, dict) else None
|
||||
issues_by_key = raw_issues if isinstance(raw_issues, dict) else {}
|
||||
stale = tuple(
|
||||
expectation.requirement
|
||||
for expectation in expectations
|
||||
if expectation.requirement.key in issues_by_key
|
||||
)
|
||||
issues = tuple(
|
||||
f"{expectation.distribution}: {issues_by_key[expectation.requirement.key]}"
|
||||
for expectation in expectations
|
||||
if expectation.requirement.key in issues_by_key
|
||||
)
|
||||
return EnvironmentValidation(stale_requirements=stale, issues=issues)
|
||||
|
||||
|
||||
def local_installation_expectations(
|
||||
local_requirements: tuple[RequirementEntry, ...],
|
||||
) -> tuple[LocalInstallationExpectation, ...]:
|
||||
expectations: list[LocalInstallationExpectation] = []
|
||||
for requirement in local_requirements:
|
||||
if requirement.pyproject is None:
|
||||
continue
|
||||
with Path(requirement.pyproject).open("rb") as handle:
|
||||
payload = tomllib.load(handle)
|
||||
project = payload.get("project")
|
||||
if not isinstance(project, dict):
|
||||
continue
|
||||
distribution = project.get("name")
|
||||
if not isinstance(distribution, str) or not distribution.strip():
|
||||
continue
|
||||
declared_entry_points: list[tuple[str, str, str]] = []
|
||||
entry_point_groups = project.get("entry-points")
|
||||
if isinstance(entry_point_groups, dict):
|
||||
for group, entries in entry_point_groups.items():
|
||||
if not isinstance(group, str) or not isinstance(entries, dict):
|
||||
continue
|
||||
for name, value in entries.items():
|
||||
if isinstance(name, str) and isinstance(value, str):
|
||||
declared_entry_points.append((group, name, value))
|
||||
expectations.append(
|
||||
LocalInstallationExpectation(
|
||||
requirement=requirement,
|
||||
distribution=distribution,
|
||||
entry_points=tuple(sorted(declared_entry_points)),
|
||||
)
|
||||
)
|
||||
return tuple(expectations)
|
||||
|
||||
|
||||
def requirement_delta_plan(previous_entries: object, current_entries: tuple[RequirementEntry, ...]) -> InstallPlan | None:
|
||||
parsed_previous = previous_requirement_entries(previous_entries)
|
||||
if parsed_previous is None:
|
||||
@@ -481,5 +639,35 @@ def format_command(command: tuple[str, ...]) -> str:
|
||||
return " ".join(shlex.quote(item) for item in command)
|
||||
|
||||
|
||||
_LOCAL_INSTALLATION_PROBE = """
|
||||
import importlib.metadata
|
||||
import json
|
||||
import sys
|
||||
|
||||
expectations = json.load(sys.stdin)
|
||||
issues = {}
|
||||
for expectation in expectations:
|
||||
key = expectation["key"]
|
||||
distribution_name = expectation["distribution"]
|
||||
try:
|
||||
distribution = importlib.metadata.distribution(distribution_name)
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
issues[key] = "distribution is not installed"
|
||||
continue
|
||||
installed = {
|
||||
(entry_point.group, entry_point.name): entry_point.value
|
||||
for entry_point in distribution.entry_points
|
||||
}
|
||||
mismatches = []
|
||||
for entry_point in expectation["entry_points"]:
|
||||
identity = (entry_point["group"], entry_point["name"])
|
||||
if installed.get(identity) != entry_point["value"]:
|
||||
mismatches.append(f"{identity[0]}:{identity[1]}")
|
||||
if mismatches:
|
||||
issues[key] = "entry-point metadata is missing or stale: " + ", ".join(mismatches)
|
||||
print(json.dumps({"issues": issues}, sort_keys=True))
|
||||
"""
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
||||
Reference in New Issue
Block a user