Release v0.1.7
All checks were successful
Dependency Audit / dependency-audit (push) Successful in 1m42s
All checks were successful
Dependency Audit / dependency-audit (push) Successful in 1m42s
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
[](https://git.add-ideas.de/add-ideas/govoplan-core/actions?workflow=module-matrix.yml&actor=0&status=0)
|
[](https://git.add-ideas.de/add-ideas/govoplan-core/actions?workflow=module-matrix.yml&actor=0&status=0)
|
||||||
|
[](https://git.add-ideas.de/add-ideas/govoplan-core/actions?workflow=release-integration.yml&actor=0&status=0)
|
||||||
[](https://git.add-ideas.de/add-ideas/govoplan-core/actions?workflow=dependency-audit.yml&actor=0&status=0)
|
[](https://git.add-ideas.de/add-ideas/govoplan-core/actions?workflow=dependency-audit.yml&actor=0&status=0)
|
||||||
|
|
||||||
# govoplan-core
|
# govoplan-core
|
||||||
|
|||||||
@@ -133,13 +133,12 @@ Access:
|
|||||||
|
|
||||||
Tenancy:
|
Tenancy:
|
||||||
|
|
||||||
- `tenancy.tenant.created`
|
- `tenant.created`
|
||||||
- `tenancy.tenant.updated`
|
- `tenant.updated`
|
||||||
- `tenancy.tenant.suspended`
|
- `tenant.suspended`
|
||||||
- `tenancy.tenant.reactivated`
|
- `tenant.resumed`
|
||||||
- `tenancy.tenant.delete_requested`
|
- `tenant.deletion_requested`
|
||||||
- `tenancy.tenant.delete_blocked`
|
- `tenant.erasure_completed`
|
||||||
- `tenancy.tenant.deleted`
|
|
||||||
|
|
||||||
Policy:
|
Policy:
|
||||||
|
|
||||||
|
|||||||
@@ -458,6 +458,17 @@ routers and live module activation fail fast if two routers register the same
|
|||||||
HTTP method and path. That keeps OpenAPI output and FastAPI route order from
|
HTTP method and path. That keeps OpenAPI output and FastAPI route order from
|
||||||
silently masking a module collision.
|
silently masking a module collision.
|
||||||
|
|
||||||
|
Tenant deletion and cleanup use the registry-owned delete-veto contract. A
|
||||||
|
module that owns tenant-bound data may declare `delete_veto_providers` on its
|
||||||
|
manifest for resource types such as `tenant` or `group`. Providers receive
|
||||||
|
`(session, tenant_id, resource_id)` and should return `DeleteVetoIssue`, an
|
||||||
|
iterable of `DeleteVetoIssue`, or `None`; older exception-based providers are
|
||||||
|
still treated as blocking vetoes. Core attributes each issue to the provider
|
||||||
|
module and adds resource context before the tenancy module exposes the issues
|
||||||
|
through the deletion plan. `blocker` issues prevent destructive or retire
|
||||||
|
operations, `warning` issues explain retained data, and `info` issues document
|
||||||
|
non-blocking lifecycle facts.
|
||||||
|
|
||||||
## Database And Migrations
|
## Database And Migrations
|
||||||
|
|
||||||
Core owns the database/session lifecycle. Modules access the database through core session dependencies and register their models/migrations through their manifest.
|
Core owns the database/session lifecycle. Modules access the database through core session dependencies and register their models/migrations through their manifest.
|
||||||
@@ -492,13 +503,34 @@ against the configured offline license before adding the entry to the install
|
|||||||
plan. Catalog entries may also declare `migration_safety` as `automatic`,
|
plan. Catalog entries may also declare `migration_safety` as `automatic`,
|
||||||
`requires_review`, `forward_only`, or `destructive`; forward-only and
|
`requires_review`, `forward_only`, or `destructive`; forward-only and
|
||||||
destructive entries require explicit operator acknowledgement in the install
|
destructive entries require explicit operator acknowledgement in the install
|
||||||
plan before installer preflight allows activation.
|
plan before installer preflight allows activation. Forward-only and destructive
|
||||||
|
catalog entries must also declare a tested recovery path. Catalog update entries
|
||||||
|
can define direct-update windows with `current_version_min` and
|
||||||
|
`current_version_max_exclusive`, mark intermediate `bridge_release` targets, and
|
||||||
|
explicitly opt into reviewed downgrade or same-version package-refresh plans.
|
||||||
|
Module migration order can be declared with `migration_after` and
|
||||||
|
`migration_before` in manifests or release catalogs; installer preflight turns
|
||||||
|
that metadata, module dependencies, and named interface relationships into an
|
||||||
|
ordered migration plan.
|
||||||
|
|
||||||
|
Modules that need live-data work outside Alembic schema revisions may declare
|
||||||
|
`migration_tasks` on `MigrationSpec`. This is deliberately narrower than a
|
||||||
|
general lifecycle hook system. Each task has a stable `task_id`, one of four
|
||||||
|
phases (`pre_migration_check`, `pre_migration_prepare`,
|
||||||
|
`post_migration_backfill`, `post_migration_verify`), a short operator-facing
|
||||||
|
summary, a task version, safety metadata, and an idempotent executor. Installer
|
||||||
|
preflight blocks non-idempotent tasks, forward-only/destructive tasks without
|
||||||
|
operator acknowledgement, and installed manifest tasks that have no executor.
|
||||||
|
Catalog task metadata is surfaced before activation as pending because the
|
||||||
|
executor can only be verified after the package is installed.
|
||||||
|
|
||||||
Modules should provide:
|
Modules should provide:
|
||||||
|
|
||||||
- pinned backend and WebUI package refs for official catalog entries
|
- pinned backend and WebUI package refs for official catalog entries
|
||||||
- module dependency metadata for catalog target-state planning
|
- module dependency metadata for catalog target-state planning
|
||||||
- migration-safety metadata for catalog update planning
|
- migration-safety metadata for catalog update planning
|
||||||
|
- migration task metadata when live-data checks, preparation, backfills, or
|
||||||
|
verification must run around Alembic
|
||||||
- compatibility metadata in the module manifest
|
- compatibility metadata in the module manifest
|
||||||
- named interface contracts in the manifest and catalog entry when the module
|
- named interface contracts in the manifest and catalog entry when the module
|
||||||
provides or consumes cross-module APIs
|
provides or consumes cross-module APIs
|
||||||
@@ -984,13 +1016,19 @@ The package install-plan API records operator intent only:
|
|||||||
- `POST /api/v1/admin/system/modules/install-plan/catalog/{module_id}` saves
|
- `POST /api/v1/admin/system/modules/install-plan/catalog/{module_id}` saves
|
||||||
a planned install or update row from a validated catalog entry. Installed
|
a planned install or update row from a validated catalog entry. Installed
|
||||||
modules are planned as updates. Catalog signature and approved-channel policy
|
modules are planned as updates. Catalog signature and approved-channel policy
|
||||||
are enforced before the row is saved. The saved plan row can also carry a
|
are enforced before the row is saved. When the selected catalog row requires
|
||||||
|
companion dependency or interface-provider updates, the endpoint adds those
|
||||||
|
rows to the plan automatically. The saved plan row can also carry a
|
||||||
data-safety acknowledgement used by preflight for forward-only or destructive
|
data-safety acknowledgement used by preflight for forward-only or destructive
|
||||||
catalog entries.
|
catalog entries.
|
||||||
- Install-plan preflight returns a structured `target_plan` summary so the
|
- Install-plan preflight returns a structured `target_plan` summary so the
|
||||||
admin UI can show current version, target version, package refs,
|
admin UI can show current version, target version, package refs,
|
||||||
migration-safety level, and acknowledgement state without requiring JSON
|
migration-safety level, update-window and bridge metadata, recovery metadata,
|
||||||
editing.
|
and acknowledgement state without requiring JSON editing.
|
||||||
|
- Install-plan preflight also returns a structured `migration_plan` summary with
|
||||||
|
target enabled modules and ordered module migration steps. When the installer
|
||||||
|
runs with migration enabled, the database migration command receives that
|
||||||
|
target module set and ordered module list.
|
||||||
- `POST /api/v1/admin/system/modules/{module_id}/uninstall-plan` saves a
|
- `POST /api/v1/admin/system/modules/{module_id}/uninstall-plan` saves a
|
||||||
planned non-destructive uninstall row for an installed module after it has
|
planned non-destructive uninstall row for an installed module after it has
|
||||||
been disabled. The Python distribution name is resolved from the installed
|
been disabled. The Python distribution name is resolved from the installed
|
||||||
|
|||||||
@@ -111,10 +111,12 @@ scripts/push-release-tag.sh --version 0.1.6
|
|||||||
`ModuleManifest` objects while writing catalog entries. When a manifest is
|
`ModuleManifest` objects while writing catalog entries. When a manifest is
|
||||||
available, the catalog entry uses the manifest version, points package refs at
|
available, the catalog entry uses the manifest version, points package refs at
|
||||||
`v<manifest.version>`, and copies `provides_interfaces` /
|
`v<manifest.version>`, and copies `provides_interfaces` /
|
||||||
`requires_interfaces` from the manifest. If a manifest cannot be discovered,
|
`requires_interfaces` from the manifest. It also copies module migration order
|
||||||
the entry falls back to the release version passed with `--version` and omits
|
and `migration_tasks` metadata when present. If a manifest cannot be
|
||||||
interface metadata. This keeps the catalog aligned with independently
|
discovered, the entry falls back to the release version passed with `--version`
|
||||||
versioned module packages instead of relying on a hardcoded compatibility table.
|
and omits interface and migration-task metadata. This keeps the catalog aligned
|
||||||
|
with independently versioned module packages instead of relying on a hardcoded
|
||||||
|
compatibility table.
|
||||||
|
|
||||||
The script also includes GovOPlaN roadmap/scaffold module repositories that do
|
The script also includes GovOPlaN roadmap/scaffold module repositories that do
|
||||||
not yet have package metadata. Those repositories are committed, tagged, and
|
not yet have package metadata. Those repositories are committed, tagged, and
|
||||||
@@ -211,6 +213,21 @@ Each module entry can declare:
|
|||||||
or `destructive`
|
or `destructive`
|
||||||
- `migration_notes`, operator-facing data/migration guidance for review,
|
- `migration_notes`, operator-facing data/migration guidance for review,
|
||||||
forward-only, or destructive changes
|
forward-only, or destructive changes
|
||||||
|
- `migration_after` and `migration_before`, explicit module ids used to order
|
||||||
|
module-owned migration heads when a release needs a live-data sequencing rule
|
||||||
|
- `migration_tasks`, constrained live-data tasks that run around Alembic
|
||||||
|
migration phases. Each task declares `task_id`, `phase`, `summary`,
|
||||||
|
`task_version`, `safety`, `idempotent`, and optionally `timeout_seconds`.
|
||||||
|
The allowed phases are `pre_migration_check`, `pre_migration_prepare`,
|
||||||
|
`post_migration_backfill`, and `post_migration_verify`.
|
||||||
|
- `current_version_min` and `current_version_max_exclusive`, the installed
|
||||||
|
version window from which this catalog target may be applied directly
|
||||||
|
- `bridge_release` and `bridge_notes`, marking a target as an intermediate
|
||||||
|
compatibility release in a staged update path
|
||||||
|
- `allow_downgrade` and `allow_same_version`, explicit opt-ins for reviewed
|
||||||
|
rollback or package-refresh plans
|
||||||
|
- `recovery_tested` and `recovery_notes`, documenting the rehearsal for
|
||||||
|
forward-only or destructive data changes
|
||||||
- `provides_interfaces`, named interface contracts exported by this module
|
- `provides_interfaces`, named interface contracts exported by this module
|
||||||
- `requires_interfaces`, named interface contracts and version ranges required
|
- `requires_interfaces`, named interface contracts and version ranges required
|
||||||
by this module
|
by this module
|
||||||
@@ -282,8 +299,18 @@ Catalog provenance changes preflight severity:
|
|||||||
block activation before the installer runs
|
block activation before the installer runs
|
||||||
- selected catalog entries whose target dependencies are neither installed nor
|
- selected catalog entries whose target dependencies are neither installed nor
|
||||||
planned block activation before the installer runs
|
planned block activation before the installer runs
|
||||||
|
- catalog update targets older than the installed module version block unless
|
||||||
|
the catalog entry declares `allow_downgrade: true`
|
||||||
|
- catalog update targets equal to the installed module version block unless the
|
||||||
|
catalog entry declares `allow_same_version: true`
|
||||||
|
- catalog update targets with a `current_version_min` /
|
||||||
|
`current_version_max_exclusive` window block when the installed version is
|
||||||
|
outside that window; publish and apply a bridge release instead
|
||||||
- catalog entries marked `forward_only` or `destructive` block activation until
|
- catalog entries marked `forward_only` or `destructive` block activation until
|
||||||
the plan row has an explicit data-safety acknowledgement
|
the plan row has an explicit data-safety acknowledgement
|
||||||
|
- catalog entries marked `forward_only` or `destructive` also block unless the
|
||||||
|
catalog entry declares `recovery_tested: true` and either the catalog entry or
|
||||||
|
operator plan row contains recovery notes
|
||||||
- catalog entries marked `destructive` also require catalog migration notes or
|
- catalog entries marked `destructive` also require catalog migration notes or
|
||||||
operator notes describing the cleanup or retirement plan
|
operator notes describing the cleanup or retirement plan
|
||||||
|
|
||||||
@@ -298,12 +325,37 @@ activate the resulting set together.
|
|||||||
Install-plan rows support explicit `install`, `update`, and `uninstall`
|
Install-plan rows support explicit `install`, `update`, and `uninstall`
|
||||||
actions. Catalog planning writes `update` when the module is already installed.
|
actions. Catalog planning writes `update` when the module is already installed.
|
||||||
Preflight resolves the target set from installed manifests plus the planned
|
Preflight resolves the target set from installed manifests plus the planned
|
||||||
catalog entries. Unplanned catalog entries are not treated as installed; when
|
catalog entries. Unplanned catalog entries are not treated as installed. When a
|
||||||
they would satisfy a missing dependency or named interface, preflight blocks
|
catalog entry would satisfy a missing dependency or named interface, preflight
|
||||||
activation with a companion-update issue so the operator can add them to the
|
blocks activation with a companion-update issue; the admin catalog planner adds
|
||||||
same plan. The preflight response also includes a structured `target_plan`
|
those companion rows automatically when it can resolve them from the current
|
||||||
summary with each planned module's action, current version, catalog target
|
catalog. The preflight response also includes a structured `target_plan` summary
|
||||||
version, package refs, migration-safety level, and acknowledgement state.
|
with each planned module's action, current version, catalog target version,
|
||||||
|
package refs, migration-safety level, current-version update window, bridge
|
||||||
|
metadata, recovery metadata, and acknowledgement state.
|
||||||
|
|
||||||
|
Database migrations are planned against that same target module set. When the
|
||||||
|
installer is run with `--migrate`, it calls `govoplan_core.commands.init_db`
|
||||||
|
with the target enabled modules rather than the pre-update startup module list,
|
||||||
|
so newly installed module migration directories are discovered before
|
||||||
|
activation. Preflight also returns a structured migration plan. Its step order is
|
||||||
|
derived from:
|
||||||
|
|
||||||
|
- manifest and catalog `migration_after` / `migration_before` declarations
|
||||||
|
- module dependencies and optional dependencies when both modules are in the
|
||||||
|
target plan
|
||||||
|
- named interface provider/consumer relationships when both sides are in the
|
||||||
|
target plan
|
||||||
|
|
||||||
|
Preflight blocks cycles in that ordering graph. It also blocks non-idempotent
|
||||||
|
module migration tasks, forward-only/destructive tasks without operator
|
||||||
|
acknowledgement, and installed manifest tasks that declare no executor.
|
||||||
|
Catalog-only task executors are marked as pending because they can only be
|
||||||
|
confirmed after the target package is installed. The migrator runs pre-migration
|
||||||
|
tasks, upgrades the ordered module heads first, finishes with Alembic `heads`,
|
||||||
|
and then runs post-migration tasks, so Alembic's revision graph remains
|
||||||
|
authoritative while GovOPlaN still gives operators a module-aware live-data
|
||||||
|
order.
|
||||||
|
|
||||||
This avoids circular "upgrade A first / upgrade B first" traps: named interface
|
This avoids circular "upgrade A first / upgrade B first" traps: named interface
|
||||||
requirements are solved against the target set, not against each intermediate
|
requirements are solved against the target set, not against each intermediate
|
||||||
@@ -330,15 +382,20 @@ modules with registered migrations as `requires_review` by default. Release
|
|||||||
authors should keep that value for ordinary reversible migrations, raise it to
|
authors should keep that value for ordinary reversible migrations, raise it to
|
||||||
`forward_only` when database rollback requires restoring a snapshot, and raise
|
`forward_only` when database rollback requires restoring a snapshot, and raise
|
||||||
it to `destructive` when the update removes or irreversibly rewrites persisted
|
it to `destructive` when the update removes or irreversibly rewrites persisted
|
||||||
data. The admin install-plan UI exposes the safety level and lets operators
|
data. Forward-only and destructive entries must include `recovery_tested: true`
|
||||||
record an explicit acknowledgement; preflight keeps acknowledged
|
and recovery notes after a verified restore or forward-recovery rehearsal. The
|
||||||
forward-only/destructive changes visible as warnings.
|
admin install-plan UI exposes the safety level and lets operators record an
|
||||||
|
explicit acknowledgement; preflight keeps acknowledged forward-only/destructive
|
||||||
|
changes visible as warnings.
|
||||||
|
|
||||||
In practice, circular dependencies are avoided by designing interfaces with
|
In practice, circular dependencies are avoided by designing interfaces with
|
||||||
compatibility windows and by publishing bridge releases. A bridge release keeps
|
compatibility windows and by publishing bridge releases. A bridge release keeps
|
||||||
the old interface while introducing the new one, allowing dependent modules to
|
the old interface while introducing the new one, allowing dependent modules to
|
||||||
move first; a later release can retire the old interface after every dependent
|
move first; a later release can retire the old interface after every dependent
|
||||||
module has a compatible target version.
|
module has a compatible target version. Use `current_version_min` and
|
||||||
|
`current_version_max_exclusive` to make those direct-update windows explicit in
|
||||||
|
the catalog, and set `bridge_release: true` on intermediate targets that exist
|
||||||
|
primarily to carry installations safely across a compatibility gap.
|
||||||
|
|
||||||
Trusted catalog keys are configured locally:
|
Trusted catalog keys are configured locally:
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,100 @@
|
|||||||
{
|
{
|
||||||
"version": 1,
|
"releases": [
|
||||||
"releases": []
|
{
|
||||||
|
"heads": [
|
||||||
|
{
|
||||||
|
"owner": "govoplan-campaign",
|
||||||
|
"revision": "2c3d4e5f7081"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-mail",
|
||||||
|
"revision": "3d4e5f708192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-core",
|
||||||
|
"revision": "4f2a9c8e7b6d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-identity",
|
||||||
|
"revision": "5c6d7e8f9a10"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-organizations",
|
||||||
|
"revision": "6d7e8f9a0b1c"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-idm",
|
||||||
|
"revision": "8f9a0b1c2d3e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-calendar",
|
||||||
|
"revision": "9e0f1a2b3c4d"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-files",
|
||||||
|
"revision": "a7b8c9d0e1f3"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"owner_heads": [
|
||||||
|
{
|
||||||
|
"owner": "govoplan-access",
|
||||||
|
"revisions": [
|
||||||
|
"4a5b6c7d8e9f"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-calendar",
|
||||||
|
"revisions": [
|
||||||
|
"9e0f1a2b3c4d"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-campaign",
|
||||||
|
"revisions": [
|
||||||
|
"2c3d4e5f7081"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-core",
|
||||||
|
"revisions": [
|
||||||
|
"4f2a9c8e7b6d"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-files",
|
||||||
|
"revisions": [
|
||||||
|
"a7b8c9d0e1f3"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-identity",
|
||||||
|
"revisions": [
|
||||||
|
"5c6d7e8f9a10"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-idm",
|
||||||
|
"revisions": [
|
||||||
|
"8f9a0b1c2d3e"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-mail",
|
||||||
|
"revisions": [
|
||||||
|
"3d4e5f708192"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "govoplan-organizations",
|
||||||
|
"revisions": [
|
||||||
|
"6d7e8f9a0b1c"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"recorded_at": "2026-07-11T00:19:07Z",
|
||||||
|
"release": "0.1.7",
|
||||||
|
"squash_policy": "reviewed-manual"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,29 @@
|
|||||||
"python_ref": "govoplan-files @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git@v0.1.4",
|
"python_ref": "govoplan-files @ git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git@v0.1.4",
|
||||||
"webui_package": "@govoplan/files-webui",
|
"webui_package": "@govoplan/files-webui",
|
||||||
"webui_ref": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git#v0.1.4",
|
"webui_ref": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git#v0.1.4",
|
||||||
|
"migration_safety": "forward_only",
|
||||||
|
"migration_notes": "Database rollback requires restoring the pre-update snapshot.",
|
||||||
|
"migration_after": ["access"],
|
||||||
|
"migration_before": ["campaigns"],
|
||||||
|
"migration_tasks": [
|
||||||
|
{
|
||||||
|
"task_id": "backfill_spaces",
|
||||||
|
"phase": "post_migration_backfill",
|
||||||
|
"summary": "Backfill file spaces after the schema migration.",
|
||||||
|
"task_version": "1",
|
||||||
|
"safety": "forward_only",
|
||||||
|
"idempotent": true,
|
||||||
|
"timeout_seconds": 300
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"current_version_min": "0.1.0",
|
||||||
|
"current_version_max_exclusive": "0.2.0",
|
||||||
|
"bridge_release": true,
|
||||||
|
"bridge_notes": "Keeps the 0.1.x campaign attachment interface while introducing the 0.2.x contract.",
|
||||||
|
"allow_downgrade": false,
|
||||||
|
"allow_same_version": false,
|
||||||
|
"recovery_tested": true,
|
||||||
|
"recovery_notes": "Snapshot restore and forward recovery were rehearsed on the release candidate dataset.",
|
||||||
"provides_interfaces": [
|
"provides_interfaces": [
|
||||||
{
|
{
|
||||||
"name": "files.campaign_attachments",
|
"name": "files.campaign_attachments",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "govoplan-core"
|
name = "govoplan-core"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ CATALOG_MODULES = (
|
|||||||
name="Policy",
|
name="Policy",
|
||||||
description="Policy and governance capability module.",
|
description="Policy and governance capability module.",
|
||||||
tags=("official", "platform-module"),
|
tags=("official", "platform-module"),
|
||||||
|
webui_package="@govoplan/policy-webui",
|
||||||
),
|
),
|
||||||
CatalogModule(
|
CatalogModule(
|
||||||
module_id="audit",
|
module_id="audit",
|
||||||
@@ -95,6 +96,7 @@ CATALOG_MODULES = (
|
|||||||
name="Audit",
|
name="Audit",
|
||||||
description="Audit-log storage and audit administration routes.",
|
description="Audit-log storage and audit administration routes.",
|
||||||
tags=("official", "platform-module"),
|
tags=("official", "platform-module"),
|
||||||
|
webui_package="@govoplan/audit-webui",
|
||||||
),
|
),
|
||||||
CatalogModule(
|
CatalogModule(
|
||||||
module_id="dashboard",
|
module_id="dashboard",
|
||||||
@@ -303,6 +305,25 @@ def _manifest_catalog_metadata(manifest: ModuleManifest | None) -> dict[str, obj
|
|||||||
if manifest.migration_spec is not None:
|
if manifest.migration_spec is not None:
|
||||||
payload["migration_safety"] = "requires_review"
|
payload["migration_safety"] = "requires_review"
|
||||||
payload["migration_notes"] = "Module owns database migrations; review release notes and migration output before activation."
|
payload["migration_notes"] = "Module owns database migrations; review release notes and migration output before activation."
|
||||||
|
if manifest.migration_spec.migration_after:
|
||||||
|
payload["migration_after"] = list(manifest.migration_spec.migration_after)
|
||||||
|
if manifest.migration_spec.migration_before:
|
||||||
|
payload["migration_before"] = list(manifest.migration_spec.migration_before)
|
||||||
|
if manifest.migration_spec.migration_tasks:
|
||||||
|
tasks: list[dict[str, object]] = []
|
||||||
|
for task in manifest.migration_spec.migration_tasks:
|
||||||
|
task_payload: dict[str, object] = {
|
||||||
|
"task_id": task.task_id,
|
||||||
|
"phase": task.phase,
|
||||||
|
"summary": task.summary,
|
||||||
|
"task_version": task.task_version,
|
||||||
|
"safety": task.safety,
|
||||||
|
"idempotent": task.idempotent,
|
||||||
|
}
|
||||||
|
if task.timeout_seconds is not None:
|
||||||
|
task_payload["timeout_seconds"] = task.timeout_seconds
|
||||||
|
tasks.append(task_payload)
|
||||||
|
payload["migration_tasks"] = tasks
|
||||||
if manifest.provides_interfaces:
|
if manifest.provides_interfaces:
|
||||||
payload["provides_interfaces"] = [
|
payload["provides_interfaces"] = [
|
||||||
{"name": item.name, "version": item.version}
|
{"name": item.name, "version": item.version}
|
||||||
|
|||||||
@@ -92,19 +92,39 @@ PARENT="$(dirname "$ROOT")"
|
|||||||
PACKAGE_MODULE_REPOS=(
|
PACKAGE_MODULE_REPOS=(
|
||||||
"$PARENT/govoplan-access"
|
"$PARENT/govoplan-access"
|
||||||
"$PARENT/govoplan-admin"
|
"$PARENT/govoplan-admin"
|
||||||
"$PARENT/govoplan-tenancy"
|
"$PARENT/govoplan-approvals"
|
||||||
"$PARENT/govoplan-organizations"
|
"$PARENT/govoplan-assets"
|
||||||
|
"$PARENT/govoplan-audit"
|
||||||
|
"$PARENT/govoplan-booking"
|
||||||
|
"$PARENT/govoplan-calendar"
|
||||||
|
"$PARENT/govoplan-campaign"
|
||||||
|
"$PARENT/govoplan-certificates"
|
||||||
|
"$PARENT/govoplan-committee"
|
||||||
|
"$PARENT/govoplan-consultation"
|
||||||
|
"$PARENT/govoplan-contracts"
|
||||||
|
"$PARENT/govoplan-dashboard"
|
||||||
|
"$PARENT/govoplan-docs"
|
||||||
|
"$PARENT/govoplan-facilities"
|
||||||
|
"$PARENT/govoplan-files"
|
||||||
|
"$PARENT/govoplan-forms-runtime"
|
||||||
|
"$PARENT/govoplan-grants"
|
||||||
|
"$PARENT/govoplan-helpdesk"
|
||||||
"$PARENT/govoplan-identity"
|
"$PARENT/govoplan-identity"
|
||||||
"$PARENT/govoplan-idm"
|
"$PARENT/govoplan-idm"
|
||||||
"$PARENT/govoplan-policy"
|
"$PARENT/govoplan-inspections"
|
||||||
"$PARENT/govoplan-audit"
|
"$PARENT/govoplan-issue-reporting"
|
||||||
"$PARENT/govoplan-dashboard"
|
"$PARENT/govoplan-learning"
|
||||||
"$PARENT/govoplan-files"
|
|
||||||
"$PARENT/govoplan-mail"
|
"$PARENT/govoplan-mail"
|
||||||
"$PARENT/govoplan-campaign"
|
|
||||||
"$PARENT/govoplan-calendar"
|
|
||||||
"$PARENT/govoplan-docs"
|
|
||||||
"$PARENT/govoplan-ops"
|
"$PARENT/govoplan-ops"
|
||||||
|
"$PARENT/govoplan-organizations"
|
||||||
|
"$PARENT/govoplan-permits"
|
||||||
|
"$PARENT/govoplan-policy"
|
||||||
|
"$PARENT/govoplan-procurement"
|
||||||
|
"$PARENT/govoplan-records"
|
||||||
|
"$PARENT/govoplan-resources"
|
||||||
|
"$PARENT/govoplan-risk-compliance"
|
||||||
|
"$PARENT/govoplan-tenancy"
|
||||||
|
"$PARENT/govoplan-transparency"
|
||||||
)
|
)
|
||||||
TAG_ONLY_MODULE_REPOS=(
|
TAG_ONLY_MODULE_REPOS=(
|
||||||
"$PARENT/govoplan-addresses"
|
"$PARENT/govoplan-addresses"
|
||||||
@@ -120,6 +140,7 @@ TAG_ONLY_MODULE_REPOS=(
|
|||||||
"$PARENT/govoplan-notifications"
|
"$PARENT/govoplan-notifications"
|
||||||
"$PARENT/govoplan-payments"
|
"$PARENT/govoplan-payments"
|
||||||
"$PARENT/govoplan-portal"
|
"$PARENT/govoplan-portal"
|
||||||
|
"$PARENT/govoplan-postbox"
|
||||||
"$PARENT/govoplan-reporting"
|
"$PARENT/govoplan-reporting"
|
||||||
"$PARENT/govoplan-scheduling"
|
"$PARENT/govoplan-scheduling"
|
||||||
"$PARENT/govoplan-search"
|
"$PARENT/govoplan-search"
|
||||||
@@ -173,6 +194,9 @@ if [[ -z "$NPM_BIN" ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
export GIT_SSH_COMMAND="${GIT_SSH_COMMAND:-ssh -o BatchMode=yes -o ConnectTimeout=15}"
|
||||||
|
GIT_REMOTE_TIMEOUT="${GIT_REMOTE_TIMEOUT:-30}"
|
||||||
|
|
||||||
normalize_bump() {
|
normalize_bump() {
|
||||||
local value="${1,,}"
|
local value="${1,,}"
|
||||||
case "$value" in
|
case "$value" in
|
||||||
@@ -282,56 +306,20 @@ update_manifest_version() {
|
|||||||
local repo="$1"
|
local repo="$1"
|
||||||
local project_name="$2"
|
local project_name="$2"
|
||||||
local version="$3"
|
local version="$3"
|
||||||
local manifest_path=""
|
local manifest_paths=()
|
||||||
|
|
||||||
case "$project_name" in
|
if [[ "$project_name" == "govoplan-core" ]]; then
|
||||||
govoplan-core)
|
return 0
|
||||||
return 0
|
fi
|
||||||
;;
|
|
||||||
govoplan-access)
|
|
||||||
manifest_path="$repo/src/govoplan_access/backend/manifest.py"
|
|
||||||
;;
|
|
||||||
govoplan-admin)
|
|
||||||
manifest_path="$repo/src/govoplan_admin/backend/manifest.py"
|
|
||||||
;;
|
|
||||||
govoplan-tenancy)
|
|
||||||
manifest_path="$repo/src/govoplan_tenancy/backend/manifest.py"
|
|
||||||
;;
|
|
||||||
govoplan-organizations)
|
|
||||||
manifest_path="$repo/src/govoplan_organizations/backend/manifest.py"
|
|
||||||
;;
|
|
||||||
govoplan-identity)
|
|
||||||
manifest_path="$repo/src/govoplan_identity/backend/manifest.py"
|
|
||||||
;;
|
|
||||||
govoplan-idm)
|
|
||||||
manifest_path="$repo/src/govoplan_idm/backend/manifest.py"
|
|
||||||
;;
|
|
||||||
govoplan-policy)
|
|
||||||
manifest_path="$repo/src/govoplan_policy/backend/manifest.py"
|
|
||||||
;;
|
|
||||||
govoplan-audit)
|
|
||||||
manifest_path="$repo/src/govoplan_audit/backend/manifest.py"
|
|
||||||
;;
|
|
||||||
govoplan-files)
|
|
||||||
manifest_path="$repo/src/govoplan_files/backend/manifest.py"
|
|
||||||
;;
|
|
||||||
govoplan-mail)
|
|
||||||
manifest_path="$repo/src/govoplan_mail/backend/manifest.py"
|
|
||||||
;;
|
|
||||||
govoplan-campaign)
|
|
||||||
manifest_path="$repo/src/govoplan_campaign/backend/manifest.py"
|
|
||||||
;;
|
|
||||||
govoplan-calendar)
|
|
||||||
manifest_path="$repo/src/govoplan_calendar/backend/manifest.py"
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
fail "unknown project for manifest version update: $project_name"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
[[ -f "$manifest_path" ]] || fail "missing module manifest: $manifest_path"
|
while IFS= read -r -d '' manifest_path; do
|
||||||
|
manifest_paths+=("$manifest_path")
|
||||||
|
done < <(find "$repo/src" -path "*/backend/manifest.py" -print0 2>/dev/null)
|
||||||
|
|
||||||
"$PYTHON" - "$manifest_path" "$version" <<'PYCODE'
|
[[ "${#manifest_paths[@]}" -gt 0 ]] || fail "missing module manifest under $repo/src"
|
||||||
|
|
||||||
|
for manifest_path in "${manifest_paths[@]}"; do
|
||||||
|
"$PYTHON" - "$manifest_path" "$version" <<'PYCODE'
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pathlib
|
import pathlib
|
||||||
@@ -347,10 +335,18 @@ text, count = re.subn(
|
|||||||
text,
|
text,
|
||||||
count=1,
|
count=1,
|
||||||
)
|
)
|
||||||
|
if count == 0:
|
||||||
|
text, count = re.subn(
|
||||||
|
r'(?m)^(MODULE_VERSION\s*=\s*)["\'][^"\']+["\'](\s*)$',
|
||||||
|
rf'\1"{new_version}"\2',
|
||||||
|
text,
|
||||||
|
count=1,
|
||||||
|
)
|
||||||
if count != 1:
|
if count != 1:
|
||||||
raise SystemExit(f"could not update ModuleManifest.version in {path}")
|
raise SystemExit(f"could not update ModuleManifest.version in {path}")
|
||||||
path.write_text(text)
|
path.write_text(text)
|
||||||
PYCODE
|
PYCODE
|
||||||
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
update_webui_package() {
|
update_webui_package() {
|
||||||
@@ -729,7 +725,7 @@ for repo in "${REPOS[@]}"; do
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
set +e
|
set +e
|
||||||
git -C "$repo" ls-remote --exit-code --tags "$REMOTE" "refs/tags/$TAG" >/dev/null
|
timeout "$GIT_REMOTE_TIMEOUT" git -C "$repo" ls-remote --exit-code --tags "$REMOTE" "refs/tags/$TAG" >/dev/null
|
||||||
ls_remote_status=$?
|
ls_remote_status=$?
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
@@ -790,9 +786,14 @@ for repo in "${MODULE_REPOS[@]}"; do
|
|||||||
if [[ "$DRY_RUN" -eq 0 ]]; then
|
if [[ "$DRY_RUN" -eq 0 ]]; then
|
||||||
if git -C "$repo" diff --cached --quiet; then
|
if git -C "$repo" diff --cached --quiet; then
|
||||||
if [[ "${REPO_KINDS[$repo]}" == "package" ]]; then
|
if [[ "${REPO_KINDS[$repo]}" == "package" ]]; then
|
||||||
fail "no staged changes for ${PROJECT_NAMES[$repo]} after version bump"
|
if [[ "$TARGET_VERSION" == "${OLD_VERSIONS[$repo]}" ]]; then
|
||||||
|
echo "No staged changes for ${PROJECT_NAMES[$repo]}; version is already $TARGET_VERSION, tagging current HEAD."
|
||||||
|
else
|
||||||
|
fail "no staged changes for ${PROJECT_NAMES[$repo]} after version bump"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No staged changes for ${PROJECT_NAMES[$repo]}; tagging current HEAD."
|
||||||
fi
|
fi
|
||||||
echo "No staged changes for ${PROJECT_NAMES[$repo]}; tagging current HEAD."
|
|
||||||
else
|
else
|
||||||
run git -C "$repo" commit -m "$COMMIT_MESSAGE"
|
run git -C "$repo" commit -m "$COMMIT_MESSAGE"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from govoplan_core.db.bootstrap import bootstrap_dev_data
|
from govoplan_core.db.bootstrap import bootstrap_dev_data
|
||||||
from govoplan_core.db.migrations import migrate_database
|
from govoplan_core.db.migrations import (
|
||||||
|
ModuleMigrationTaskExecutionError,
|
||||||
|
POST_MIGRATION_TASK_PHASES,
|
||||||
|
PRE_MIGRATION_TASK_PHASES,
|
||||||
|
migrate_database,
|
||||||
|
run_registered_module_migration_tasks,
|
||||||
|
)
|
||||||
from govoplan_core.db.session import configure_database, get_database
|
from govoplan_core.db.session import configure_database, get_database
|
||||||
from govoplan_core.settings import settings
|
from govoplan_core.settings import settings
|
||||||
|
|
||||||
@@ -11,12 +19,43 @@ from govoplan_core.settings import settings
|
|||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser(description="Initialize the GovOPlaN database")
|
parser = argparse.ArgumentParser(description="Initialize the GovOPlaN database")
|
||||||
parser.add_argument("--database-url", default=settings.database_url, help="Database URL to migrate")
|
parser.add_argument("--database-url", default=settings.database_url, help="Database URL to migrate")
|
||||||
|
parser.add_argument("--enabled-module", action="append", default=[], help="Target enabled module id used to discover module migrations; may be repeated.")
|
||||||
|
parser.add_argument("--migration-module", action="append", default=[], help="Module id whose migration heads should be upgraded in this order before final heads.")
|
||||||
|
parser.add_argument("--migration-task-record-output", type=Path, help="Write executed module migration task records to this JSON file.")
|
||||||
parser.add_argument("--with-dev-data", action="store_true", help="Create default tenant/user/roles and a development API key")
|
parser.add_argument("--with-dev-data", action="store_true", help="Create default tenant/user/roles and a development API key")
|
||||||
parser.add_argument("--dev-api-key", default=settings.dev_bootstrap_api_key, help="Development API key secret to create")
|
parser.add_argument("--dev-api-key", default=settings.dev_bootstrap_api_key, help="Development API key secret to create")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
configure_database(args.database_url)
|
configure_database(args.database_url)
|
||||||
migration = migrate_database(database_url=args.database_url)
|
enabled_modules = tuple(args.enabled_module) if args.enabled_module else None
|
||||||
|
migration_order = tuple(args.migration_module) if args.migration_module else None
|
||||||
|
task_records: list[dict[str, object]] = []
|
||||||
|
try:
|
||||||
|
_run_migration_tasks(
|
||||||
|
task_records,
|
||||||
|
database_url=args.database_url,
|
||||||
|
enabled_modules=enabled_modules,
|
||||||
|
migration_order=migration_order,
|
||||||
|
phases=PRE_MIGRATION_TASK_PHASES,
|
||||||
|
)
|
||||||
|
migration = migrate_database(
|
||||||
|
database_url=args.database_url,
|
||||||
|
enabled_modules=enabled_modules,
|
||||||
|
migration_module_order=migration_order,
|
||||||
|
)
|
||||||
|
_run_migration_tasks(
|
||||||
|
task_records,
|
||||||
|
database_url=args.database_url,
|
||||||
|
enabled_modules=enabled_modules,
|
||||||
|
migration_order=migration_order,
|
||||||
|
phases=POST_MIGRATION_TASK_PHASES,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
if args.migration_task_record_output:
|
||||||
|
args.migration_task_record_output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
args.migration_task_record_output.write_text(json.dumps(task_records, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
if task_records:
|
||||||
|
print(f"Executed {len(task_records)} module migration task(s).")
|
||||||
if migration.reconciled_revision:
|
if migration.reconciled_revision:
|
||||||
print(f"Reconciled legacy database marker to {migration.reconciled_revision}.")
|
print(f"Reconciled legacy database marker to {migration.reconciled_revision}.")
|
||||||
print(f"Database schema upgraded to {migration.current_revision}.")
|
print(f"Database schema upgraded to {migration.current_revision}.")
|
||||||
@@ -33,5 +72,26 @@ def main() -> None:
|
|||||||
print("Development API key already exists or was not requested.")
|
print("Development API key already exists or was not requested.")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_migration_tasks(
|
||||||
|
target: list[dict[str, object]],
|
||||||
|
*,
|
||||||
|
database_url: str,
|
||||||
|
enabled_modules: tuple[str, ...] | None,
|
||||||
|
migration_order: tuple[str, ...] | None,
|
||||||
|
phases: tuple[str, ...],
|
||||||
|
) -> None:
|
||||||
|
try:
|
||||||
|
records = run_registered_module_migration_tasks(
|
||||||
|
database_url=database_url,
|
||||||
|
enabled_modules=enabled_modules,
|
||||||
|
migration_module_order=migration_order,
|
||||||
|
phases=phases,
|
||||||
|
)
|
||||||
|
except ModuleMigrationTaskExecutionError as exc:
|
||||||
|
target.extend(exc.records)
|
||||||
|
raise
|
||||||
|
target.extend(records)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -12,6 +12,29 @@ from govoplan_core.core.registry import PlatformRegistry
|
|||||||
class MigrationMetadataPlan:
|
class MigrationMetadataPlan:
|
||||||
metadata: tuple[MetaData, ...]
|
metadata: tuple[MetaData, ...]
|
||||||
script_locations: tuple[str, ...]
|
script_locations: tuple[str, ...]
|
||||||
|
modules: tuple[ModuleMigrationMetadata, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ModuleMigrationMetadata:
|
||||||
|
module_id: str
|
||||||
|
script_location: str | None = None
|
||||||
|
has_metadata: bool = False
|
||||||
|
migration_after: tuple[str, ...] = ()
|
||||||
|
migration_before: tuple[str, ...] = ()
|
||||||
|
migration_tasks: tuple[ModuleMigrationTaskMetadata, ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ModuleMigrationTaskMetadata:
|
||||||
|
task_id: str
|
||||||
|
phase: str
|
||||||
|
summary: str
|
||||||
|
task_version: str = "1"
|
||||||
|
safety: str = "automatic"
|
||||||
|
idempotent: bool = True
|
||||||
|
timeout_seconds: int | None = None
|
||||||
|
has_executor: bool = False
|
||||||
|
|
||||||
|
|
||||||
def migration_metadata_plan(registry: PlatformRegistry, *, extra_metadata: Iterable[MetaData] = ()) -> MigrationMetadataPlan:
|
def migration_metadata_plan(registry: PlatformRegistry, *, extra_metadata: Iterable[MetaData] = ()) -> MigrationMetadataPlan:
|
||||||
@@ -28,4 +51,28 @@ def migration_metadata_plan(registry: PlatformRegistry, *, extra_metadata: Itera
|
|||||||
metadata.append(spec.metadata)
|
metadata.append(spec.metadata)
|
||||||
if spec.script_location:
|
if spec.script_location:
|
||||||
script_locations.append(spec.script_location)
|
script_locations.append(spec.script_location)
|
||||||
return MigrationMetadataPlan(metadata=tuple(metadata), script_locations=tuple(script_locations))
|
modules = tuple(
|
||||||
|
ModuleMigrationMetadata(
|
||||||
|
module_id=manifest.id,
|
||||||
|
script_location=manifest.migration_spec.script_location,
|
||||||
|
has_metadata=isinstance(manifest.migration_spec.metadata, MetaData),
|
||||||
|
migration_after=tuple(manifest.migration_spec.migration_after),
|
||||||
|
migration_before=tuple(manifest.migration_spec.migration_before),
|
||||||
|
migration_tasks=tuple(
|
||||||
|
ModuleMigrationTaskMetadata(
|
||||||
|
task_id=task.task_id,
|
||||||
|
phase=task.phase,
|
||||||
|
summary=task.summary,
|
||||||
|
task_version=task.task_version,
|
||||||
|
safety=task.safety,
|
||||||
|
idempotent=task.idempotent,
|
||||||
|
timeout_seconds=task.timeout_seconds,
|
||||||
|
has_executor=task.executor is not None,
|
||||||
|
)
|
||||||
|
for task in manifest.migration_spec.migration_tasks
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for manifest in registry.manifests()
|
||||||
|
if manifest.migration_spec is not None
|
||||||
|
)
|
||||||
|
return MigrationMetadataPlan(metadata=tuple(metadata), script_locations=tuple(script_locations), modules=modules)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,12 @@ from govoplan_core.core.versioning import format_version_range, version_range_is
|
|||||||
|
|
||||||
_INTERFACE_NAME_RE = re.compile(r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$")
|
_INTERFACE_NAME_RE = re.compile(r"^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$")
|
||||||
CATALOG_MIGRATION_SAFETY = ("automatic", "requires_review", "forward_only", "destructive")
|
CATALOG_MIGRATION_SAFETY = ("automatic", "requires_review", "forward_only", "destructive")
|
||||||
|
CATALOG_MIGRATION_TASK_PHASES = (
|
||||||
|
"pre_migration_check",
|
||||||
|
"pre_migration_prepare",
|
||||||
|
"post_migration_backfill",
|
||||||
|
"post_migration_verify",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def module_package_catalog(
|
def module_package_catalog(
|
||||||
@@ -611,6 +617,17 @@ def _normalize_catalog_item(value: Any) -> dict[str, object]:
|
|||||||
"optional_dependencies": _string_list(value.get("optional_dependencies")),
|
"optional_dependencies": _string_list(value.get("optional_dependencies")),
|
||||||
"migration_safety": _catalog_migration_safety(value.get("migration_safety"), module_id=module_id),
|
"migration_safety": _catalog_migration_safety(value.get("migration_safety"), module_id=module_id),
|
||||||
"migration_notes": _optional_str(value, "migration_notes"),
|
"migration_notes": _optional_str(value, "migration_notes"),
|
||||||
|
"migration_after": _string_list(value.get("migration_after")),
|
||||||
|
"migration_before": _string_list(value.get("migration_before")),
|
||||||
|
"migration_tasks": _normalize_catalog_migration_tasks(value.get("migration_tasks"), module_id=module_id),
|
||||||
|
"current_version_min": _optional_str(value, "current_version_min"),
|
||||||
|
"current_version_max_exclusive": _optional_str(value, "current_version_max_exclusive"),
|
||||||
|
"bridge_release": _optional_bool(value, "bridge_release"),
|
||||||
|
"bridge_notes": _optional_str(value, "bridge_notes"),
|
||||||
|
"allow_downgrade": _optional_bool(value, "allow_downgrade"),
|
||||||
|
"allow_same_version": _optional_bool(value, "allow_same_version"),
|
||||||
|
"recovery_tested": _optional_bool(value, "recovery_tested"),
|
||||||
|
"recovery_notes": _optional_str(value, "recovery_notes"),
|
||||||
"python_package": _optional_str(value, "python_package"),
|
"python_package": _optional_str(value, "python_package"),
|
||||||
"python_ref": _optional_str(value, "python_ref"),
|
"python_ref": _optional_str(value, "python_ref"),
|
||||||
"webui_package": _optional_str(value, "webui_package"),
|
"webui_package": _optional_str(value, "webui_package"),
|
||||||
@@ -621,6 +638,15 @@ def _normalize_catalog_item(value: Any) -> dict[str, object]:
|
|||||||
"notes": _optional_str(value, "notes"),
|
"notes": _optional_str(value, "notes"),
|
||||||
"tags": _string_list(value.get("tags")),
|
"tags": _string_list(value.get("tags")),
|
||||||
}
|
}
|
||||||
|
if not version_range_is_valid(
|
||||||
|
version_min=item["current_version_min"] if isinstance(item["current_version_min"], str) else None,
|
||||||
|
version_max_exclusive=item["current_version_max_exclusive"] if isinstance(item["current_version_max_exclusive"], str) else None,
|
||||||
|
):
|
||||||
|
version_range = format_version_range(
|
||||||
|
version_min=item["current_version_min"] if isinstance(item["current_version_min"], str) else None,
|
||||||
|
version_max_exclusive=item["current_version_max_exclusive"] if isinstance(item["current_version_max_exclusive"], str) else None,
|
||||||
|
)
|
||||||
|
raise ValueError(f"Module package catalog entry {module_id!r} has invalid current-version range {version_range!r}.")
|
||||||
artifact_integrity = _normalize_artifact_integrity(value.get("artifact_integrity"))
|
artifact_integrity = _normalize_artifact_integrity(value.get("artifact_integrity"))
|
||||||
if artifact_integrity:
|
if artifact_integrity:
|
||||||
item["artifact_integrity"] = artifact_integrity
|
item["artifact_integrity"] = artifact_integrity
|
||||||
@@ -637,6 +663,66 @@ def _catalog_migration_safety(value: Any, *, module_id: str) -> str:
|
|||||||
return safety
|
return safety
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_catalog_migration_tasks(value: Any, *, module_id: str) -> list[dict[str, object]]:
|
||||||
|
if value is None:
|
||||||
|
return []
|
||||||
|
if not isinstance(value, list):
|
||||||
|
raise ValueError(f"Module package catalog migration_tasks for {module_id!r} must be a list.")
|
||||||
|
normalized: list[dict[str, object]] = []
|
||||||
|
seen: set[str] = set()
|
||||||
|
for raw in value:
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise ValueError(f"Module package catalog migration_tasks entries for {module_id!r} must be objects.")
|
||||||
|
task_id = _catalog_task_required_str(raw, "task_id", module_id=module_id)
|
||||||
|
if task_id in seen:
|
||||||
|
raise ValueError(f"Module package catalog entry {module_id!r} declares migration task {task_id!r} more than once.")
|
||||||
|
seen.add(task_id)
|
||||||
|
phase = _catalog_task_required_str(raw, "phase", module_id=module_id)
|
||||||
|
if phase not in CATALOG_MIGRATION_TASK_PHASES:
|
||||||
|
allowed = ", ".join(CATALOG_MIGRATION_TASK_PHASES)
|
||||||
|
raise ValueError(f"Unsupported migration task phase for {module_id!r}/{task_id!r}: {phase!r}; expected one of {allowed}.")
|
||||||
|
summary = _catalog_task_required_str(raw, "summary", module_id=module_id)
|
||||||
|
safety = _catalog_migration_safety(raw.get("safety"), module_id=module_id)
|
||||||
|
item: dict[str, object] = {
|
||||||
|
"task_id": task_id,
|
||||||
|
"phase": phase,
|
||||||
|
"summary": summary,
|
||||||
|
"task_version": _optional_str(raw, "task_version") or "1",
|
||||||
|
"safety": safety,
|
||||||
|
"idempotent": _catalog_optional_bool_default(raw, "idempotent", default=True),
|
||||||
|
}
|
||||||
|
timeout_seconds = _catalog_optional_positive_int(raw, "timeout_seconds", module_id=module_id, task_id=task_id)
|
||||||
|
if timeout_seconds is not None:
|
||||||
|
item["timeout_seconds"] = timeout_seconds
|
||||||
|
normalized.append(item)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _catalog_task_required_str(value: dict[str, Any], key: str, *, module_id: str) -> str:
|
||||||
|
item = _optional_str(value, key)
|
||||||
|
if not item:
|
||||||
|
raise ValueError(f"Module package catalog migration task for {module_id!r} is missing {key!r}.")
|
||||||
|
return item
|
||||||
|
|
||||||
|
|
||||||
|
def _catalog_optional_bool_default(value: dict[str, Any], key: str, *, default: bool) -> bool:
|
||||||
|
if key not in value:
|
||||||
|
return default
|
||||||
|
return _optional_bool(value, key)
|
||||||
|
|
||||||
|
|
||||||
|
def _catalog_optional_positive_int(value: dict[str, Any], key: str, *, module_id: str, task_id: str) -> int | None:
|
||||||
|
if value.get(key) is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
integer = int(value[key])
|
||||||
|
except (TypeError, ValueError) as exc:
|
||||||
|
raise ValueError(f"Module package catalog migration task {module_id!r}/{task_id!r} has invalid {key!r}.") from exc
|
||||||
|
if integer <= 0:
|
||||||
|
raise ValueError(f"Module package catalog migration task {module_id!r}/{task_id!r} requires positive {key!r}.")
|
||||||
|
return integer
|
||||||
|
|
||||||
|
|
||||||
def _required_str(value: dict[str, Any], key: str) -> str:
|
def _required_str(value: dict[str, Any], key: str) -> str:
|
||||||
item = _optional_str(value, key)
|
item = _optional_str(value, key)
|
||||||
if not item:
|
if not item:
|
||||||
|
|||||||
@@ -13,6 +13,14 @@ SUPPORTED_FRONTEND_ASSET_MANIFEST_CONTRACT_VERSION = "1"
|
|||||||
|
|
||||||
PermissionLevel = Literal["system", "tenant"]
|
PermissionLevel = Literal["system", "tenant"]
|
||||||
SubjectType = Literal["account", "membership", "group", "service_account", "tenant"]
|
SubjectType = Literal["account", "membership", "group", "service_account", "tenant"]
|
||||||
|
MigrationTaskPhase = Literal[
|
||||||
|
"pre_migration_check",
|
||||||
|
"pre_migration_prepare",
|
||||||
|
"post_migration_backfill",
|
||||||
|
"post_migration_verify",
|
||||||
|
]
|
||||||
|
MigrationTaskSafety = Literal["automatic", "requires_review", "forward_only", "destructive"]
|
||||||
|
MigrationTaskStatus = Literal["ok", "warning", "blocked", "skipped"]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
@@ -91,11 +99,49 @@ MigrationRetirementExecutor = Callable[[object, str], None]
|
|||||||
MigrationRetirementProvider = Callable[[object | None, str], MigrationRetirementPlan]
|
MigrationRetirementProvider = Callable[[object | None, str], MigrationRetirementPlan]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ModuleMigrationTaskContext:
|
||||||
|
module_id: str
|
||||||
|
task_id: str
|
||||||
|
phase: MigrationTaskPhase
|
||||||
|
database_url: str
|
||||||
|
target_version: str | None = None
|
||||||
|
session: object | None = None
|
||||||
|
dry_run: bool = False
|
||||||
|
metadata: Mapping[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ModuleMigrationTaskResult:
|
||||||
|
status: MigrationTaskStatus = "ok"
|
||||||
|
message: str | None = None
|
||||||
|
details: Mapping[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
ModuleMigrationTaskExecutor = Callable[[ModuleMigrationTaskContext], ModuleMigrationTaskResult | None]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class ModuleMigrationTask:
|
||||||
|
task_id: str
|
||||||
|
phase: MigrationTaskPhase
|
||||||
|
summary: str
|
||||||
|
task_version: str = "1"
|
||||||
|
safety: MigrationTaskSafety = "automatic"
|
||||||
|
idempotent: bool = True
|
||||||
|
timeout_seconds: int | None = None
|
||||||
|
executor: ModuleMigrationTaskExecutor | None = None
|
||||||
|
metadata: Mapping[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
class MigrationSpec:
|
class MigrationSpec:
|
||||||
module_id: str
|
module_id: str
|
||||||
metadata: object | None = None
|
metadata: object | None = None
|
||||||
script_location: str | None = None
|
script_location: str | None = None
|
||||||
|
migration_after: tuple[str, ...] = ()
|
||||||
|
migration_before: tuple[str, ...] = ()
|
||||||
|
migration_tasks: tuple[ModuleMigrationTask, ...] = ()
|
||||||
retirement_supported: bool = False
|
retirement_supported: bool = False
|
||||||
retirement_provider: MigrationRetirementProvider | None = None
|
retirement_provider: MigrationRetirementProvider | None = None
|
||||||
retirement_notes: str | None = None
|
retirement_notes: str | None = None
|
||||||
@@ -215,7 +261,28 @@ class ResourceAclProvider(Protocol):
|
|||||||
|
|
||||||
|
|
||||||
TenantSummaryProvider = Callable[[object, str], Mapping[str, int]]
|
TenantSummaryProvider = Callable[[object, str], Mapping[str, int]]
|
||||||
DeleteVetoProvider = Callable[[object, str, str], None]
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DeleteVetoIssue:
|
||||||
|
severity: Literal["blocker", "warning", "info"]
|
||||||
|
code: str
|
||||||
|
message: str
|
||||||
|
module_id: str | None = None
|
||||||
|
details: Mapping[str, Any] = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
DeleteVetoProviderResult = DeleteVetoIssue | Iterable[DeleteVetoIssue] | None
|
||||||
|
DeleteVetoProvider = Callable[[object, str, str], DeleteVetoProviderResult]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class DeleteVetoProviderRegistration:
|
||||||
|
module_id: str
|
||||||
|
resource_type: str
|
||||||
|
provider: DeleteVetoProvider
|
||||||
|
|
||||||
|
|
||||||
RouteFactory = Callable[[ModuleContext], "APIRouter"]
|
RouteFactory = Callable[[ModuleContext], "APIRouter"]
|
||||||
CapabilityFactory = Callable[[ModuleContext], object]
|
CapabilityFactory = Callable[[ModuleContext], object]
|
||||||
DocumentationProvider = Callable[[DocumentationContext], Iterable[DocumentationTopic]]
|
DocumentationProvider = Callable[[DocumentationContext], Iterable[DocumentationTopic]]
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
from collections.abc import Iterable, Mapping
|
from collections.abc import Iterable, Mapping
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass, replace
|
||||||
|
|
||||||
from govoplan_core.core.modules import (
|
from govoplan_core.core.modules import (
|
||||||
CapabilityFactory,
|
CapabilityFactory,
|
||||||
|
DeleteVetoIssue,
|
||||||
DeleteVetoProvider,
|
DeleteVetoProvider,
|
||||||
|
DeleteVetoProviderRegistration,
|
||||||
ModuleInterfaceProvider,
|
ModuleInterfaceProvider,
|
||||||
ModuleInterfaceRequirement,
|
ModuleInterfaceRequirement,
|
||||||
ModuleContext,
|
ModuleContext,
|
||||||
@@ -45,7 +47,7 @@ class PlatformRegistry:
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._manifests: dict[str, ModuleManifest] = {}
|
self._manifests: dict[str, ModuleManifest] = {}
|
||||||
self._tenant_summary_providers: dict[str, TenantSummaryProvider] = {}
|
self._tenant_summary_providers: dict[str, TenantSummaryProvider] = {}
|
||||||
self._delete_veto_providers: dict[str, list[DeleteVetoProvider]] = defaultdict(list)
|
self._delete_veto_providers: dict[str, list[DeleteVetoProviderRegistration]] = defaultdict(list)
|
||||||
self._capability_factories: dict[str, CapabilityFactory] = {}
|
self._capability_factories: dict[str, CapabilityFactory] = {}
|
||||||
self._capabilities: dict[str, object] = {}
|
self._capabilities: dict[str, object] = {}
|
||||||
self._capability_context: ModuleContext | None = None
|
self._capability_context: ModuleContext | None = None
|
||||||
@@ -58,7 +60,7 @@ class PlatformRegistry:
|
|||||||
self.register_tenant_summary_provider(manifest.id, provider)
|
self.register_tenant_summary_provider(manifest.id, provider)
|
||||||
for resource_type, providers in manifest.delete_veto_providers.items():
|
for resource_type, providers in manifest.delete_veto_providers.items():
|
||||||
for provider in providers:
|
for provider in providers:
|
||||||
self.register_delete_veto(resource_type, provider)
|
self.register_delete_veto(manifest.id, resource_type, provider)
|
||||||
for name, factory in manifest.capability_factories.items():
|
for name, factory in manifest.capability_factories.items():
|
||||||
self.register_capability_factory(manifest.id, name, factory)
|
self.register_capability_factory(manifest.id, name, factory)
|
||||||
return manifest
|
return manifest
|
||||||
@@ -148,12 +150,39 @@ class PlatformRegistry:
|
|||||||
def tenant_summary_providers(self) -> Mapping[str, TenantSummaryProvider]:
|
def tenant_summary_providers(self) -> Mapping[str, TenantSummaryProvider]:
|
||||||
return dict(self._tenant_summary_providers)
|
return dict(self._tenant_summary_providers)
|
||||||
|
|
||||||
def register_delete_veto(self, resource_type: str, provider: DeleteVetoProvider) -> None:
|
def register_delete_veto(self, module_id: str, resource_type: str, provider: DeleteVetoProvider) -> None:
|
||||||
self._delete_veto_providers[resource_type].append(provider)
|
self._delete_veto_providers[resource_type].append(DeleteVetoProviderRegistration(
|
||||||
|
module_id=module_id,
|
||||||
|
resource_type=resource_type,
|
||||||
|
provider=provider,
|
||||||
|
))
|
||||||
|
|
||||||
def delete_veto_providers(self, resource_type: str) -> tuple[DeleteVetoProvider, ...]:
|
def delete_veto_providers(self, resource_type: str) -> tuple[DeleteVetoProvider, ...]:
|
||||||
|
return tuple(registration.provider for registration in self._delete_veto_providers.get(resource_type, ()))
|
||||||
|
|
||||||
|
def delete_veto_provider_registrations(self, resource_type: str) -> tuple[DeleteVetoProviderRegistration, ...]:
|
||||||
return tuple(self._delete_veto_providers.get(resource_type, ()))
|
return tuple(self._delete_veto_providers.get(resource_type, ()))
|
||||||
|
|
||||||
|
def collect_delete_veto_issues(self, resource_type: str, session: object, tenant_id: str, resource_id: str) -> tuple[DeleteVetoIssue, ...]:
|
||||||
|
issues: list[DeleteVetoIssue] = []
|
||||||
|
for registration in self.delete_veto_provider_registrations(resource_type):
|
||||||
|
try:
|
||||||
|
result = registration.provider(session, tenant_id, resource_id)
|
||||||
|
except Exception as exc:
|
||||||
|
issues.append(DeleteVetoIssue(
|
||||||
|
severity="blocker",
|
||||||
|
code="module_veto",
|
||||||
|
message=str(exc),
|
||||||
|
module_id=registration.module_id,
|
||||||
|
details={
|
||||||
|
"resource_type": resource_type,
|
||||||
|
"resource_id": resource_id,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
continue
|
||||||
|
issues.extend(_normalize_delete_veto_result(result, registration=registration, resource_id=resource_id))
|
||||||
|
return tuple(issues)
|
||||||
|
|
||||||
def validate(self) -> RegistrySnapshot:
|
def validate(self) -> RegistrySnapshot:
|
||||||
ordered = tuple(self._topologically_sorted())
|
ordered = tuple(self._topologically_sorted())
|
||||||
available_capabilities = set(self._capability_factories)
|
available_capabilities = set(self._capability_factories)
|
||||||
@@ -224,6 +253,52 @@ class PlatformRegistry:
|
|||||||
return (self._manifests[module_id] for module_id in ordered)
|
return (self._manifests[module_id] for module_id in ordered)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_delete_veto_result(
|
||||||
|
result: object,
|
||||||
|
*,
|
||||||
|
registration: DeleteVetoProviderRegistration,
|
||||||
|
resource_id: str,
|
||||||
|
) -> tuple[DeleteVetoIssue, ...]:
|
||||||
|
if result is None:
|
||||||
|
return ()
|
||||||
|
if isinstance(result, DeleteVetoIssue):
|
||||||
|
return (_attribute_delete_veto_issue(result, registration=registration, resource_id=resource_id),)
|
||||||
|
if isinstance(result, str):
|
||||||
|
return (DeleteVetoIssue(
|
||||||
|
severity="blocker",
|
||||||
|
code="module_veto",
|
||||||
|
message=result,
|
||||||
|
module_id=registration.module_id,
|
||||||
|
details={
|
||||||
|
"resource_type": registration.resource_type,
|
||||||
|
"resource_id": resource_id,
|
||||||
|
},
|
||||||
|
),)
|
||||||
|
if isinstance(result, Iterable):
|
||||||
|
return tuple(
|
||||||
|
_attribute_delete_veto_issue(issue, registration=registration, resource_id=resource_id)
|
||||||
|
for issue in result
|
||||||
|
if isinstance(issue, DeleteVetoIssue)
|
||||||
|
)
|
||||||
|
return ()
|
||||||
|
|
||||||
|
|
||||||
|
def _attribute_delete_veto_issue(
|
||||||
|
issue: DeleteVetoIssue,
|
||||||
|
*,
|
||||||
|
registration: DeleteVetoProviderRegistration,
|
||||||
|
resource_id: str,
|
||||||
|
) -> DeleteVetoIssue:
|
||||||
|
details = {
|
||||||
|
"resource_type": registration.resource_type,
|
||||||
|
"resource_id": resource_id,
|
||||||
|
**dict(issue.details),
|
||||||
|
}
|
||||||
|
if issue.module_id == registration.module_id and issue.details == details:
|
||||||
|
return issue
|
||||||
|
return replace(issue, module_id=issue.module_id or registration.module_id, details=details)
|
||||||
|
|
||||||
|
|
||||||
def _validate_manifest_shape(manifest: ModuleManifest) -> None:
|
def _validate_manifest_shape(manifest: ModuleManifest) -> None:
|
||||||
if not _MODULE_ID_RE.match(manifest.id):
|
if not _MODULE_ID_RE.match(manifest.id):
|
||||||
raise RegistryError(f"Module manifest id must match {_MODULE_ID_RE.pattern}: {manifest.id!r}")
|
raise RegistryError(f"Module manifest id must match {_MODULE_ID_RE.pattern}: {manifest.id!r}")
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from alembic import command
|
from alembic import command
|
||||||
from alembic.config import Config
|
from alembic.config import Config
|
||||||
from alembic.runtime.migration import MigrationContext
|
from alembic.runtime.migration import MigrationContext
|
||||||
|
from alembic.script import ScriptDirectory
|
||||||
from sqlalchemy import create_engine, inspect, text
|
from sqlalchemy import create_engine, inspect, text
|
||||||
|
|
||||||
from govoplan_core.core.migrations import MigrationMetadataPlan, migration_metadata_plan
|
from govoplan_core.core.migrations import MigrationMetadataPlan, migration_metadata_plan
|
||||||
from govoplan_core.core import change_sequence as core_change_sequence_models # noqa: F401 - populate core metadata
|
from govoplan_core.core import change_sequence as core_change_sequence_models # noqa: F401 - populate core metadata
|
||||||
from govoplan_core.core.change_sequence import ChangeSequenceEntry, ChangeSequenceRetentionFloor
|
from govoplan_core.core.change_sequence import ChangeSequenceEntry, ChangeSequenceRetentionFloor
|
||||||
from govoplan_core.core.module_management import load_startup_enabled_modules, startup_candidate_module_ids
|
from govoplan_core.core.module_management import load_startup_enabled_modules, startup_candidate_module_ids
|
||||||
from govoplan_core.db.session import configure_database
|
from govoplan_core.core.modules import ModuleMigrationTaskContext, ModuleMigrationTaskResult
|
||||||
|
from govoplan_core.db.session import configure_database, get_database
|
||||||
from govoplan_core.server.default_config import get_server_config
|
from govoplan_core.server.default_config import get_server_config
|
||||||
from govoplan_core.server.config import ManifestFactory
|
from govoplan_core.server.config import ManifestFactory
|
||||||
from govoplan_core.server.registry import available_module_manifests, build_platform_registry
|
from govoplan_core.server.registry import available_module_manifests, build_platform_registry
|
||||||
@@ -33,6 +38,9 @@ REVISION_CORE_CHANGE_SEQUENCE = "3f4a5b6c7d8e"
|
|||||||
REVISION_HIERARCHICAL_SETTINGS = "f5a6b7c8d9e0"
|
REVISION_HIERARCHICAL_SETTINGS = "f5a6b7c8d9e0"
|
||||||
LEGACY_SCOPE_TABLE = "tenancy_tenants"
|
LEGACY_SCOPE_TABLE = "tenancy_tenants"
|
||||||
CORE_SCOPE_TABLE = "core_scopes"
|
CORE_SCOPE_TABLE = "core_scopes"
|
||||||
|
PRE_MIGRATION_TASK_PHASES = ("pre_migration_check", "pre_migration_prepare")
|
||||||
|
POST_MIGRATION_TASK_PHASES = ("post_migration_backfill", "post_migration_verify")
|
||||||
|
MIGRATION_TASK_PHASES = (*PRE_MIGRATION_TASK_PHASES, *POST_MIGRATION_TASK_PHASES)
|
||||||
|
|
||||||
_NAMESPACE_TABLE_RENAMES = (
|
_NAMESPACE_TABLE_RENAMES = (
|
||||||
("tenants", "tenancy_tenants"),
|
("tenants", "tenancy_tenants"),
|
||||||
@@ -160,12 +168,31 @@ class MigrationResult:
|
|||||||
current_revision: str | None
|
current_revision: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class ModuleMigrationTaskExecutionError(RuntimeError):
|
||||||
|
def __init__(self, message: str, *, records: tuple[dict[str, object], ...]) -> None:
|
||||||
|
super().__init__(message)
|
||||||
|
self.records = records
|
||||||
|
|
||||||
|
|
||||||
def registered_module_migration_plan(
|
def registered_module_migration_plan(
|
||||||
database_url: str | None = None,
|
database_url: str | None = None,
|
||||||
*,
|
*,
|
||||||
enabled_modules: tuple[str, ...] | list[str] | None = None,
|
enabled_modules: tuple[str, ...] | list[str] | None = None,
|
||||||
manifest_factories: tuple[ManifestFactory, ...] = (),
|
manifest_factories: tuple[ManifestFactory, ...] = (),
|
||||||
) -> MigrationMetadataPlan:
|
) -> MigrationMetadataPlan:
|
||||||
|
return migration_metadata_plan(_registered_module_registry(
|
||||||
|
database_url=database_url,
|
||||||
|
enabled_modules=enabled_modules,
|
||||||
|
manifest_factories=manifest_factories,
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
def _registered_module_registry(
|
||||||
|
*,
|
||||||
|
database_url: str | None = None,
|
||||||
|
enabled_modules: tuple[str, ...] | list[str] | None = None,
|
||||||
|
manifest_factories: tuple[ManifestFactory, ...] = (),
|
||||||
|
):
|
||||||
server_config = get_server_config()
|
server_config = get_server_config()
|
||||||
active_database_url = database_url or settings.database_url
|
active_database_url = database_url or settings.database_url
|
||||||
if active_database_url:
|
if active_database_url:
|
||||||
@@ -187,7 +214,130 @@ def registered_module_migration_plan(
|
|||||||
active_enabled_modules,
|
active_enabled_modules,
|
||||||
manifest_factories=active_manifest_factories,
|
manifest_factories=active_manifest_factories,
|
||||||
)
|
)
|
||||||
return migration_metadata_plan(registry)
|
return registry
|
||||||
|
|
||||||
|
|
||||||
|
def run_registered_module_migration_tasks(
|
||||||
|
*,
|
||||||
|
database_url: str | None = None,
|
||||||
|
enabled_modules: tuple[str, ...] | list[str] | None = None,
|
||||||
|
migration_module_order: tuple[str, ...] | list[str] | None = None,
|
||||||
|
phases: tuple[str, ...] | list[str] = MIGRATION_TASK_PHASES,
|
||||||
|
dry_run: bool = False,
|
||||||
|
manifest_factories: tuple[ManifestFactory, ...] = (),
|
||||||
|
) -> tuple[dict[str, object], ...]:
|
||||||
|
active_phases = tuple(dict.fromkeys(str(item).strip() for item in phases if str(item).strip()))
|
||||||
|
invalid_phases = tuple(phase for phase in active_phases if phase not in MIGRATION_TASK_PHASES)
|
||||||
|
if invalid_phases:
|
||||||
|
raise ValueError("Unsupported module migration task phase(s): " + ", ".join(invalid_phases))
|
||||||
|
url = database_url or settings.database_url
|
||||||
|
registry = _registered_module_registry(
|
||||||
|
database_url=url,
|
||||||
|
enabled_modules=enabled_modules,
|
||||||
|
manifest_factories=manifest_factories,
|
||||||
|
)
|
||||||
|
manifests = {manifest.id: manifest for manifest in registry.manifests()}
|
||||||
|
ordered_ids = tuple(dict.fromkeys([
|
||||||
|
*(str(item).strip() for item in (migration_module_order or ()) if str(item).strip()),
|
||||||
|
*manifests.keys(),
|
||||||
|
]))
|
||||||
|
records: list[dict[str, object]] = []
|
||||||
|
database = get_database()
|
||||||
|
with database.SessionLocal() as session:
|
||||||
|
for phase in active_phases:
|
||||||
|
for module_id in ordered_ids:
|
||||||
|
manifest = manifests.get(module_id)
|
||||||
|
if manifest is None or manifest.migration_spec is None:
|
||||||
|
continue
|
||||||
|
for task in manifest.migration_spec.migration_tasks:
|
||||||
|
if task.phase != phase:
|
||||||
|
continue
|
||||||
|
record: dict[str, object] = {
|
||||||
|
"module_id": manifest.id,
|
||||||
|
"task_id": task.task_id,
|
||||||
|
"phase": task.phase,
|
||||||
|
"summary": task.summary,
|
||||||
|
"task_version": task.task_version,
|
||||||
|
"safety": task.safety,
|
||||||
|
"idempotent": task.idempotent,
|
||||||
|
"dry_run": dry_run,
|
||||||
|
}
|
||||||
|
if task.timeout_seconds is not None:
|
||||||
|
record["timeout_seconds"] = task.timeout_seconds
|
||||||
|
if not task.idempotent:
|
||||||
|
record.update({"status": "blocked", "message": "Task is not idempotent."})
|
||||||
|
records.append(record)
|
||||||
|
raise ModuleMigrationTaskExecutionError(
|
||||||
|
f"Module migration task {manifest.id}/{task.task_id} is not idempotent.",
|
||||||
|
records=tuple(records),
|
||||||
|
)
|
||||||
|
if dry_run:
|
||||||
|
record.update({"status": "skipped", "message": "Dry run; executor was not called."})
|
||||||
|
records.append(record)
|
||||||
|
continue
|
||||||
|
if task.executor is None:
|
||||||
|
record.update({"status": "blocked", "message": "Task has no executor."})
|
||||||
|
records.append(record)
|
||||||
|
raise ModuleMigrationTaskExecutionError(
|
||||||
|
f"Module migration task {manifest.id}/{task.task_id} has no executor.",
|
||||||
|
records=tuple(records),
|
||||||
|
)
|
||||||
|
context = ModuleMigrationTaskContext(
|
||||||
|
module_id=manifest.id,
|
||||||
|
task_id=task.task_id,
|
||||||
|
phase=task.phase,
|
||||||
|
database_url=url,
|
||||||
|
target_version=manifest.version,
|
||||||
|
session=session,
|
||||||
|
dry_run=dry_run,
|
||||||
|
metadata=task.metadata,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
result = task.executor(context)
|
||||||
|
normalized = _normalize_migration_task_result(result)
|
||||||
|
except Exception as exc:
|
||||||
|
session.rollback()
|
||||||
|
record.update({
|
||||||
|
"status": "blocked",
|
||||||
|
"message": f"{type(exc).__name__}: {exc}",
|
||||||
|
})
|
||||||
|
records.append(record)
|
||||||
|
raise ModuleMigrationTaskExecutionError(
|
||||||
|
f"Module migration task {manifest.id}/{task.task_id} failed.",
|
||||||
|
records=tuple(records),
|
||||||
|
) from exc
|
||||||
|
record.update({
|
||||||
|
"status": normalized.status,
|
||||||
|
"message": normalized.message,
|
||||||
|
"details": _jsonable_migration_task_details(normalized.details),
|
||||||
|
})
|
||||||
|
records.append(record)
|
||||||
|
if normalized.status == "blocked":
|
||||||
|
session.rollback()
|
||||||
|
raise ModuleMigrationTaskExecutionError(
|
||||||
|
normalized.message or f"Module migration task {manifest.id}/{task.task_id} blocked migration.",
|
||||||
|
records=tuple(records),
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
return tuple(records)
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_migration_task_result(result: ModuleMigrationTaskResult | None) -> ModuleMigrationTaskResult:
|
||||||
|
if result is None:
|
||||||
|
return ModuleMigrationTaskResult()
|
||||||
|
if not isinstance(result, ModuleMigrationTaskResult):
|
||||||
|
raise TypeError("Module migration task executors must return ModuleMigrationTaskResult or None.")
|
||||||
|
if result.status not in {"ok", "warning", "blocked", "skipped"}:
|
||||||
|
raise ValueError(f"Unsupported module migration task status: {result.status!r}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _jsonable_migration_task_details(value: Mapping[str, Any]) -> Mapping[str, Any] | str:
|
||||||
|
try:
|
||||||
|
json.dumps(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return str(dict(value))
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
def _repo_root() -> Path:
|
def _repo_root() -> Path:
|
||||||
@@ -518,6 +668,7 @@ def migrate_database(
|
|||||||
database_url: str | None = None,
|
database_url: str | None = None,
|
||||||
reconcile_legacy_schema: bool = True,
|
reconcile_legacy_schema: bool = True,
|
||||||
enabled_modules: tuple[str, ...] | list[str] | None = None,
|
enabled_modules: tuple[str, ...] | list[str] | None = None,
|
||||||
|
migration_module_order: tuple[str, ...] | list[str] | None = None,
|
||||||
manifest_factories: tuple[ManifestFactory, ...] = (),
|
manifest_factories: tuple[ManifestFactory, ...] = (),
|
||||||
) -> MigrationResult:
|
) -> MigrationResult:
|
||||||
url = database_url or settings.database_url
|
url = database_url or settings.database_url
|
||||||
@@ -526,14 +677,19 @@ def migrate_database(
|
|||||||
reconcile_change_sequence_retention_floor_drift(url)
|
reconcile_change_sequence_retention_floor_drift(url)
|
||||||
previous = database_revision(url)
|
previous = database_revision(url)
|
||||||
reconciled = reconcile_legacy_create_all_schema(url) if reconcile_legacy_schema else None
|
reconciled = reconcile_legacy_create_all_schema(url) if reconcile_legacy_schema else None
|
||||||
command.upgrade(
|
config = alembic_config(
|
||||||
alembic_config(
|
database_url=url,
|
||||||
database_url=url,
|
enabled_modules=enabled_modules,
|
||||||
|
manifest_factories=manifest_factories,
|
||||||
|
)
|
||||||
|
if migration_module_order:
|
||||||
|
plan = registered_module_migration_plan(
|
||||||
|
url,
|
||||||
enabled_modules=enabled_modules,
|
enabled_modules=enabled_modules,
|
||||||
manifest_factories=manifest_factories,
|
manifest_factories=manifest_factories,
|
||||||
),
|
)
|
||||||
"heads",
|
_upgrade_ordered_module_heads(config, plan, tuple(migration_module_order))
|
||||||
)
|
command.upgrade(config, "heads")
|
||||||
if reconcile_legacy_schema:
|
if reconcile_legacy_schema:
|
||||||
reconcile_scope_table_retirement_drift(url)
|
reconcile_scope_table_retirement_drift(url)
|
||||||
current = database_revision(url)
|
current = database_revision(url)
|
||||||
@@ -542,3 +698,43 @@ def migrate_database(
|
|||||||
reconciled_revision=reconciled,
|
reconciled_revision=reconciled,
|
||||||
current_revision=current,
|
current_revision=current,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _upgrade_ordered_module_heads(
|
||||||
|
config: Config,
|
||||||
|
plan: MigrationMetadataPlan,
|
||||||
|
module_order: tuple[str, ...],
|
||||||
|
) -> None:
|
||||||
|
module_heads = _module_heads_by_id(config, plan)
|
||||||
|
upgraded: set[str] = set()
|
||||||
|
for module_id in dict.fromkeys(module_order):
|
||||||
|
for head in module_heads.get(module_id, ()):
|
||||||
|
if head in upgraded:
|
||||||
|
continue
|
||||||
|
command.upgrade(config, head)
|
||||||
|
upgraded.add(head)
|
||||||
|
|
||||||
|
|
||||||
|
def _module_heads_by_id(config: Config, plan: MigrationMetadataPlan) -> dict[str, tuple[str, ...]]:
|
||||||
|
script = ScriptDirectory.from_config(config)
|
||||||
|
heads = script.get_heads()
|
||||||
|
module_locations = {
|
||||||
|
item.module_id: Path(item.script_location).resolve()
|
||||||
|
for item in plan.modules
|
||||||
|
if item.script_location
|
||||||
|
}
|
||||||
|
grouped: dict[str, list[str]] = {module_id: [] for module_id in module_locations}
|
||||||
|
for head in heads:
|
||||||
|
revision = script.get_revision(head)
|
||||||
|
path_text = getattr(revision, "path", None)
|
||||||
|
if revision is None or not path_text:
|
||||||
|
continue
|
||||||
|
revision_path = Path(path_text).resolve()
|
||||||
|
for module_id, location in module_locations.items():
|
||||||
|
try:
|
||||||
|
revision_path.relative_to(location)
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
grouped[module_id].append(head)
|
||||||
|
break
|
||||||
|
return {module_id: tuple(sorted(values)) for module_id, values in grouped.items() if values}
|
||||||
|
|||||||
57
tests/test_delete_veto_contract.py
Normal file
57
tests/test_delete_veto_contract.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from govoplan_core.core.modules import DeleteVetoIssue, ModuleManifest
|
||||||
|
from govoplan_core.core.registry import PlatformRegistry
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteVetoContractTests(unittest.TestCase):
|
||||||
|
def test_structured_provider_result_is_module_attributed(self) -> None:
|
||||||
|
registry = PlatformRegistry()
|
||||||
|
registry.register(ModuleManifest(
|
||||||
|
id="records",
|
||||||
|
name="Records",
|
||||||
|
version="test",
|
||||||
|
delete_veto_providers={
|
||||||
|
"tenant": (
|
||||||
|
lambda _session, _tenant_id, _resource_id: DeleteVetoIssue(
|
||||||
|
severity="warning",
|
||||||
|
code="records_retained",
|
||||||
|
message="Records will remain archived.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
|
||||||
|
issues = registry.collect_delete_veto_issues("tenant", object(), "tenant-1", "tenant-1")
|
||||||
|
|
||||||
|
self.assertEqual(1, len(issues))
|
||||||
|
self.assertEqual("warning", issues[0].severity)
|
||||||
|
self.assertEqual("records_retained", issues[0].code)
|
||||||
|
self.assertEqual("records", issues[0].module_id)
|
||||||
|
self.assertEqual({"resource_type": "tenant", "resource_id": "tenant-1"}, issues[0].details)
|
||||||
|
|
||||||
|
def test_exception_based_provider_remains_blocking_veto(self) -> None:
|
||||||
|
def veto(_session: object, _tenant_id: str, _resource_id: str) -> None:
|
||||||
|
raise RuntimeError("Tenant owns external records.")
|
||||||
|
|
||||||
|
registry = PlatformRegistry()
|
||||||
|
registry.register(ModuleManifest(
|
||||||
|
id="external_records",
|
||||||
|
name="External Records",
|
||||||
|
version="test",
|
||||||
|
delete_veto_providers={"tenant": (veto,)},
|
||||||
|
))
|
||||||
|
|
||||||
|
issues = registry.collect_delete_veto_issues("tenant", object(), "tenant-1", "tenant-1")
|
||||||
|
|
||||||
|
self.assertEqual(1, len(issues))
|
||||||
|
self.assertEqual("blocker", issues[0].severity)
|
||||||
|
self.assertEqual("module_veto", issues[0].code)
|
||||||
|
self.assertEqual("external_records", issues[0].module_id)
|
||||||
|
self.assertEqual("Tenant owns external records.", issues[0].message)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -57,6 +57,7 @@ from govoplan_core.core.module_installer import (
|
|||||||
list_module_installer_requests,
|
list_module_installer_requests,
|
||||||
module_installer_daemon_status,
|
module_installer_daemon_status,
|
||||||
module_install_preflight,
|
module_install_preflight,
|
||||||
|
module_install_catalog_companion_module_ids,
|
||||||
module_manifest_compatibility_issues,
|
module_manifest_compatibility_issues,
|
||||||
module_installer_lock_status,
|
module_installer_lock_status,
|
||||||
queue_module_installer_request,
|
queue_module_installer_request,
|
||||||
@@ -65,6 +66,7 @@ from govoplan_core.core.module_installer import (
|
|||||||
retry_module_installer_request,
|
retry_module_installer_request,
|
||||||
rollback_module_install_run,
|
rollback_module_install_run,
|
||||||
run_module_install_plan,
|
run_module_install_plan,
|
||||||
|
structured_install_commands,
|
||||||
supervise_module_install_plan,
|
supervise_module_install_plan,
|
||||||
update_module_installer_daemon_status,
|
update_module_installer_daemon_status,
|
||||||
update_module_installer_request,
|
update_module_installer_request,
|
||||||
@@ -95,13 +97,14 @@ from govoplan_core.core.module_package_catalog import (
|
|||||||
sign_module_package_catalog,
|
sign_module_package_catalog,
|
||||||
validate_module_package_catalog,
|
validate_module_package_catalog,
|
||||||
)
|
)
|
||||||
from govoplan_core.core.modules import FrontendModule, FrontendRoute, MigrationRetirementPlan, ModuleCompatibility, ModuleUninstallGuardResult
|
from govoplan_core.core.modules import FrontendModule, FrontendRoute, MigrationRetirementPlan, ModuleCompatibility, ModuleMigrationTask, ModuleMigrationTaskContext, ModuleMigrationTaskResult, ModuleUninstallGuardResult
|
||||||
from govoplan_core.core.module_guards import drop_table_retirement_provider
|
from govoplan_core.core.module_guards import drop_table_retirement_provider
|
||||||
from govoplan_core.core.modules import MigrationSpec, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest
|
from govoplan_core.core.modules import MigrationSpec, ModuleInterfaceProvider, ModuleInterfaceRequirement, ModuleManifest
|
||||||
from govoplan_core.core.registry import PlatformRegistry, RegistryError
|
from govoplan_core.core.registry import PlatformRegistry, RegistryError
|
||||||
from govoplan_core.admin.models import SystemSettings
|
from govoplan_core.admin.models import SystemSettings
|
||||||
from govoplan_core.db.base import Base
|
from govoplan_core.db.base import Base
|
||||||
from govoplan_core.db.bootstrap import bootstrap_dev_data
|
from govoplan_core.db.bootstrap import bootstrap_dev_data
|
||||||
|
from govoplan_core.db.migrations import run_registered_module_migration_tasks
|
||||||
from govoplan_core.db.session import configure_database as configure_database_handle, get_database, reset_database
|
from govoplan_core.db.session import configure_database as configure_database_handle, get_database, reset_database
|
||||||
from govoplan_core.security.module_permissions import scopes_grant_compatible
|
from govoplan_core.security.module_permissions import scopes_grant_compatible
|
||||||
from govoplan_core.security.permissions import scope_grants
|
from govoplan_core.security.permissions import scope_grants
|
||||||
@@ -1228,6 +1231,103 @@ finally:
|
|||||||
self.assertTrue(companion_issues)
|
self.assertTrue(companion_issues)
|
||||||
self.assertTrue(any("files" in issue.message for issue in companion_issues))
|
self.assertTrue(any("files" in issue.message for issue in companion_issues))
|
||||||
|
|
||||||
|
def test_module_installer_suggests_catalog_companion_provider_updates(self) -> None:
|
||||||
|
available = {
|
||||||
|
"files": ModuleManifest(
|
||||||
|
id="files",
|
||||||
|
name="Files",
|
||||||
|
version="1.0.0",
|
||||||
|
provides_interfaces=(ModuleInterfaceProvider(name="files.attachments", version="1.0.0"),),
|
||||||
|
),
|
||||||
|
"campaigns": ModuleManifest(id="campaigns", name="Campaigns", version="1.0.0"),
|
||||||
|
}
|
||||||
|
plan = ModuleInstallPlan(items=(
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="campaigns",
|
||||||
|
action="update",
|
||||||
|
source="catalog",
|
||||||
|
python_package="govoplan-campaign",
|
||||||
|
python_ref="govoplan-campaign==2.0.0",
|
||||||
|
),
|
||||||
|
))
|
||||||
|
validation = {
|
||||||
|
"configured": True,
|
||||||
|
"valid": True,
|
||||||
|
"warnings": [],
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module_id": "files",
|
||||||
|
"provides_interfaces": [{"name": "files.attachments", "version": "2.0.0"}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_id": "campaigns",
|
||||||
|
"requires_interfaces": [{
|
||||||
|
"name": "files.attachments",
|
||||||
|
"version_min": "2.0.0",
|
||||||
|
"version_max_exclusive": "3.0.0",
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
companions = module_install_catalog_companion_module_ids(plan, available, validation=validation)
|
||||||
|
|
||||||
|
self.assertEqual(("files",), companions)
|
||||||
|
|
||||||
|
def test_module_installer_suggests_catalog_companion_consumer_updates(self) -> None:
|
||||||
|
available = {
|
||||||
|
"files": ModuleManifest(
|
||||||
|
id="files",
|
||||||
|
name="Files",
|
||||||
|
version="1.0.0",
|
||||||
|
provides_interfaces=(ModuleInterfaceProvider(name="files.attachments", version="1.0.0"),),
|
||||||
|
),
|
||||||
|
"campaigns": ModuleManifest(
|
||||||
|
id="campaigns",
|
||||||
|
name="Campaigns",
|
||||||
|
version="1.0.0",
|
||||||
|
requires_interfaces=(
|
||||||
|
ModuleInterfaceRequirement(
|
||||||
|
name="files.attachments",
|
||||||
|
version_min="1.0.0",
|
||||||
|
version_max_exclusive="2.0.0",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
plan = ModuleInstallPlan(items=(
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="files",
|
||||||
|
action="update",
|
||||||
|
source="catalog",
|
||||||
|
python_package="govoplan-files",
|
||||||
|
python_ref="govoplan-files==2.0.0",
|
||||||
|
),
|
||||||
|
))
|
||||||
|
validation = {
|
||||||
|
"configured": True,
|
||||||
|
"valid": True,
|
||||||
|
"warnings": [],
|
||||||
|
"modules": [
|
||||||
|
{
|
||||||
|
"module_id": "files",
|
||||||
|
"provides_interfaces": [{"name": "files.attachments", "version": "2.0.0"}],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"module_id": "campaigns",
|
||||||
|
"requires_interfaces": [{
|
||||||
|
"name": "files.attachments",
|
||||||
|
"version_min": "2.0.0",
|
||||||
|
"version_max_exclusive": "3.0.0",
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
companions = module_install_catalog_companion_module_ids(plan, available, validation=validation)
|
||||||
|
|
||||||
|
self.assertEqual(("campaigns",), companions)
|
||||||
|
|
||||||
def test_module_installer_preflight_blocks_unacknowledged_forward_only_catalog_update(self) -> None:
|
def test_module_installer_preflight_blocks_unacknowledged_forward_only_catalog_update(self) -> None:
|
||||||
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
|
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
|
||||||
plan = ModuleInstallPlan(items=(
|
plan = ModuleInstallPlan(items=(
|
||||||
@@ -1251,6 +1351,8 @@ finally:
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"migration_safety": "forward_only",
|
"migration_safety": "forward_only",
|
||||||
"migration_notes": "Database rollback requires restoring the pre-update snapshot.",
|
"migration_notes": "Database rollback requires restoring the pre-update snapshot.",
|
||||||
|
"recovery_tested": True,
|
||||||
|
"recovery_notes": "Snapshot restore was rehearsed on the staging dataset.",
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
):
|
):
|
||||||
@@ -1266,7 +1368,7 @@ finally:
|
|||||||
blockers = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
|
blockers = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
|
||||||
self.assertIn("forward_only_migration_acknowledgement_required", blockers)
|
self.assertIn("forward_only_migration_acknowledgement_required", blockers)
|
||||||
|
|
||||||
def test_module_installer_preflight_allows_acknowledged_forward_only_catalog_update(self) -> None:
|
def test_module_installer_preflight_requires_recovery_validation_for_forward_only_update(self) -> None:
|
||||||
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
|
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
|
||||||
plan = ModuleInstallPlan(items=(
|
plan = ModuleInstallPlan(items=(
|
||||||
ModuleInstallPlanItem(
|
ModuleInstallPlanItem(
|
||||||
@@ -1301,16 +1403,234 @@ finally:
|
|||||||
maintenance_mode=True,
|
maintenance_mode=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.assertFalse(preflight.allowed)
|
||||||
|
blockers = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
|
||||||
|
self.assertIn("recovery_validation_required", blockers)
|
||||||
|
self.assertIn("recovery_notes_required", blockers)
|
||||||
|
|
||||||
|
def test_module_installer_preflight_allows_acknowledged_forward_only_catalog_update(self) -> None:
|
||||||
|
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
|
||||||
|
plan = ModuleInstallPlan(items=(
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="files",
|
||||||
|
action="update",
|
||||||
|
source="catalog",
|
||||||
|
python_package="govoplan-files",
|
||||||
|
python_ref="govoplan-files==2.0.0",
|
||||||
|
data_safety_acknowledged=True,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"govoplan_core.core.module_package_catalog.validate_module_package_catalog",
|
||||||
|
return_value={
|
||||||
|
"configured": True,
|
||||||
|
"valid": True,
|
||||||
|
"warnings": [],
|
||||||
|
"modules": [{
|
||||||
|
"module_id": "files",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"migration_safety": "forward_only",
|
||||||
|
"migration_notes": "Database rollback requires restoring the pre-update snapshot.",
|
||||||
|
"recovery_tested": True,
|
||||||
|
"recovery_notes": "Snapshot restore was rehearsed on the staging dataset.",
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
):
|
||||||
|
preflight = module_install_preflight(
|
||||||
|
plan=plan,
|
||||||
|
available=available,
|
||||||
|
current_enabled=(),
|
||||||
|
desired_enabled=(),
|
||||||
|
maintenance_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
self.assertTrue(preflight.allowed)
|
self.assertTrue(preflight.allowed)
|
||||||
self.assertEqual(("files",), tuple(item.module_id for item in preflight.target_plan))
|
self.assertEqual(("files",), tuple(item.module_id for item in preflight.target_plan))
|
||||||
self.assertEqual("1.0.0", preflight.target_plan[0].current_version)
|
self.assertEqual("1.0.0", preflight.target_plan[0].current_version)
|
||||||
self.assertEqual("2.0.0", preflight.target_plan[0].target_version)
|
self.assertEqual("2.0.0", preflight.target_plan[0].target_version)
|
||||||
self.assertEqual("forward_only", preflight.target_plan[0].migration_safety)
|
self.assertEqual("forward_only", preflight.target_plan[0].migration_safety)
|
||||||
|
self.assertTrue(preflight.target_plan[0].recovery_tested)
|
||||||
|
self.assertEqual("Snapshot restore was rehearsed on the staging dataset.", preflight.target_plan[0].recovery_notes)
|
||||||
self.assertTrue(preflight.target_plan[0].data_safety_acknowledged)
|
self.assertTrue(preflight.target_plan[0].data_safety_acknowledged)
|
||||||
self.assertEqual("2.0.0", preflight.as_dict()["target_plan"][0]["target_version"])
|
self.assertEqual("2.0.0", preflight.as_dict()["target_plan"][0]["target_version"])
|
||||||
warnings = {issue.code for issue in preflight.issues if issue.severity == "warning"}
|
warnings = {issue.code for issue in preflight.issues if issue.severity == "warning"}
|
||||||
self.assertIn("forward_only_migration_acknowledged", warnings)
|
self.assertIn("forward_only_migration_acknowledged", warnings)
|
||||||
|
|
||||||
|
def test_module_installer_preflight_blocks_catalog_downgrade_and_noop_updates_by_default(self) -> None:
|
||||||
|
available = {
|
||||||
|
"files": ModuleManifest(id="files", name="Files", version="1.0.0"),
|
||||||
|
"mail": ModuleManifest(id="mail", name="Mail", version="1.0.0"),
|
||||||
|
}
|
||||||
|
plan = ModuleInstallPlan(items=(
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="files",
|
||||||
|
action="update",
|
||||||
|
source="catalog",
|
||||||
|
python_package="govoplan-files",
|
||||||
|
python_ref="govoplan-files==0.9.0",
|
||||||
|
),
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="mail",
|
||||||
|
action="update",
|
||||||
|
source="catalog",
|
||||||
|
python_package="govoplan-mail",
|
||||||
|
python_ref="govoplan-mail==1.0.0",
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"govoplan_core.core.module_package_catalog.validate_module_package_catalog",
|
||||||
|
return_value={
|
||||||
|
"configured": True,
|
||||||
|
"valid": True,
|
||||||
|
"warnings": [],
|
||||||
|
"modules": [
|
||||||
|
{"module_id": "files", "version": "0.9.0"},
|
||||||
|
{"module_id": "mail", "version": "1.0.0"},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
):
|
||||||
|
preflight = module_install_preflight(
|
||||||
|
plan=plan,
|
||||||
|
available=available,
|
||||||
|
current_enabled=(),
|
||||||
|
desired_enabled=(),
|
||||||
|
maintenance_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(preflight.allowed)
|
||||||
|
blockers = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
|
||||||
|
self.assertIn("catalog_downgrade_blocked", blockers)
|
||||||
|
self.assertIn("catalog_noop_update_blocked", blockers)
|
||||||
|
|
||||||
|
def test_module_installer_preflight_allows_reviewed_downgrade_and_noop_updates(self) -> None:
|
||||||
|
available = {
|
||||||
|
"files": ModuleManifest(id="files", name="Files", version="1.0.0"),
|
||||||
|
"mail": ModuleManifest(id="mail", name="Mail", version="1.0.0"),
|
||||||
|
}
|
||||||
|
plan = ModuleInstallPlan(items=(
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="files",
|
||||||
|
action="update",
|
||||||
|
source="catalog",
|
||||||
|
python_package="govoplan-files",
|
||||||
|
python_ref="govoplan-files==0.9.0",
|
||||||
|
),
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="mail",
|
||||||
|
action="update",
|
||||||
|
source="catalog",
|
||||||
|
python_package="govoplan-mail",
|
||||||
|
python_ref="govoplan-mail==1.0.0",
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"govoplan_core.core.module_package_catalog.validate_module_package_catalog",
|
||||||
|
return_value={
|
||||||
|
"configured": True,
|
||||||
|
"valid": True,
|
||||||
|
"warnings": [],
|
||||||
|
"modules": [
|
||||||
|
{"module_id": "files", "version": "0.9.0", "allow_downgrade": True},
|
||||||
|
{"module_id": "mail", "version": "1.0.0", "allow_same_version": True},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
):
|
||||||
|
preflight = module_install_preflight(
|
||||||
|
plan=plan,
|
||||||
|
available=available,
|
||||||
|
current_enabled=(),
|
||||||
|
desired_enabled=(),
|
||||||
|
maintenance_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(preflight.allowed)
|
||||||
|
blocker_codes = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
|
||||||
|
self.assertNotIn("catalog_downgrade_blocked", blocker_codes)
|
||||||
|
self.assertNotIn("catalog_noop_update_blocked", blocker_codes)
|
||||||
|
|
||||||
|
def test_module_installer_preflight_requires_catalog_update_bridge_when_current_version_is_outside_window(self) -> None:
|
||||||
|
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
|
||||||
|
plan = ModuleInstallPlan(items=(
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="files",
|
||||||
|
action="update",
|
||||||
|
source="catalog",
|
||||||
|
python_package="govoplan-files",
|
||||||
|
python_ref="govoplan-files==3.0.0",
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"govoplan_core.core.module_package_catalog.validate_module_package_catalog",
|
||||||
|
return_value={
|
||||||
|
"configured": True,
|
||||||
|
"valid": True,
|
||||||
|
"warnings": [],
|
||||||
|
"modules": [{
|
||||||
|
"module_id": "files",
|
||||||
|
"version": "3.0.0",
|
||||||
|
"current_version_min": "2.0.0",
|
||||||
|
"current_version_max_exclusive": "3.0.0",
|
||||||
|
"bridge_notes": "Install 2.x bridge before moving to 3.x.",
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
):
|
||||||
|
preflight = module_install_preflight(
|
||||||
|
plan=plan,
|
||||||
|
available=available,
|
||||||
|
current_enabled=(),
|
||||||
|
desired_enabled=(),
|
||||||
|
maintenance_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(preflight.allowed)
|
||||||
|
blockers = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
|
||||||
|
self.assertIn("catalog_update_bridge_required", blockers)
|
||||||
|
self.assertEqual("2.0.0", preflight.target_plan[0].current_version_min)
|
||||||
|
self.assertEqual("3.0.0", preflight.target_plan[0].current_version_max_exclusive)
|
||||||
|
|
||||||
|
def test_module_installer_preflight_reports_catalog_bridge_release_metadata(self) -> None:
|
||||||
|
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
|
||||||
|
plan = ModuleInstallPlan(items=(
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="files",
|
||||||
|
action="update",
|
||||||
|
source="catalog",
|
||||||
|
python_package="govoplan-files",
|
||||||
|
python_ref="govoplan-files==2.0.0",
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"govoplan_core.core.module_package_catalog.validate_module_package_catalog",
|
||||||
|
return_value={
|
||||||
|
"configured": True,
|
||||||
|
"valid": True,
|
||||||
|
"warnings": [],
|
||||||
|
"modules": [{
|
||||||
|
"module_id": "files",
|
||||||
|
"version": "2.0.0",
|
||||||
|
"bridge_release": True,
|
||||||
|
"bridge_notes": "Keeps both 1.x and 2.x attachment interfaces available.",
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
):
|
||||||
|
preflight = module_install_preflight(
|
||||||
|
plan=plan,
|
||||||
|
available=available,
|
||||||
|
current_enabled=(),
|
||||||
|
desired_enabled=(),
|
||||||
|
maintenance_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(preflight.allowed)
|
||||||
|
self.assertIn("catalog_bridge_release", {issue.code for issue in preflight.issues if issue.severity == "info"})
|
||||||
|
self.assertTrue(preflight.target_plan[0].bridge_release)
|
||||||
|
self.assertEqual("Keeps both 1.x and 2.x attachment interfaces available.", preflight.target_plan[0].bridge_notes)
|
||||||
|
|
||||||
def test_module_installer_preflight_requires_destructive_catalog_update_plan(self) -> None:
|
def test_module_installer_preflight_requires_destructive_catalog_update_plan(self) -> None:
|
||||||
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
|
available = {"files": ModuleManifest(id="files", name="Files", version="1.0.0")}
|
||||||
plan = ModuleInstallPlan(items=(
|
plan = ModuleInstallPlan(items=(
|
||||||
@@ -1371,6 +1691,8 @@ finally:
|
|||||||
"module_id": "files",
|
"module_id": "files",
|
||||||
"migration_safety": "destructive",
|
"migration_safety": "destructive",
|
||||||
"migration_notes": "Drops obsolete cache tables after exporting the retained documents.",
|
"migration_notes": "Drops obsolete cache tables after exporting the retained documents.",
|
||||||
|
"recovery_tested": True,
|
||||||
|
"recovery_notes": "Restore and forward-recovery path verified on staging.",
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
):
|
):
|
||||||
@@ -1446,6 +1768,275 @@ finally:
|
|||||||
self.assertTrue(preflight.allowed)
|
self.assertTrue(preflight.allowed)
|
||||||
self.assertNotIn("companion_update_required", {issue.code for issue in preflight.issues})
|
self.assertNotIn("companion_update_required", {issue.code for issue in preflight.issues})
|
||||||
|
|
||||||
|
def test_module_installer_preflight_orders_migration_plan_from_interface_requirements(self) -> None:
|
||||||
|
metadata = MetaData()
|
||||||
|
available = {
|
||||||
|
"files": ModuleManifest(
|
||||||
|
id="files",
|
||||||
|
name="Files",
|
||||||
|
version="1.0.0",
|
||||||
|
provides_interfaces=(ModuleInterfaceProvider(name="files.attachments", version="1.0.0"),),
|
||||||
|
migration_spec=MigrationSpec(module_id="files", metadata=metadata),
|
||||||
|
),
|
||||||
|
"campaigns": ModuleManifest(
|
||||||
|
id="campaigns",
|
||||||
|
name="Campaigns",
|
||||||
|
version="1.0.0",
|
||||||
|
requires_interfaces=(
|
||||||
|
ModuleInterfaceRequirement(
|
||||||
|
name="files.attachments",
|
||||||
|
version_min="1.0.0",
|
||||||
|
version_max_exclusive="2.0.0",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migration_spec=MigrationSpec(module_id="campaigns", metadata=metadata),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
plan = ModuleInstallPlan(items=(
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="campaigns",
|
||||||
|
action="update",
|
||||||
|
python_package="govoplan-campaign",
|
||||||
|
python_ref="govoplan-campaign==2.0.0",
|
||||||
|
),
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="files",
|
||||||
|
action="update",
|
||||||
|
python_package="govoplan-files",
|
||||||
|
python_ref="govoplan-files==2.0.0",
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
preflight = module_install_preflight(
|
||||||
|
plan=plan,
|
||||||
|
available=available,
|
||||||
|
current_enabled=("files", "campaigns"),
|
||||||
|
desired_enabled=("files", "campaigns"),
|
||||||
|
maintenance_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(preflight.allowed)
|
||||||
|
self.assertEqual(("files", "campaigns"), tuple(step.module_id for step in preflight.migration_plan.steps))
|
||||||
|
self.assertEqual(("files", "campaigns"), tuple(preflight.migration_plan.enabled_modules))
|
||||||
|
self.assertEqual(["files", "campaigns"], preflight.as_dict()["migration_plan"]["enabled_modules"])
|
||||||
|
|
||||||
|
def test_module_installer_preflight_blocks_migration_graph_cycles(self) -> None:
|
||||||
|
metadata = MetaData()
|
||||||
|
available = {
|
||||||
|
"files": ModuleManifest(
|
||||||
|
id="files",
|
||||||
|
name="Files",
|
||||||
|
version="1.0.0",
|
||||||
|
migration_spec=MigrationSpec(module_id="files", metadata=metadata, migration_after=("campaigns",)),
|
||||||
|
),
|
||||||
|
"campaigns": ModuleManifest(
|
||||||
|
id="campaigns",
|
||||||
|
name="Campaigns",
|
||||||
|
version="1.0.0",
|
||||||
|
migration_spec=MigrationSpec(module_id="campaigns", metadata=metadata, migration_after=("files",)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
plan = ModuleInstallPlan(items=(
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="campaigns",
|
||||||
|
action="update",
|
||||||
|
python_package="govoplan-campaign",
|
||||||
|
python_ref="govoplan-campaign==2.0.0",
|
||||||
|
),
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="files",
|
||||||
|
action="update",
|
||||||
|
python_package="govoplan-files",
|
||||||
|
python_ref="govoplan-files==2.0.0",
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
preflight = module_install_preflight(
|
||||||
|
plan=plan,
|
||||||
|
available=available,
|
||||||
|
current_enabled=("files", "campaigns"),
|
||||||
|
desired_enabled=("files", "campaigns"),
|
||||||
|
maintenance_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(preflight.allowed)
|
||||||
|
blockers = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
|
||||||
|
self.assertIn("migration_graph_cycle", blockers)
|
||||||
|
|
||||||
|
def test_structured_install_commands_pass_target_modules_and_migration_order(self) -> None:
|
||||||
|
plan = ModuleInstallPlan(items=(
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="files",
|
||||||
|
action="install",
|
||||||
|
python_package="govoplan-files",
|
||||||
|
python_ref="govoplan-files==2.0.0",
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
commands = structured_install_commands(
|
||||||
|
plan,
|
||||||
|
webui_root=None,
|
||||||
|
migrate_database_url="sqlite:///example.db",
|
||||||
|
migration_enabled_modules=("access", "files"),
|
||||||
|
migration_module_order=("files",),
|
||||||
|
migration_task_record_path=Path("/tmp/govoplan-migration-tasks.json"),
|
||||||
|
verify_modules=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
init_db = commands[-1]["argv"]
|
||||||
|
self.assertIn("govoplan_core.commands.init_db", init_db)
|
||||||
|
self.assertIn("--enabled-module", init_db)
|
||||||
|
self.assertIn("access", init_db)
|
||||||
|
self.assertIn("files", init_db)
|
||||||
|
self.assertIn("--migration-module", init_db)
|
||||||
|
self.assertIn("--migration-task-record-output", init_db)
|
||||||
|
self.assertEqual("/tmp/govoplan-migration-tasks.json", commands[-1]["migration_task_record_path"])
|
||||||
|
|
||||||
|
def test_module_installer_preflight_reports_and_blocks_unsafe_migration_tasks(self) -> None:
|
||||||
|
metadata = MetaData()
|
||||||
|
available = {
|
||||||
|
"files": ModuleManifest(
|
||||||
|
id="files",
|
||||||
|
name="Files",
|
||||||
|
version="1.0.0",
|
||||||
|
migration_spec=MigrationSpec(
|
||||||
|
module_id="files",
|
||||||
|
metadata=metadata,
|
||||||
|
migration_tasks=(
|
||||||
|
ModuleMigrationTask(
|
||||||
|
task_id="legacy_path_check",
|
||||||
|
phase="pre_migration_check",
|
||||||
|
summary="Check legacy paths before the schema migration.",
|
||||||
|
executor=lambda _context: ModuleMigrationTaskResult(),
|
||||||
|
),
|
||||||
|
ModuleMigrationTask(
|
||||||
|
task_id="unsafe_backfill",
|
||||||
|
phase="post_migration_backfill",
|
||||||
|
summary="Backfill legacy file spaces.",
|
||||||
|
safety="forward_only",
|
||||||
|
idempotent=False,
|
||||||
|
executor=lambda _context: ModuleMigrationTaskResult(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
plan = ModuleInstallPlan(items=(
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="files",
|
||||||
|
action="update",
|
||||||
|
python_package="govoplan-files",
|
||||||
|
python_ref="govoplan-files==2.0.0",
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
preflight = module_install_preflight(
|
||||||
|
plan=plan,
|
||||||
|
available=available,
|
||||||
|
current_enabled=("files",),
|
||||||
|
desired_enabled=("files",),
|
||||||
|
maintenance_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(preflight.allowed)
|
||||||
|
task_ids = {task.task_id for task in preflight.migration_plan.tasks}
|
||||||
|
self.assertEqual({"legacy_path_check", "unsafe_backfill"}, task_ids)
|
||||||
|
blockers = {issue.code for issue in preflight.issues if issue.severity == "blocker"}
|
||||||
|
self.assertIn("migration_task_not_idempotent", blockers)
|
||||||
|
self.assertIn("migration_task_acknowledgement_required", blockers)
|
||||||
|
|
||||||
|
def test_module_installer_preflight_allows_acknowledged_forward_only_migration_task(self) -> None:
|
||||||
|
metadata = MetaData()
|
||||||
|
available = {
|
||||||
|
"files": ModuleManifest(
|
||||||
|
id="files",
|
||||||
|
name="Files",
|
||||||
|
version="1.0.0",
|
||||||
|
migration_spec=MigrationSpec(
|
||||||
|
module_id="files",
|
||||||
|
metadata=metadata,
|
||||||
|
migration_tasks=(
|
||||||
|
ModuleMigrationTask(
|
||||||
|
task_id="backfill_spaces",
|
||||||
|
phase="post_migration_backfill",
|
||||||
|
summary="Backfill file spaces.",
|
||||||
|
safety="forward_only",
|
||||||
|
executor=lambda _context: ModuleMigrationTaskResult(status="ok"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
plan = ModuleInstallPlan(items=(
|
||||||
|
ModuleInstallPlanItem(
|
||||||
|
module_id="files",
|
||||||
|
action="update",
|
||||||
|
python_package="govoplan-files",
|
||||||
|
python_ref="govoplan-files==2.0.0",
|
||||||
|
data_safety_acknowledged=True,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
|
||||||
|
preflight = module_install_preflight(
|
||||||
|
plan=plan,
|
||||||
|
available=available,
|
||||||
|
current_enabled=("files",),
|
||||||
|
desired_enabled=("files",),
|
||||||
|
maintenance_mode=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(preflight.allowed)
|
||||||
|
self.assertEqual(("backfill_spaces",), tuple(task.task_id for task in preflight.migration_plan.tasks))
|
||||||
|
self.assertIn("tasks", preflight.as_dict()["migration_plan"])
|
||||||
|
|
||||||
|
def test_registered_module_migration_tasks_execute_in_phase_order(self) -> None:
|
||||||
|
root = Path(tempfile.mkdtemp(prefix="govoplan-module-migration-tasks-", dir=_TEST_ROOT))
|
||||||
|
database_url = f"sqlite:///{root / 'tasks.db'}"
|
||||||
|
calls: list[tuple[str, str, bool, str | None]] = []
|
||||||
|
|
||||||
|
def executor(context: ModuleMigrationTaskContext) -> ModuleMigrationTaskResult:
|
||||||
|
calls.append((context.module_id, context.task_id, context.session is not None, context.target_version))
|
||||||
|
return ModuleMigrationTaskResult(status="warning", message=f"ran {context.task_id}", details={"phase": context.phase})
|
||||||
|
|
||||||
|
manifest = ModuleManifest(
|
||||||
|
id="taskmod",
|
||||||
|
name="Task Module",
|
||||||
|
version="2.0.0",
|
||||||
|
migration_spec=MigrationSpec(
|
||||||
|
module_id="taskmod",
|
||||||
|
metadata=MetaData(),
|
||||||
|
migration_tasks=(
|
||||||
|
ModuleMigrationTask(
|
||||||
|
task_id="verify",
|
||||||
|
phase="post_migration_verify",
|
||||||
|
summary="Verify migrated state.",
|
||||||
|
executor=executor,
|
||||||
|
),
|
||||||
|
ModuleMigrationTask(
|
||||||
|
task_id="check",
|
||||||
|
phase="pre_migration_check",
|
||||||
|
summary="Check source state.",
|
||||||
|
executor=executor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
records = run_registered_module_migration_tasks(
|
||||||
|
database_url=database_url,
|
||||||
|
enabled_modules=("taskmod",),
|
||||||
|
migration_module_order=("taskmod",),
|
||||||
|
phases=("pre_migration_check", "post_migration_verify"),
|
||||||
|
manifest_factories=(lambda: manifest,),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[("taskmod", "check", True, "2.0.0"), ("taskmod", "verify", True, "2.0.0")],
|
||||||
|
calls,
|
||||||
|
)
|
||||||
|
self.assertEqual(["check", "verify"], [record["task_id"] for record in records])
|
||||||
|
self.assertEqual(["warning", "warning"], [record["status"] for record in records])
|
||||||
|
|
||||||
def test_module_installer_preflight_blocks_provider_update_that_breaks_installed_consumer(self) -> None:
|
def test_module_installer_preflight_blocks_provider_update_that_breaks_installed_consumer(self) -> None:
|
||||||
available = {
|
available = {
|
||||||
"files": ModuleManifest(
|
"files": ModuleManifest(
|
||||||
@@ -2155,6 +2746,25 @@ finally:
|
|||||||
"optional_dependencies": ["mail"],
|
"optional_dependencies": ["mail"],
|
||||||
"migration_safety": "forward_only",
|
"migration_safety": "forward_only",
|
||||||
"migration_notes": "Requires a tested backup restore path.",
|
"migration_notes": "Requires a tested backup restore path.",
|
||||||
|
"migration_after": ["access"],
|
||||||
|
"migration_before": ["campaigns"],
|
||||||
|
"migration_tasks": [{
|
||||||
|
"task_id": "backfill_spaces",
|
||||||
|
"phase": "post_migration_backfill",
|
||||||
|
"summary": "Backfill file spaces after schema migration.",
|
||||||
|
"task_version": "2",
|
||||||
|
"safety": "forward_only",
|
||||||
|
"idempotent": True,
|
||||||
|
"timeout_seconds": 30,
|
||||||
|
}],
|
||||||
|
"current_version_min": "0.1.0",
|
||||||
|
"current_version_max_exclusive": "0.2.0",
|
||||||
|
"bridge_release": True,
|
||||||
|
"bridge_notes": "Keeps 0.1.x and 0.2.x file-space contracts available.",
|
||||||
|
"allow_downgrade": True,
|
||||||
|
"allow_same_version": True,
|
||||||
|
"recovery_tested": True,
|
||||||
|
"recovery_notes": "Restore rehearsal completed for the release candidate.",
|
||||||
"provides_interfaces": [{"name": "files.spaces", "version": "1.2.0"}],
|
"provides_interfaces": [{"name": "files.spaces", "version": "1.2.0"}],
|
||||||
"requires_interfaces": [{"name": "access.directory", "version_min": "1.0.0", "optional": True}],
|
"requires_interfaces": [{"name": "access.directory", "version_min": "1.0.0", "optional": True}],
|
||||||
"artifact_integrity": {
|
"artifact_integrity": {
|
||||||
@@ -2177,6 +2787,25 @@ finally:
|
|||||||
self.assertEqual(["mail"], catalog[0]["optional_dependencies"])
|
self.assertEqual(["mail"], catalog[0]["optional_dependencies"])
|
||||||
self.assertEqual("forward_only", catalog[0]["migration_safety"])
|
self.assertEqual("forward_only", catalog[0]["migration_safety"])
|
||||||
self.assertEqual("Requires a tested backup restore path.", catalog[0]["migration_notes"])
|
self.assertEqual("Requires a tested backup restore path.", catalog[0]["migration_notes"])
|
||||||
|
self.assertEqual(["access"], catalog[0]["migration_after"])
|
||||||
|
self.assertEqual(["campaigns"], catalog[0]["migration_before"])
|
||||||
|
self.assertEqual([{
|
||||||
|
"task_id": "backfill_spaces",
|
||||||
|
"phase": "post_migration_backfill",
|
||||||
|
"summary": "Backfill file spaces after schema migration.",
|
||||||
|
"task_version": "2",
|
||||||
|
"safety": "forward_only",
|
||||||
|
"idempotent": True,
|
||||||
|
"timeout_seconds": 30,
|
||||||
|
}], catalog[0]["migration_tasks"])
|
||||||
|
self.assertEqual("0.1.0", catalog[0]["current_version_min"])
|
||||||
|
self.assertEqual("0.2.0", catalog[0]["current_version_max_exclusive"])
|
||||||
|
self.assertTrue(catalog[0]["bridge_release"])
|
||||||
|
self.assertEqual("Keeps 0.1.x and 0.2.x file-space contracts available.", catalog[0]["bridge_notes"])
|
||||||
|
self.assertTrue(catalog[0]["allow_downgrade"])
|
||||||
|
self.assertTrue(catalog[0]["allow_same_version"])
|
||||||
|
self.assertTrue(catalog[0]["recovery_tested"])
|
||||||
|
self.assertEqual("Restore rehearsal completed for the release candidate.", catalog[0]["recovery_notes"])
|
||||||
self.assertEqual(["official"], catalog[0]["tags"])
|
self.assertEqual(["official"], catalog[0]["tags"])
|
||||||
self.assertEqual([{"name": "files.spaces", "version": "1.2.0"}], catalog[0]["provides_interfaces"])
|
self.assertEqual([{"name": "files.spaces", "version": "1.2.0"}], catalog[0]["provides_interfaces"])
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -2245,6 +2874,22 @@ finally:
|
|||||||
self.assertFalse(validation["valid"])
|
self.assertFalse(validation["valid"])
|
||||||
self.assertIn("migration_safety", str(validation["error"]))
|
self.assertIn("migration_safety", str(validation["error"]))
|
||||||
|
|
||||||
|
def test_module_package_catalog_rejects_invalid_current_version_window(self) -> None:
|
||||||
|
root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-invalid-current-window-", dir=_TEST_ROOT))
|
||||||
|
catalog_path = root / "catalog.json"
|
||||||
|
catalog_path.write_text(json.dumps({
|
||||||
|
"modules": [{
|
||||||
|
"module_id": "files",
|
||||||
|
"current_version_min": "2.0.0",
|
||||||
|
"current_version_max_exclusive": "2.0.0",
|
||||||
|
}],
|
||||||
|
}), encoding="utf-8")
|
||||||
|
|
||||||
|
validation = validate_module_package_catalog(catalog_path)
|
||||||
|
|
||||||
|
self.assertFalse(validation["valid"])
|
||||||
|
self.assertIn("current-version range", str(validation["error"]))
|
||||||
|
|
||||||
def test_module_package_catalog_rejects_invalid_interface_ranges(self) -> None:
|
def test_module_package_catalog_rejects_invalid_interface_ranges(self) -> None:
|
||||||
root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-invalid-interface-", dir=_TEST_ROOT))
|
root = Path(tempfile.mkdtemp(prefix="govoplan-module-package-catalog-invalid-interface-", dir=_TEST_ROOT))
|
||||||
catalog_path = root / "catalog.json"
|
catalog_path = root / "catalog.json"
|
||||||
@@ -3158,9 +3803,11 @@ finally:
|
|||||||
self.assertEqual(1, len(core_plan.script_locations))
|
self.assertEqual(1, len(core_plan.script_locations))
|
||||||
self.assertTrue(core_plan.script_locations[0].endswith("govoplan_access/backend/migrations/versions"))
|
self.assertTrue(core_plan.script_locations[0].endswith("govoplan_access/backend/migrations/versions"))
|
||||||
self.assertEqual(1, len(core_plan.metadata))
|
self.assertEqual(1, len(core_plan.metadata))
|
||||||
|
self.assertEqual(("access",), tuple(item.module_id for item in core_plan.modules))
|
||||||
|
|
||||||
files_plan = migration_metadata_plan(build_platform_registry(("files",)))
|
files_plan = migration_metadata_plan(build_platform_registry(("files",)))
|
||||||
self.assertEqual(2, len(files_plan.script_locations))
|
self.assertEqual(2, len(files_plan.script_locations))
|
||||||
|
self.assertIn("files", {item.module_id for item in files_plan.modules})
|
||||||
files_locations = "\n".join(files_plan.script_locations)
|
files_locations = "\n".join(files_plan.script_locations)
|
||||||
self.assertIn("govoplan_access/backend/migrations/versions", files_locations)
|
self.assertIn("govoplan_access/backend/migrations/versions", files_locations)
|
||||||
self.assertIn("govoplan_files/backend/migrations/versions", files_locations)
|
self.assertIn("govoplan_files/backend/migrations/versions", files_locations)
|
||||||
|
|||||||
94
webui/package-lock.json
generated
94
webui/package-lock.json
generated
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||||
"@govoplan/admin-webui": "file:../../govoplan-admin/webui",
|
"@govoplan/admin-webui": "file:../../govoplan-admin/webui",
|
||||||
|
"@govoplan/audit-webui": "file:../../govoplan-audit/webui",
|
||||||
"@govoplan/calendar-webui": "file:../../govoplan-calendar/webui",
|
"@govoplan/calendar-webui": "file:../../govoplan-calendar/webui",
|
||||||
"@govoplan/campaign-webui": "file:../../govoplan-campaign/webui",
|
"@govoplan/campaign-webui": "file:../../govoplan-campaign/webui",
|
||||||
"@govoplan/dashboard-webui": "file:../../govoplan-dashboard/webui",
|
"@govoplan/dashboard-webui": "file:../../govoplan-dashboard/webui",
|
||||||
@@ -18,7 +19,8 @@
|
|||||||
"@govoplan/idm-webui": "file:../../govoplan-idm/webui",
|
"@govoplan/idm-webui": "file:../../govoplan-idm/webui",
|
||||||
"@govoplan/mail-webui": "file:../../govoplan-mail/webui",
|
"@govoplan/mail-webui": "file:../../govoplan-mail/webui",
|
||||||
"@govoplan/ops-webui": "file:../../govoplan-ops/webui",
|
"@govoplan/ops-webui": "file:../../govoplan-ops/webui",
|
||||||
"@govoplan/organizations-webui": "file:../../govoplan-organizations/webui"
|
"@govoplan/organizations-webui": "file:../../govoplan-organizations/webui",
|
||||||
|
"@govoplan/policy-webui": "file:../../govoplan-policy/webui"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.2",
|
"@types/react": "^19.0.2",
|
||||||
@@ -41,12 +43,12 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-access/webui": {
|
"../../govoplan-access/webui": {
|
||||||
"name": "@govoplan/access-webui",
|
"name": "@govoplan/access-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -60,12 +62,28 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-admin/webui": {
|
"../../govoplan-admin/webui": {
|
||||||
"name": "@govoplan/admin-webui",
|
"name": "@govoplan/admin-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"../../govoplan-audit/webui": {
|
||||||
|
"name": "@govoplan/audit-webui",
|
||||||
|
"version": "0.1.7",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -79,9 +97,9 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-calendar/webui": {
|
"../../govoplan-calendar/webui": {
|
||||||
"name": "@govoplan/calendar-webui",
|
"name": "@govoplan/calendar-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -98,15 +116,15 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-campaign/webui": {
|
"../../govoplan-campaign/webui": {
|
||||||
"name": "@govoplan/campaign-webui",
|
"name": "@govoplan/campaign-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"read-excel-file": "^9.2.0"
|
"read-excel-file": "9.2.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -120,9 +138,9 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-dashboard/webui": {
|
"../../govoplan-dashboard/webui": {
|
||||||
"name": "@govoplan/dashboard-webui",
|
"name": "@govoplan/dashboard-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -136,9 +154,9 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-docs/webui": {
|
"../../govoplan-docs/webui": {
|
||||||
"name": "@govoplan/docs-webui",
|
"name": "@govoplan/docs-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -155,9 +173,9 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-files/webui": {
|
"../../govoplan-files/webui": {
|
||||||
"name": "@govoplan/files-webui",
|
"name": "@govoplan/files-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -174,9 +192,9 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-idm/webui": {
|
"../../govoplan-idm/webui": {
|
||||||
"name": "@govoplan/idm-webui",
|
"name": "@govoplan/idm-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -193,12 +211,12 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-mail/webui": {
|
"../../govoplan-mail/webui": {
|
||||||
"name": "@govoplan/mail-webui",
|
"name": "@govoplan/mail-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5.7.2"
|
"typescript": "^5.7.2"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
@@ -212,9 +230,9 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-ops/webui": {
|
"../../govoplan-ops/webui": {
|
||||||
"name": "@govoplan/ops-webui",
|
"name": "@govoplan/ops-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -231,9 +249,9 @@
|
|||||||
},
|
},
|
||||||
"../../govoplan-organizations/webui": {
|
"../../govoplan-organizations/webui": {
|
||||||
"name": "@govoplan/organizations-webui",
|
"name": "@govoplan/organizations-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -248,6 +266,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../../govoplan-policy/webui": {
|
||||||
|
"name": "@govoplan/policy-webui",
|
||||||
|
"version": "0.1.7",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||||
@@ -980,6 +1014,10 @@
|
|||||||
"resolved": "../../govoplan-admin/webui",
|
"resolved": "../../govoplan-admin/webui",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@govoplan/audit-webui": {
|
||||||
|
"resolved": "../../govoplan-audit/webui",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@govoplan/calendar-webui": {
|
"node_modules/@govoplan/calendar-webui": {
|
||||||
"resolved": "../../govoplan-calendar/webui",
|
"resolved": "../../govoplan-calendar/webui",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -1016,6 +1054,10 @@
|
|||||||
"resolved": "../../govoplan-organizations/webui",
|
"resolved": "../../govoplan-organizations/webui",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@govoplan/policy-webui": {
|
||||||
|
"resolved": "../../govoplan-policy/webui",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@jridgewell/gen-mapping": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
|
|||||||
@@ -1,25 +1,32 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-access.git#v0.1.6",
|
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-access.git#v0.1.7",
|
||||||
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-admin.git#v0.1.6",
|
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-admin.git#v0.1.7",
|
||||||
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-calendar.git#v0.1.6",
|
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-audit.git#v0.1.7",
|
||||||
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-campaign.git#v0.1.6",
|
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-calendar.git#v0.1.7",
|
||||||
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git#v0.1.6",
|
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-campaign.git#v0.1.7",
|
||||||
"@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-mail.git#v0.1.6"
|
"@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-dashboard.git#v0.1.7",
|
||||||
|
"@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-docs.git#v0.1.7",
|
||||||
|
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git#v0.1.7",
|
||||||
|
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-idm.git#v0.1.7",
|
||||||
|
"@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-mail.git#v0.1.7",
|
||||||
|
"@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-ops.git#v0.1.7",
|
||||||
|
"@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-organizations.git#v0.1.7",
|
||||||
|
"@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-policy.git#v0.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.2",
|
"@types/react": "^19.0.2",
|
||||||
"@types/react-dom": "^19.0.2",
|
"@types/react-dom": "^19.0.2",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"lucide-react": "^0.555.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.1.1",
|
"react-router-dom": "^7.1.1",
|
||||||
@@ -27,7 +34,7 @@
|
|||||||
"vite": "^6.0.6"
|
"vite": "^6.0.6"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"lucide-react": "^0.555.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.1.1"
|
"react-router-dom": "^7.1.1"
|
||||||
@@ -713,11 +720,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@govoplan/access-webui": {
|
"node_modules/@govoplan/access-webui": {
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-access.git#37828fe34099c17267e63cfbc3a78ae1075b6b52",
|
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-access.git#d08a41fb56053331cc6f6ffe2757b6f2e241f33a",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"lucide-react": "^0.555.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.1.1"
|
"react-router-dom": "^7.1.1"
|
||||||
@@ -729,11 +736,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@govoplan/admin-webui": {
|
"node_modules/@govoplan/admin-webui": {
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-admin.git#0f2a9beca76c695a70a709bce2d843e6b1349277",
|
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-admin.git#24edb7eb8a4f7b63297718319a3bbdf74b3a9aeb",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"lucide-react": "^0.555.0",
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@govoplan/audit-webui": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-audit.git#7ffb16d98198f7485275bb3204ed71fe13f9e76a",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.1.1"
|
"react-router-dom": "^7.1.1"
|
||||||
@@ -745,12 +768,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@govoplan/calendar-webui": {
|
"node_modules/@govoplan/calendar-webui": {
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-calendar.git#c3c867391c39813593930f0272a1a0142c70872b",
|
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-calendar.git#39d2c28ab12f59d549f5e6d11584e8dbba1e3f52",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"lucide-react": "^0.555.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.1.1",
|
"react-router-dom": "^7.1.1",
|
||||||
@@ -764,14 +787,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@govoplan/campaign-webui": {
|
"node_modules/@govoplan/campaign-webui": {
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-campaign.git#57f6066bf72ba817ce5a1973405d2422ae59678d",
|
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-campaign.git#83b2082c74ae9d09c1fec7eb709368ffc5116ecb",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"read-excel-file": "^9.2.0"
|
"read-excel-file": "9.2.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"lucide-react": "^0.555.0",
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.1.1"
|
"react-router-dom": "^7.1.1"
|
||||||
@@ -782,13 +805,67 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@govoplan/files-webui": {
|
"node_modules/@govoplan/dashboard-webui": {
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git#18e6c3eb9b776bd58c18a750725d6cd8a403bc5c",
|
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-dashboard.git#a8cf9949c64be366ce1d45ec4495a99b53aa0ee3",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@govoplan/docs-webui": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-docs.git#370356882cf5becfdf6fc0c53f2a2b2b3db1e975",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"lucide-react": "^0.555.0",
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.6"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@govoplan/files-webui": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git#620fdda2fc434005ccf9550f730687b9d84083ed",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.6"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@govoplan/idm-webui": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-idm.git#6db1cea1b18a199a200e147c229abf8b3b999dac",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.1.1",
|
"react-router-dom": "^7.1.1",
|
||||||
@@ -802,11 +879,65 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@govoplan/mail-webui": {
|
"node_modules/@govoplan/mail-webui": {
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-mail.git#b4b0c76455e5fddadd03af3b023c82678dedf36f",
|
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-mail.git#286b6cafd977a223418b5cc9446fd1a95e29428c",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@govoplan/core-webui": "^0.1.6",
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
"lucide-react": "^0.555.0",
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@govoplan/ops-webui": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-ops.git#812216ad13b513b9dd02b752bc3e8cf213e5e510",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.6"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@govoplan/organizations-webui": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-organizations.git#4c6953e43e3f458f8a7204d501c3c071f4972224",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.1.1",
|
||||||
|
"typescript": "^5.7.2",
|
||||||
|
"vite": "^6.0.6"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@govoplan/core-webui": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@govoplan/policy-webui": {
|
||||||
|
"version": "0.1.7",
|
||||||
|
"resolved": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-policy.git#97f05a083fcc5c694e60f1939b47ee8cfbcac089",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@govoplan/core-webui": "^0.1.7",
|
||||||
|
"lucide-react": "^1.23.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-router-dom": "^7.1.1"
|
"react-router-dom": "^7.1.1"
|
||||||
@@ -1584,9 +1715,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lucide-react": {
|
"node_modules/lucide-react": {
|
||||||
"version": "0.555.0",
|
"version": "1.24.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.555.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.24.0.tgz",
|
||||||
"integrity": "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA==",
|
"integrity": "sha512-YT6mBD8lGKkg4nM39enlm94/sfJIiW0YKUT60fBy4YK8tai31ylg1VhGNWxkpSKHo9UagfnZqwIff3HTDQwXeA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||||
"@govoplan/admin-webui": "file:../../govoplan-admin/webui",
|
"@govoplan/admin-webui": "file:../../govoplan-admin/webui",
|
||||||
|
"@govoplan/audit-webui": "file:../../govoplan-audit/webui",
|
||||||
"@govoplan/calendar-webui": "file:../../govoplan-calendar/webui",
|
"@govoplan/calendar-webui": "file:../../govoplan-calendar/webui",
|
||||||
"@govoplan/campaign-webui": "file:../../govoplan-campaign/webui",
|
"@govoplan/campaign-webui": "file:../../govoplan-campaign/webui",
|
||||||
"@govoplan/dashboard-webui": "file:../../govoplan-dashboard/webui",
|
"@govoplan/dashboard-webui": "file:../../govoplan-dashboard/webui",
|
||||||
@@ -36,7 +37,8 @@
|
|||||||
"@govoplan/idm-webui": "file:../../govoplan-idm/webui",
|
"@govoplan/idm-webui": "file:../../govoplan-idm/webui",
|
||||||
"@govoplan/mail-webui": "file:../../govoplan-mail/webui",
|
"@govoplan/mail-webui": "file:../../govoplan-mail/webui",
|
||||||
"@govoplan/organizations-webui": "file:../../govoplan-organizations/webui",
|
"@govoplan/organizations-webui": "file:../../govoplan-organizations/webui",
|
||||||
"@govoplan/ops-webui": "file:../../govoplan-ops/webui"
|
"@govoplan/ops-webui": "file:../../govoplan-ops/webui",
|
||||||
|
"@govoplan/policy-webui": "file:../../govoplan-policy/webui"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.2",
|
"@types/react": "^19.0.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@govoplan/core-webui",
|
"name": "@govoplan/core-webui",
|
||||||
"version": "0.1.6",
|
"version": "0.1.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
@@ -22,17 +22,19 @@
|
|||||||
"preview": "vite preview --host 127.0.0.1 --port 4173"
|
"preview": "vite preview --host 127.0.0.1 --port 4173"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-access.git#v0.1.6",
|
"@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-access.git#v0.1.7",
|
||||||
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-admin.git#v0.1.6",
|
"@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-admin.git#v0.1.7",
|
||||||
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-calendar.git#v0.1.6",
|
"@govoplan/audit-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-audit.git#v0.1.7",
|
||||||
"@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-dashboard.git#v0.1.6",
|
"@govoplan/calendar-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-calendar.git#v0.1.7",
|
||||||
"@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-docs.git#v0.1.6",
|
"@govoplan/dashboard-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-dashboard.git#v0.1.7",
|
||||||
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git#v0.1.6",
|
"@govoplan/docs-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-docs.git#v0.1.7",
|
||||||
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-idm.git#v0.1.6",
|
"@govoplan/files-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-files.git#v0.1.7",
|
||||||
"@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-mail.git#v0.1.6",
|
"@govoplan/idm-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-idm.git#v0.1.7",
|
||||||
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-campaign.git#v0.1.6",
|
"@govoplan/mail-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-mail.git#v0.1.7",
|
||||||
"@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-organizations.git#v0.1.6",
|
"@govoplan/campaign-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-campaign.git#v0.1.7",
|
||||||
"@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-ops.git#v0.1.6"
|
"@govoplan/organizations-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-organizations.git#v0.1.7",
|
||||||
|
"@govoplan/ops-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-ops.git#v0.1.7",
|
||||||
|
"@govoplan/policy-webui": "git+ssh://git@git.add-ideas.de/add-ideas/govoplan-policy.git#v0.1.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"lucide-react": "^1.23.0",
|
"lucide-react": "^1.23.0",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process";
|
|||||||
const packageByModule = {
|
const packageByModule = {
|
||||||
access: "@govoplan/access-webui",
|
access: "@govoplan/access-webui",
|
||||||
admin: "@govoplan/admin-webui",
|
admin: "@govoplan/admin-webui",
|
||||||
|
audit: "@govoplan/audit-webui",
|
||||||
campaigns: "@govoplan/campaign-webui",
|
campaigns: "@govoplan/campaign-webui",
|
||||||
dashboard: "@govoplan/dashboard-webui",
|
dashboard: "@govoplan/dashboard-webui",
|
||||||
docs: "@govoplan/docs-webui",
|
docs: "@govoplan/docs-webui",
|
||||||
@@ -10,7 +11,8 @@ const packageByModule = {
|
|||||||
idm: "@govoplan/idm-webui",
|
idm: "@govoplan/idm-webui",
|
||||||
mail: "@govoplan/mail-webui",
|
mail: "@govoplan/mail-webui",
|
||||||
organizations: "@govoplan/organizations-webui",
|
organizations: "@govoplan/organizations-webui",
|
||||||
ops: "@govoplan/ops-webui"
|
ops: "@govoplan/ops-webui",
|
||||||
|
policy: "@govoplan/policy-webui"
|
||||||
};
|
};
|
||||||
|
|
||||||
const cases = [
|
const cases = [
|
||||||
@@ -18,6 +20,7 @@ const cases = [
|
|||||||
{ name: "access-only", modules: ["access"] },
|
{ name: "access-only", modules: ["access"] },
|
||||||
{ name: "admin-only", modules: ["admin"] },
|
{ name: "admin-only", modules: ["admin"] },
|
||||||
{ name: "access-with-admin", modules: ["access", "admin"] },
|
{ name: "access-with-admin", modules: ["access", "admin"] },
|
||||||
|
{ name: "admin-with-policy-and-audit", modules: ["access", "admin", "policy", "audit"] },
|
||||||
{ name: "dashboard-only", modules: ["dashboard"] },
|
{ name: "dashboard-only", modules: ["dashboard"] },
|
||||||
{ name: "files-only", modules: ["files"] },
|
{ name: "files-only", modules: ["files"] },
|
||||||
{ name: "mail-only", modules: ["mail"] },
|
{ name: "mail-only", modules: ["mail"] },
|
||||||
@@ -27,7 +30,7 @@ const cases = [
|
|||||||
{ name: "campaign-with-files-no-mail", modules: ["campaigns", "files"] },
|
{ name: "campaign-with-files-no-mail", modules: ["campaigns", "files"] },
|
||||||
{ name: "campaign-with-mail-no-files", modules: ["campaigns", "mail"] },
|
{ name: "campaign-with-mail-no-files", modules: ["campaigns", "mail"] },
|
||||||
{ name: "docs-and-ops", modules: ["access", "docs", "ops"] },
|
{ name: "docs-and-ops", modules: ["access", "docs", "ops"] },
|
||||||
{ name: "full-product", modules: ["access", "admin", "dashboard", "organizations", "idm", "campaigns", "files", "mail", "docs", "ops"] }
|
{ name: "full-product", modules: ["access", "admin", "policy", "audit", "dashboard", "organizations", "idm", "campaigns", "files", "mail", "docs", "ops"] }
|
||||||
];
|
];
|
||||||
|
|
||||||
const npmExec = process.env.npm_execpath;
|
const npmExec = process.env.npm_execpath;
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ const resolvedInstalledModulesVirtualId = `\0${installedModulesVirtualId}`;
|
|||||||
const defaultWebModulePackages = [
|
const defaultWebModulePackages = [
|
||||||
"@govoplan/access-webui",
|
"@govoplan/access-webui",
|
||||||
"@govoplan/admin-webui",
|
"@govoplan/admin-webui",
|
||||||
|
"@govoplan/audit-webui",
|
||||||
"@govoplan/calendar-webui",
|
"@govoplan/calendar-webui",
|
||||||
"@govoplan/campaign-webui",
|
"@govoplan/campaign-webui",
|
||||||
"@govoplan/dashboard-webui",
|
"@govoplan/dashboard-webui",
|
||||||
@@ -21,7 +22,8 @@ const defaultWebModulePackages = [
|
|||||||
"@govoplan/idm-webui",
|
"@govoplan/idm-webui",
|
||||||
"@govoplan/mail-webui",
|
"@govoplan/mail-webui",
|
||||||
"@govoplan/organizations-webui",
|
"@govoplan/organizations-webui",
|
||||||
"@govoplan/ops-webui"
|
"@govoplan/ops-webui",
|
||||||
|
"@govoplan/policy-webui"
|
||||||
];
|
];
|
||||||
|
|
||||||
function configuredWebModulePackages(): string[] {
|
function configuredWebModulePackages(): string[] {
|
||||||
@@ -92,6 +94,7 @@ export default defineConfig({
|
|||||||
fileURLToPath(new URL('.', import.meta.url)),
|
fileURLToPath(new URL('.', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-access/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-access/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-admin/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-admin/webui', import.meta.url)),
|
||||||
|
fileURLToPath(new URL('../../govoplan-audit/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-calendar/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-calendar/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-dashboard/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-dashboard/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-docs/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-docs/webui', import.meta.url)),
|
||||||
@@ -100,7 +103,8 @@ export default defineConfig({
|
|||||||
fileURLToPath(new URL('../../govoplan-mail/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-mail/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-organizations/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-organizations/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-campaign/webui', import.meta.url)),
|
fileURLToPath(new URL('../../govoplan-campaign/webui', import.meta.url)),
|
||||||
fileURLToPath(new URL('../../govoplan-ops/webui', import.meta.url))
|
fileURLToPath(new URL('../../govoplan-ops/webui', import.meta.url)),
|
||||||
|
fileURLToPath(new URL('../../govoplan-policy/webui', import.meta.url))
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
proxy: {
|
proxy: {
|
||||||
|
|||||||
Reference in New Issue
Block a user