fix(release): prepare aligned 0.1.45 composition and guarded candidate sequencing
This commit is contained in:
@@ -221,6 +221,9 @@ Repository capabilities are frozen into each plan unit (`python-package`,
|
||||
`core-release-bundle`, and the universal `git-source`) and determine which
|
||||
steps appear. Internally aligned version changes are rendered deterministically
|
||||
from recognized TOML, JSON, lockfile, manifest, and package declarations.
|
||||
The manifest may use a literal version or a top-level literal `MODULE_VERSION`;
|
||||
the latter is updated without rewriting independently versioned interfaces.
|
||||
Computed or missing version declarations fail before any metadata is written.
|
||||
Pre-existing dirty worktrees remain visible but have no commit executor; the
|
||||
console never absorbs unrelated operator changes.
|
||||
|
||||
@@ -232,6 +235,17 @@ runs a receipt-bound alignment gate before exposing any atomic branch/tag push.
|
||||
A failed step stops later steps while preserving prior receipts for explicit
|
||||
retry or reconciliation.
|
||||
|
||||
Local module candidate creation deliberately does not require those candidates
|
||||
to be resolved already in Core's release lock: their annotated tags are inputs
|
||||
to the next lock-generation step. The internal tag helper applies this ordering
|
||||
only when no Core repository is selected and remote publication is disabled.
|
||||
Module version/lock consistency, manifest validity, clean/non-behind worktrees,
|
||||
and local/remote tag immutability checks still apply. Core candidate tagging
|
||||
continues to validate its own complete bundle, and every remote-publication
|
||||
preview and execution requires the selected modules to match Core's release
|
||||
input and resolved lock. A local candidate is therefore not publication
|
||||
approval; a stale Core lock blocks publication without changing remote refs.
|
||||
|
||||
The browser likewise retains the request identifier for an uncertain
|
||||
resume/retry/reconciliation response and replays it after reload. A successful
|
||||
replay selects the returned run state. Transport and server failures retain the
|
||||
@@ -515,8 +529,9 @@ updated catalog, and keep the published keyring healthy.
|
||||
|
||||
When a selected module exposes a WebUI package, its requested version must also
|
||||
match Core's `webui/package.release.json` input and the resolved
|
||||
`package-lock.release.json` entry. The source-tag preflight, selective plan, and
|
||||
catalog-candidate writer all enforce this composition boundary. Pins for modules
|
||||
`package-lock.release.json` entry. The source-publication preflight, selective
|
||||
plan, and catalog-candidate writer all enforce this composition boundary;
|
||||
module-only local candidate tags use the staged order described above. Pins for modules
|
||||
that are not part of the selective release remain unchanged.
|
||||
|
||||
Release integration also enforces repository and composition version alignment
|
||||
|
||||
@@ -4,46 +4,46 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan"
|
||||
version = "0.1.44"
|
||||
version = "0.1.45"
|
||||
description = "Developer convenience package for a versioned GovOPlaN composition"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
license = { text = "AGPL-3.0-or-later" }
|
||||
dependencies = [
|
||||
"govoplan-core[server]==0.1.44",
|
||||
"govoplan-tenancy==0.1.21",
|
||||
"govoplan-organizations==0.1.20",
|
||||
"govoplan-core[server]==0.1.45",
|
||||
"govoplan-tenancy==0.1.22",
|
||||
"govoplan-organizations==0.1.21",
|
||||
"govoplan-identity==0.1.20",
|
||||
"govoplan-idm==0.1.24",
|
||||
"govoplan-access==0.1.24",
|
||||
"govoplan-admin==0.1.22",
|
||||
"govoplan-policy==0.1.22",
|
||||
"govoplan-idm==0.1.25",
|
||||
"govoplan-access==0.1.25",
|
||||
"govoplan-admin==0.1.23",
|
||||
"govoplan-policy==0.1.23",
|
||||
"govoplan-audit==0.1.20",
|
||||
"govoplan-dashboard==0.1.20",
|
||||
"govoplan-files==0.1.25",
|
||||
"govoplan-mail==0.1.26",
|
||||
"govoplan-campaign==0.1.27",
|
||||
"govoplan-files==0.1.26",
|
||||
"govoplan-mail==0.1.27",
|
||||
"govoplan-campaign==0.1.28",
|
||||
"govoplan-calendar==0.1.23",
|
||||
"govoplan-docs==0.1.22",
|
||||
"govoplan-ops==0.1.21",
|
||||
"govoplan-docs==0.1.23",
|
||||
"govoplan-ops==0.1.22",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
full = [
|
||||
"govoplan-addresses==0.1.21",
|
||||
"govoplan-addresses==0.1.22",
|
||||
"govoplan-approvals==0.1.20",
|
||||
"govoplan-assets==0.1.20",
|
||||
"govoplan-booking==0.1.20",
|
||||
"govoplan-cases==0.1.23",
|
||||
"govoplan-cases==0.1.24",
|
||||
"govoplan-certificates==0.1.20",
|
||||
"govoplan-committee==0.1.20",
|
||||
"govoplan-connectors==0.1.25",
|
||||
"govoplan-committee==0.1.21",
|
||||
"govoplan-connectors==0.1.26",
|
||||
"govoplan-consultation==0.1.20",
|
||||
"govoplan-contracts==0.1.20",
|
||||
"govoplan-dataflow==0.1.23",
|
||||
"govoplan-datasources==0.1.24",
|
||||
"govoplan-dataflow==0.1.24",
|
||||
"govoplan-datasources==0.1.25",
|
||||
"govoplan-decisions==0.1.19",
|
||||
"govoplan-dist-lists==0.1.20",
|
||||
"govoplan-dist-lists==0.1.21",
|
||||
"govoplan-dms==0.1.20",
|
||||
"govoplan-encryption==0.1.19",
|
||||
"govoplan-erp==0.1.20",
|
||||
@@ -58,7 +58,7 @@ full = [
|
||||
"govoplan-inspections==0.1.20",
|
||||
"govoplan-learning==0.1.20",
|
||||
"govoplan-mandates==0.1.19",
|
||||
"govoplan-notifications==0.1.19",
|
||||
"govoplan-notifications==0.1.20",
|
||||
"govoplan-parties==0.1.19",
|
||||
"govoplan-payments==0.1.21",
|
||||
"govoplan-permits==0.1.20",
|
||||
@@ -69,22 +69,22 @@ full = [
|
||||
"govoplan-projects==0.1.19",
|
||||
"govoplan-quick-access==0.1.20",
|
||||
"govoplan-records==0.1.23",
|
||||
"govoplan-reporting==0.1.20",
|
||||
"govoplan-reporting==0.1.21",
|
||||
"govoplan-resources==0.1.20",
|
||||
"govoplan-rest==0.1.19",
|
||||
"govoplan-risk-compliance==0.1.20",
|
||||
"govoplan-scheduling==0.1.21",
|
||||
"govoplan-search==0.1.19",
|
||||
"govoplan-risk-compliance==0.1.21",
|
||||
"govoplan-scheduling==0.1.22",
|
||||
"govoplan-search==0.1.20",
|
||||
"govoplan-services==0.1.19",
|
||||
"govoplan-soap==0.1.19",
|
||||
"govoplan-tasks==0.1.22",
|
||||
"govoplan-templates==0.1.21",
|
||||
"govoplan-tasks==0.1.23",
|
||||
"govoplan-templates==0.1.22",
|
||||
"govoplan-tickets==0.1.22",
|
||||
"govoplan-transparency==0.1.20",
|
||||
"govoplan-views==0.1.21",
|
||||
"govoplan-voting==0.1.20",
|
||||
"govoplan-views==0.1.22",
|
||||
"govoplan-voting==0.1.21",
|
||||
"govoplan-wiki==0.1.22",
|
||||
"govoplan-workflow==0.1.22",
|
||||
"govoplan-workflow==0.1.23",
|
||||
"govoplan-workflow-engine==0.1.21",
|
||||
"govoplan-xrechnung==0.1.21",
|
||||
]
|
||||
|
||||
@@ -2,7 +2,7 @@ bandit>=1.8,<2
|
||||
click>=8.3.3
|
||||
filelock>=3.20.3
|
||||
idna>=3.15
|
||||
pip>=26.1.2
|
||||
pip>=26.2
|
||||
pip-audit>=2.9,<3
|
||||
python-multipart>=0.0.31
|
||||
radon>=6,<7
|
||||
|
||||
@@ -56,7 +56,7 @@ httpx2>=2.5,<3
|
||||
filelock>=3.20.3
|
||||
idna>=3.15
|
||||
jsonschema>=4,<5
|
||||
pip>=26.1.2
|
||||
pip>=26.2
|
||||
pip-audit>=2.9,<3
|
||||
pytest>=9.0.3,<10
|
||||
pygments>=2.20,<3
|
||||
|
||||
+11
-11
@@ -1,18 +1,18 @@
|
||||
# Whole-product release install from immutable, independently versioned module tags.
|
||||
# Only add a module after its referenced tag has been published.
|
||||
../govoplan-core[server]
|
||||
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.21
|
||||
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.20
|
||||
govoplan-tenancy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-tenancy.git@v0.1.22
|
||||
govoplan-organizations @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-organizations.git@v0.1.21
|
||||
govoplan-identity @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-identity.git@v0.1.20
|
||||
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.24
|
||||
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.24
|
||||
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.22
|
||||
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.22
|
||||
govoplan-idm @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-idm.git@v0.1.25
|
||||
govoplan-access @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git@v0.1.25
|
||||
govoplan-admin @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git@v0.1.23
|
||||
govoplan-policy @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-policy.git@v0.1.23
|
||||
govoplan-audit @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-audit.git@v0.1.20
|
||||
govoplan-dashboard @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-dashboard.git@v0.1.20
|
||||
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.25
|
||||
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.26
|
||||
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.27
|
||||
govoplan-files @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-files.git@v0.1.26
|
||||
govoplan-mail @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-mail.git@v0.1.27
|
||||
govoplan-campaign @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-campaign.git@v0.1.28
|
||||
govoplan-calendar @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-calendar.git@v0.1.23
|
||||
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.22
|
||||
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.21
|
||||
govoplan-docs @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-docs.git@v0.1.23
|
||||
govoplan-ops @ git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-ops.git@v0.1.22
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { findTypeOnlyJsxImports } from "../tools/checks/check-jsx-value-imports.mjs";
|
||||
|
||||
const findings = (source) => findTypeOnlyJsxImports([{ path: "/fixture.tsx", source }]).map((item) => item.component);
|
||||
assert.deepEqual(findings('import type { FormGrid, AuthInfo } from "@govoplan/core-webui"; const page = <Dialog><FormGrid /></Dialog>;'), ["FormGrid"]);
|
||||
assert.deepEqual(findings('import { type FormGrid as Layout } from "ui"; const page = <Layout>Content</Layout>;'), ["Layout"]);
|
||||
assert.deepEqual(findings('import type Layout from "ui"; const page = <Layout />;'), ["Layout"]);
|
||||
assert.deepEqual(findings('import type * as ui from "ui"; const page = <ui.Layout />;'), ["ui.Layout"]);
|
||||
assert.deepEqual(findings('import type { FormGrid } from "ui"; const page = <div title={<FormGrid />} />;'), ["FormGrid"]);
|
||||
assert.deepEqual(findings('import { FormGrid, type AuthInfo } from "ui"; const page = <FormGrid />;'), []);
|
||||
assert.deepEqual(findings('import type { FormGrid } from "ui"; function Page({ FormGrid }: Props) { return <FormGrid />; }'), []);
|
||||
assert.deepEqual(findings('import type * as ui from "ui"; function Page(ui: RuntimeControls) { return <ui.Layout />; }'), []);
|
||||
assert.deepEqual(findings('import type { Layout } from "ui"; const page: Layout = {};'), []);
|
||||
assert.deepEqual(findings('import type { input } from "ui"; const page = <input />;'), []);
|
||||
console.log("JSX runtime-import AST regression tests passed (10 cases).");
|
||||
@@ -25,6 +25,11 @@ from govoplan_core.core.institutional import (
|
||||
TemporalRevision,
|
||||
service_launch_capability,
|
||||
)
|
||||
from govoplan_core.core.temporal import (
|
||||
TemporalDataContext,
|
||||
bind_temporal_data_context,
|
||||
reset_temporal_data_context,
|
||||
)
|
||||
from govoplan_cases.backend.party_context import CasePartyContext
|
||||
from govoplan_cases.backend.db.models import (
|
||||
CaseAccessGrant,
|
||||
@@ -134,6 +139,13 @@ class _Registry:
|
||||
|
||||
|
||||
class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
# Portal's effective_at does not replace the SQL provider's request-local
|
||||
# read clock. Keep both on the journey date, without bypassing validity
|
||||
# filtering or extending the fixture's finite publication interval.
|
||||
token = bind_temporal_data_context(TemporalDataContext(evaluated_at=NOW))
|
||||
self.addCleanup(reset_temporal_data_context, token)
|
||||
|
||||
def test_service_to_formal_outcome_retains_governed_context(self) -> None:
|
||||
engine = create_engine("sqlite+pysqlite:///:memory:")
|
||||
for table in (
|
||||
@@ -220,13 +232,44 @@ class InstitutionalGovernanceJourneyTests(unittest.TestCase):
|
||||
service_launch_capability("case"): object(),
|
||||
}
|
||||
)
|
||||
entry = PortalServiceDirectory(service_registry).list_entries(
|
||||
directory = PortalServiceDirectory(service_registry)
|
||||
for outside_interval in (
|
||||
service.temporal.valid_from - timedelta(microseconds=1),
|
||||
service.temporal.valid_to,
|
||||
):
|
||||
with self.subTest(outside_interval=outside_interval):
|
||||
token = bind_temporal_data_context(
|
||||
TemporalDataContext(evaluated_at=outside_interval)
|
||||
)
|
||||
try:
|
||||
# Keep Portal inside the valid interval: the real SQL
|
||||
# provider must still exclude a service outside its own
|
||||
# temporal read context, before Portal can project it.
|
||||
self.assertEqual(
|
||||
(),
|
||||
directory.list_entries(
|
||||
session,
|
||||
principal,
|
||||
tenant_id="tenant-1",
|
||||
effective_at=NOW,
|
||||
audiences=("resident",),
|
||||
),
|
||||
)
|
||||
finally:
|
||||
reset_temporal_data_context(token)
|
||||
|
||||
entries = directory.list_entries(
|
||||
session,
|
||||
principal,
|
||||
tenant_id="tenant-1",
|
||||
effective_at=NOW,
|
||||
audiences=("resident",),
|
||||
)[0]
|
||||
)
|
||||
self.assertEqual(
|
||||
(service.reference,),
|
||||
tuple(entry.definition.reference for entry in entries),
|
||||
)
|
||||
entry = entries[0]
|
||||
self.assertTrue(entry.available)
|
||||
intake = CaseServiceIntake().plan(
|
||||
entry.definition,
|
||||
|
||||
@@ -62,22 +62,37 @@ class PackageRegistryReleaseTests(unittest.TestCase):
|
||||
encoding="utf-8"
|
||||
)
|
||||
)["project"]["version"]
|
||||
meta_package = ROOT / "packages/govoplan-meta/pyproject.toml"
|
||||
meta_project = tomllib.loads(
|
||||
meta_package.read_text(encoding="utf-8")
|
||||
)["project"]
|
||||
expected_tasks_pin = next(
|
||||
requirement
|
||||
for requirement in (
|
||||
*meta_project["dependencies"],
|
||||
*meta_project["optional-dependencies"]["full"],
|
||||
)
|
||||
if requirement.startswith("govoplan-tasks==")
|
||||
)
|
||||
selected = PACKAGE_SET.parse_meta_package(
|
||||
ROOT / "packages/govoplan-meta/pyproject.toml",
|
||||
meta_package,
|
||||
core_version=core_version,
|
||||
)
|
||||
|
||||
by_name = {item["name"]: item for item in selected}
|
||||
self.assertIn("govoplan-core", by_name)
|
||||
self.assertIn("govoplan-records", by_name)
|
||||
self.assertEqual("0.1.22", by_name["govoplan-tasks"]["version"])
|
||||
self.assertEqual(
|
||||
expected_tasks_pin,
|
||||
f"govoplan-tasks=={by_name['govoplan-tasks']['version']}",
|
||||
)
|
||||
|
||||
payload = PACKAGE_SET.generate_package_set(
|
||||
core_version=core_version,
|
||||
requirements=ROOT / "requirements-release.txt",
|
||||
workspace=ROOT.parent,
|
||||
profile="full",
|
||||
meta_package=ROOT / "packages/govoplan-meta/pyproject.toml",
|
||||
meta_package=meta_package,
|
||||
)
|
||||
self.assertEqual("full", payload["profile"])
|
||||
self.assertEqual(len(selected), len(payload["python"]))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import runpy
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
@@ -214,21 +215,23 @@ class ReleaseRepositoryTagTests(unittest.TestCase):
|
||||
git(self.repo, "commit", "-m", "Add release WebUI composition")
|
||||
git(self.repo, "push", "origin", "main")
|
||||
|
||||
result = tag_repositories(
|
||||
repos=("govoplan-core", "govoplan-campaign"),
|
||||
repo_versions={"govoplan-core": "0.1.10", "govoplan-campaign": "0.1.10"},
|
||||
workspace_root=self.workspace,
|
||||
apply=True,
|
||||
push=True,
|
||||
)
|
||||
for push in (False, True):
|
||||
with self.subTest(push=push):
|
||||
result = tag_repositories(
|
||||
repos=("govoplan-core", "govoplan-campaign"),
|
||||
repo_versions={"govoplan-core": "0.1.10", "govoplan-campaign": "0.1.10"},
|
||||
workspace_root=self.workspace,
|
||||
apply=True,
|
||||
push=push,
|
||||
)
|
||||
|
||||
self.assertEqual("blocked", result["status"])
|
||||
self.assertIn("no selected repository was mutated", result["detail"])
|
||||
self.assertEqual("skipped", result["repositories"][0]["status"])
|
||||
self.assertEqual("blocked", result["repositories"][1]["status"])
|
||||
self.assertIn("release WebUI composition gate failed", result["repositories"][1]["detail"])
|
||||
for repository in (self.repo, self.remote, campaign, campaign_remote):
|
||||
self.assertFalse(ref_exists(repository, "refs/tags/v0.1.10"))
|
||||
self.assertEqual("blocked", result["status"])
|
||||
self.assertIn("no selected repository was mutated", result["detail"])
|
||||
self.assertEqual("skipped", result["repositories"][0]["status"])
|
||||
self.assertEqual("blocked", result["repositories"][1]["status"])
|
||||
self.assertIn("release WebUI composition gate failed", result["repositories"][1]["detail"])
|
||||
for repository in (self.repo, self.remote, campaign, campaign_remote):
|
||||
self.assertFalse(ref_exists(repository, "refs/tags/v0.1.10"))
|
||||
|
||||
def test_api_keeps_legacy_source_release_preview_only(self) -> None:
|
||||
with TestClient(
|
||||
@@ -300,6 +303,183 @@ class ReleaseRepositoryTagTests(unittest.TestCase):
|
||||
self.assertIn("Signed Website Catalog", ui.text)
|
||||
self.assertIn("Apply + Website Tag", ui.text)
|
||||
|
||||
def test_local_module_candidate_precedes_core_lock_but_publication_does_not(self) -> None:
|
||||
campaign, campaign_remote = self._staged_campaign_bundle()
|
||||
remotes = (self.remote, self.manifest_remote, campaign_remote)
|
||||
remote_refs = {path: git_text(path, "show-ref") for path in remotes}
|
||||
arguments = {
|
||||
"repos": ("govoplan-campaign",),
|
||||
"repo_versions": {"govoplan-campaign": "0.1.10"},
|
||||
"workspace_root": self.workspace,
|
||||
}
|
||||
|
||||
preview = tag_repositories(**arguments, apply=False, push=False)
|
||||
self.assertEqual("planned", preview["status"], preview)
|
||||
self.assertFalse(ref_exists(campaign, "refs/tags/v0.1.10"))
|
||||
candidate = tag_repositories(**arguments, apply=True, push=False)
|
||||
self.assertEqual("tagged", candidate["status"], candidate)
|
||||
self.assertEqual("tag", git_text(campaign, "cat-file", "-t", "v0.1.10"))
|
||||
tag_object = git_text(campaign, "rev-parse", "v0.1.10")
|
||||
head = git_text(campaign, "rev-parse", "HEAD")
|
||||
self.assertEqual(head, git_text(campaign, "rev-parse", "v0.1.10^{commit}"))
|
||||
|
||||
for apply in (False, True):
|
||||
with self.subTest(publish_apply=apply):
|
||||
blocked = tag_repositories(**arguments, apply=apply, push=True)
|
||||
self.assertEqual("blocked", blocked["status"], blocked)
|
||||
self.assertIn(
|
||||
"release WebUI composition gate failed",
|
||||
blocked["repositories"][0]["detail"],
|
||||
)
|
||||
self.assertEqual(tag_object, git_text(campaign, "rev-parse", "v0.1.10"))
|
||||
self.assertEqual(remote_refs, {path: git_text(path, "show-ref") for path in remotes})
|
||||
|
||||
lock_path = self.repo / "webui" / "package-lock.release.json"
|
||||
lock = json.loads(lock_path.read_text(encoding="utf-8"))
|
||||
locked_campaign = lock["packages"]["node_modules/@govoplan/campaign-webui"]
|
||||
locked_campaign["version"] = "0.1.10"
|
||||
locked_campaign["resolved"] = f"git+ssh://git@example.test/acme/govoplan-campaign.git#{head}"
|
||||
lock_path.write_text(json.dumps(lock) + "\n", encoding="utf-8")
|
||||
git(self.repo, "add", "webui/package-lock.release.json")
|
||||
git(self.repo, "commit", "-m", "Resolve reviewed local Campaign candidate")
|
||||
|
||||
core_candidate = tag_repositories(
|
||||
repos=("govoplan-core",),
|
||||
repo_versions={"govoplan-core": "0.1.10"},
|
||||
workspace_root=self.workspace,
|
||||
apply=True,
|
||||
push=False,
|
||||
)
|
||||
self.assertEqual("tagged", core_candidate["status"], core_candidate)
|
||||
self.assertEqual(remote_refs, {path: git_text(path, "show-ref") for path in remotes})
|
||||
published = tag_repositories(**arguments, apply=True, push=True)
|
||||
self.assertEqual("published", published["status"], published)
|
||||
self.assertEqual(tag_object, git_text(campaign_remote, "rev-parse", "v0.1.10"))
|
||||
self.assertEqual(head, git_text(campaign_remote, "rev-parse", "refs/heads/main"))
|
||||
self.assertEqual(remote_refs[self.remote], git_text(self.remote, "show-ref"))
|
||||
|
||||
def test_local_core_candidate_still_requires_resolved_module_tags(self) -> None:
|
||||
campaign, campaign_remote = self._staged_campaign_bundle()
|
||||
for selected in (("govoplan-core",), ("govoplan-campaign", "govoplan-core")):
|
||||
with self.subTest(selected=selected):
|
||||
result = tag_repositories(
|
||||
repos=selected,
|
||||
repo_versions={repo: "0.1.10" for repo in selected},
|
||||
workspace_root=self.workspace,
|
||||
apply=True,
|
||||
push=False,
|
||||
)
|
||||
self.assertEqual("blocked", result["status"], result)
|
||||
core_row = next(row for row in result["repositories"] if row["repo"] == "govoplan-core")
|
||||
self.assertIn("version alignment gate failed", core_row["detail"])
|
||||
self.assertIn("@govoplan/campaign-webui:resolved", core_row["detail"])
|
||||
self.assertIn("expected 'local tag v0.1.10'", core_row["detail"])
|
||||
for repository in (self.repo, self.remote, campaign, campaign_remote):
|
||||
self.assertFalse(ref_exists(repository, "refs/tags/v0.1.10"))
|
||||
|
||||
def test_local_module_candidate_preserves_version_and_worktree_gates(self) -> None:
|
||||
campaign, campaign_remote = self._staged_campaign_bundle()
|
||||
arguments = {
|
||||
"repos": ("govoplan-campaign",),
|
||||
"workspace_root": self.workspace,
|
||||
"apply": True,
|
||||
"push": False,
|
||||
}
|
||||
mismatch = tag_repositories(**arguments, repo_versions={"govoplan-campaign": "0.1.11"})
|
||||
self.assertEqual("blocked", mismatch["status"], mismatch)
|
||||
self.assertIn("version alignment gate failed", mismatch["repositories"][0]["detail"])
|
||||
|
||||
(campaign / "unreviewed.txt").write_text("operator work\n", encoding="utf-8")
|
||||
dirty = tag_repositories(**arguments, repo_versions={"govoplan-campaign": "0.1.10"})
|
||||
self.assertEqual("blocked", dirty["status"], dirty)
|
||||
self.assertIn("worktree is not clean", dirty["repositories"][0]["detail"])
|
||||
for repository in (campaign, campaign_remote):
|
||||
for tag in ("v0.1.10", "v0.1.11"):
|
||||
self.assertFalse(ref_exists(repository, f"refs/tags/{tag}"))
|
||||
|
||||
def test_local_module_candidate_preserves_manifest_gate(self) -> None:
|
||||
campaign, campaign_remote = self._staged_campaign_bundle()
|
||||
replace_with_unscoped_workflow_manifest(self.manifest_repo)
|
||||
result = tag_repositories(
|
||||
repos=("govoplan-campaign",),
|
||||
repo_versions={"govoplan-campaign": "0.1.10"},
|
||||
workspace_root=self.workspace,
|
||||
apply=True,
|
||||
push=False,
|
||||
)
|
||||
self.assertEqual("blocked", result["status"], result)
|
||||
self.assertIn("scope-conditioned alternative", result["repositories"][0]["detail"])
|
||||
for repository in (campaign, campaign_remote):
|
||||
self.assertFalse(ref_exists(repository, "refs/tags/v0.1.10"))
|
||||
|
||||
def test_local_module_candidate_preserves_remote_tag_immutability(self) -> None:
|
||||
campaign, campaign_remote = self._staged_campaign_bundle()
|
||||
git(campaign, "tag", "-a", "v0.1.10", "-m", "Existing immutable tag", "v0.1.9^{commit}")
|
||||
git(campaign, "push", "origin", "refs/tags/v0.1.10")
|
||||
remote_refs = git_text(campaign_remote, "show-ref")
|
||||
for local_exists in (True, False):
|
||||
with self.subTest(local_exists=local_exists):
|
||||
if not local_exists:
|
||||
git(campaign, "tag", "-d", "v0.1.10")
|
||||
result = tag_repositories(
|
||||
repos=("govoplan-campaign",),
|
||||
repo_versions={"govoplan-campaign": "0.1.10"},
|
||||
workspace_root=self.workspace,
|
||||
apply=True,
|
||||
push=False,
|
||||
)
|
||||
self.assertEqual("blocked", result["status"], result)
|
||||
self.assertIn("immutable tag", result["repositories"][0]["detail"])
|
||||
self.assertIn("not HEAD", result["repositories"][0]["detail"])
|
||||
self.assertEqual(remote_refs, git_text(campaign_remote, "show-ref"))
|
||||
self.assertEqual(local_exists, ref_exists(campaign, "refs/tags/v0.1.10"))
|
||||
|
||||
def _staged_campaign_bundle(self) -> tuple[Path, Path]:
|
||||
campaign, remote = create_release_repo(
|
||||
root=self.root,
|
||||
workspace=self.workspace,
|
||||
name="govoplan-campaign",
|
||||
version="0.1.9",
|
||||
)
|
||||
campaign_webui = campaign / "webui"
|
||||
campaign_webui.mkdir()
|
||||
package_path = campaign_webui / "package.json"
|
||||
package_path.write_text(
|
||||
'{"name":"@govoplan/campaign-webui","version":"0.1.9"}\n', encoding="utf-8"
|
||||
)
|
||||
git(campaign, "add", "webui/package.json")
|
||||
git(campaign, "commit", "-m", "Prior Campaign WebUI package")
|
||||
git(campaign, "tag", "-a", "v0.1.9", "-m", "Prior Campaign release")
|
||||
git(campaign, "push", "origin", "main", "refs/tags/v0.1.9")
|
||||
prior_commit = git_text(campaign, "rev-parse", "HEAD")
|
||||
for path in (campaign / "pyproject.toml", package_path):
|
||||
path.write_text(path.read_text(encoding="utf-8").replace("0.1.9", "0.1.10"), encoding="utf-8")
|
||||
git(campaign, "add", "pyproject.toml", "webui/package.json")
|
||||
git(campaign, "commit", "-m", "Reviewed Campaign candidate")
|
||||
|
||||
core_webui = self.repo / "webui"
|
||||
core_webui.mkdir()
|
||||
dependency_ref = "git+ssh://git@example.test/acme/govoplan-campaign.git#v0.1.10"
|
||||
package = {
|
||||
"name": "@govoplan/core-webui",
|
||||
"version": "0.1.10",
|
||||
"dependencies": {"@govoplan/campaign-webui": dependency_ref},
|
||||
}
|
||||
(core_webui / "package.release.json").write_text(json.dumps(package) + "\n", encoding="utf-8")
|
||||
(core_webui / "package-lock.release.json").write_text(
|
||||
json.dumps({"packages": {
|
||||
"": package,
|
||||
"node_modules/@govoplan/campaign-webui": {
|
||||
"version": "0.1.9",
|
||||
"resolved": f"git+ssh://git@example.test/acme/govoplan-campaign.git#{prior_commit}",
|
||||
},
|
||||
}}) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
git(self.repo, "add", "webui/package.release.json", "webui/package-lock.release.json")
|
||||
git(self.repo, "commit", "-m", "Stage Core input before candidate lock resolution")
|
||||
return campaign, remote
|
||||
|
||||
|
||||
def git(cwd: Path, *args: str) -> None:
|
||||
result = subprocess.run(
|
||||
@@ -338,9 +518,50 @@ def add_scoped_workflow_manifest(repo: Path) -> None:
|
||||
backend.mkdir(parents=True)
|
||||
(package / "__init__.py").write_text("", encoding="utf-8")
|
||||
(backend / "__init__.py").write_text("", encoding="utf-8")
|
||||
# This small workspace still has to satisfy the real presentation contract.
|
||||
# Keep that prerequisite shared by both valid and intentionally unscoped
|
||||
# documentation fixtures, so each test reaches its intended release gate.
|
||||
canonical_areas = runpy.run_path(
|
||||
str(META_ROOT / "tools" / "checks" / "check-manifest-shapes.py")
|
||||
)["CANONICAL_PRODUCT_AREAS"]
|
||||
(backend / "release_fixture.py").write_text(
|
||||
"""from govoplan_core.core.modules import FrontendModule, ProductAreaContribution
|
||||
from govoplan_core.core.views import ViewSurface
|
||||
|
||||
|
||||
def fixture_frontend():
|
||||
return FrontendModule(
|
||||
module_id="access",
|
||||
view_surfaces=(
|
||||
ViewSurface(
|
||||
id="access.section.release-fixture",
|
||||
module_id="access",
|
||||
kind="section",
|
||||
label="Release fixture",
|
||||
),
|
||||
),
|
||||
product_areas=tuple(
|
||||
ProductAreaContribution(
|
||||
id=area_id,
|
||||
module_id="access",
|
||||
label=label,
|
||||
icon=icon,
|
||||
description=description,
|
||||
order=order,
|
||||
surface_ids=("access.section.release-fixture",),
|
||||
)
|
||||
for area_id, (label, icon, description, order) in CANONICAL_AREAS.items()
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
CANONICAL_AREAS = """ + repr(canonical_areas) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(backend / "manifest.py").write_text(
|
||||
"""from govoplan_core.core.modules import DocumentationCondition, DocumentationTopic, ModuleManifest, PermissionDefinition
|
||||
from govoplan_core.core.provider_governance import declared_module_architecture
|
||||
from .release_fixture import fixture_frontend
|
||||
|
||||
|
||||
def get_manifest():
|
||||
@@ -348,6 +569,7 @@ def get_manifest():
|
||||
id="access",
|
||||
name="Access",
|
||||
version="0.1.10",
|
||||
frontend=fixture_frontend(),
|
||||
permissions=(
|
||||
PermissionDefinition(
|
||||
scope="access:item:read",
|
||||
@@ -403,6 +625,7 @@ def replace_with_unscoped_workflow_manifest(repo: Path) -> None:
|
||||
manifest.write_text(
|
||||
"""from govoplan_core.core.modules import DocumentationTopic, ModuleManifest
|
||||
from govoplan_core.core.provider_governance import declared_module_architecture
|
||||
from .release_fixture import fixture_frontend
|
||||
|
||||
|
||||
def get_manifest():
|
||||
@@ -410,6 +633,7 @@ def get_manifest():
|
||||
id="access",
|
||||
name="Access",
|
||||
version="0.1.10",
|
||||
frontend=fixture_frontend(),
|
||||
documentation=(
|
||||
DocumentationTopic(
|
||||
id="access.workflow.unscoped",
|
||||
|
||||
@@ -13,12 +13,47 @@ if str(RELEASE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(RELEASE_ROOT))
|
||||
|
||||
from govoplan_release.version_metadata import ( # noqa: E402
|
||||
VersionMetadataError,
|
||||
apply_version_metadata_mutations,
|
||||
version_metadata_mutations,
|
||||
)
|
||||
|
||||
|
||||
class ReleaseVersionMetadataTests(unittest.TestCase):
|
||||
def test_updates_shared_module_version_without_rewriting_independent_interfaces(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
backend = root / "src" / "govoplan_example" / "backend"
|
||||
backend.mkdir(parents=True)
|
||||
manifest = backend / "manifest.py"
|
||||
manifest.write_text(
|
||||
'MODULE_VERSION: str = "1.2.3"\n'
|
||||
'manifest = ModuleManifest(id="example", version=MODULE_VERSION,\n'
|
||||
' provides_interfaces=(ModuleInterfaceProvider(name="api", version="2.0"),))\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
changed = apply_version_metadata_mutations(root, target_version="1.2.4")
|
||||
self.assertEqual(("src/govoplan_example/backend/manifest.py",), changed)
|
||||
self.assertIn('MODULE_VERSION: str = "1.2.4"', manifest.read_text())
|
||||
self.assertIn('version="2.0"', manifest.read_text())
|
||||
self.assertEqual((), version_metadata_mutations(root, target_version="1.2.4"))
|
||||
|
||||
def test_dynamic_module_version_fails_before_any_metadata_is_written(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
root = Path(temp_dir)
|
||||
backend = root / "src" / "govoplan_example" / "backend"
|
||||
backend.mkdir(parents=True)
|
||||
project = root / "pyproject.toml"
|
||||
project.write_text('[project]\nname="govoplan-example"\nversion="1.2.3"\n')
|
||||
before = project.read_bytes()
|
||||
(backend / "manifest.py").write_text(
|
||||
'MODULE_VERSION = compute_version()\n'
|
||||
'manifest = ModuleManifest(id="example", version=MODULE_VERSION)\n',
|
||||
)
|
||||
with self.assertRaisesRegex(VersionMetadataError, "no literal MODULE_VERSION"):
|
||||
apply_version_metadata_mutations(root, target_version="1.2.4")
|
||||
self.assertEqual(before, project.read_bytes())
|
||||
|
||||
def test_updates_recognized_metadata_without_changing_interface_versions(
|
||||
self,
|
||||
) -> None:
|
||||
|
||||
@@ -98,6 +98,10 @@ class SecurityAuditWrapperTests(unittest.TestCase):
|
||||
if [[ "${1:-}" == 'git' && "${2:-}" == '--help' ]]; then
|
||||
exit 0
|
||||
fi
|
||||
if [[ " $* " != *" --redact=100 "* ]]; then
|
||||
echo 'secret scans must redact reports and logs' >&2
|
||||
exit 3
|
||||
fi
|
||||
output=''
|
||||
while [[ $# -gt 0 ]]; do
|
||||
if [[ "$1" == '--report-path' ]]; then
|
||||
|
||||
@@ -39,6 +39,9 @@ GOVOPLAN_CORE_ROOT="$ROOT" PYTHON="$PYTHON" CHECK_TESTCLIENT_DEPRECATIONS=1 bash
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-contracts.py" --no-impact
|
||||
PYTHONDONTWRITEBYTECODE=1 "$PYTHON" "$META_ROOT/tools/checks/check-manifest-shapes.py" --require-architecture
|
||||
PYTHONDONTWRITEBYTECODE=1 "$PYTHON" "$META_ROOT/tools/checks/check-dsar-coverage.py"
|
||||
"$NODE/node" "$META_ROOT/tests/test-jsx-value-imports.mjs"
|
||||
"$NODE/node" "$META_ROOT/tools/checks/check-jsx-value-imports.mjs"
|
||||
"$NODE/node" "$META_ROOT/../govoplan-files/webui/scripts/test-archive-client.mjs"
|
||||
|
||||
cd "$META_ROOT"
|
||||
"$PYTHON" tools/inventory/platform-interface-inventory.py --strict-declarations --strict-endpoints
|
||||
@@ -96,6 +99,15 @@ PY
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-primitives.py"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-shared-webui-foundations.py"
|
||||
"$PYTHON" -m unittest tests.test_module_system
|
||||
"$PYTHON" -m unittest tests.test_ownership_history_migration tests.test_ownership tests.test_ownership_api
|
||||
"$PYTHON" -m unittest tests.test_navigation_preferences tests.test_api_smoke.ApiSmokeTests.test_navigation_separator_layout_survives_system_tenant_and_personal_saves
|
||||
"$PYTHON" -m pytest -q \
|
||||
/mnt/DATA/git/govoplan-files/tests/test_managed_archives.py \
|
||||
/mnt/DATA/git/govoplan-files/tests/test_archive_work.py \
|
||||
/mnt/DATA/git/govoplan-files/tests/test_archive_staging.py \
|
||||
/mnt/DATA/git/govoplan-files/tests/test_upload_response_batching.py \
|
||||
/mnt/DATA/git/govoplan-files/tests/test_archive_performance.py
|
||||
"$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-access/tests/test_external_function_mapping_migration.py
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-connectors/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-datasources/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-dataflow/tests
|
||||
@@ -116,13 +128,31 @@ PY
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-identity-trust/tests
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-encryption/tests
|
||||
"$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-wiki/tests
|
||||
"$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-campaign/tests/test_approval_gate.py
|
||||
"$PYTHON" -m pytest -q \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_approval_gate.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_editor_state_security.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_mail_profile_boundary.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_independent_configuration_repairs.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_incremental_review_persistence.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_reviewed_build_mock.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_delivery_policy_settings.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_synchronous_delivery_policy.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_workerless_recovery.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_imap_batch_integration.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_testbed_claim_recovery.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_campaign_optimistic_concurrency.py \
|
||||
/mnt/DATA/git/govoplan-campaign/tests/test_archive_encryption_governance.py \
|
||||
/mnt/DATA/git/govoplan-policy/tests/test_campaign_archive_encryption.py \
|
||||
/mnt/DATA/git/govoplan-policy/tests/test_archive_encryption_api.py
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-datasource-composition.py"
|
||||
"$PYTHON" "$META_ROOT/tools/checks/check-sanctions-screening-composition.py"
|
||||
"$PYTHON" -m pytest -q /mnt/DATA/git/govoplan-mail/tests/test_campaign_protocol_authorization.py /mnt/DATA/git/govoplan-mail/tests/test_campaign_imap_batch.py
|
||||
"$PYTHON" -m unittest discover -s /mnt/DATA/git/govoplan-mail/tests
|
||||
"$PYTHON" -m unittest tests.test_api_smoke.ApiSmokeTests.test_mailbox_message_listing_reports_total_count
|
||||
|
||||
cd "$ROOT/webui"
|
||||
"$NPM" run test:api-client-cache
|
||||
"$NPM" run test:dependency-security
|
||||
"$NPM" run test:layout-primitives
|
||||
"$NPM" run test:mail-components
|
||||
"$NPM" run test:module-capabilities
|
||||
@@ -155,12 +185,19 @@ cd /mnt/DATA/git/govoplan-postbox/webui
|
||||
cd /mnt/DATA/git/govoplan-mail/webui
|
||||
"$NPM" run test:mail-ui
|
||||
|
||||
cd /mnt/DATA/git/govoplan-files/webui
|
||||
"$NPM" run test:managed-archive
|
||||
|
||||
cd /mnt/DATA/git/govoplan-campaign/webui
|
||||
"$NPM" run test:policy-ui
|
||||
"$NPM" run test:template-preview
|
||||
"$NPM" run test:review-workflow
|
||||
"$NPM" run test:accessibility-contract
|
||||
"$NPM" run test:campaign-collaboration
|
||||
"$NPM" run test:campaign-work
|
||||
|
||||
cd /mnt/DATA/git/govoplan-policy/webui
|
||||
"$NPM" run test:archive-encryption
|
||||
|
||||
cd /mnt/DATA/git/govoplan-wiki/webui
|
||||
"$NPM" run test:interface-pattern
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
/** Reject erased type-only imports used as runtime JSX component tags. */
|
||||
import { readFileSync, readdirSync, existsSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { resolve, relative } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const workspaceRoot = resolve(import.meta.dirname, "../../..");
|
||||
const require = createRequire(resolve(workspaceRoot, "govoplan-core/webui/package.json"));
|
||||
const ts = require("typescript");
|
||||
|
||||
function isTypeOnlyImport(declaration) {
|
||||
if (ts.isImportSpecifier(declaration)) return declaration.isTypeOnly || declaration.parent.parent.isTypeOnly;
|
||||
if (ts.isNamespaceImport(declaration)) return declaration.parent.isTypeOnly;
|
||||
return (ts.isImportClause(declaration) || ts.isImportEqualsDeclaration(declaration)) && declaration.isTypeOnly;
|
||||
}
|
||||
|
||||
/** Resolve lexical bindings, including shadowing; do not typecheck unrelated
|
||||
* optional dependencies or report ordinary application diagnostics.
|
||||
*/
|
||||
export function findTypeOnlyJsxImports(sources) {
|
||||
const files = new Map(sources.map(({ path, source }) => [resolve(path),
|
||||
ts.createSourceFile(resolve(path), source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)]));
|
||||
const options = { noEmit: true, noResolve: true, noLib: true, types: [], jsx: ts.JsxEmit.Preserve };
|
||||
const host = ts.createCompilerHost(options);
|
||||
host.getSourceFile = (path) => files.get(resolve(path));
|
||||
const program = ts.createProgram([...files.keys()], options, host);
|
||||
const checker = program.getTypeChecker();
|
||||
const findings = [];
|
||||
for (const [path, source] of files) {
|
||||
function visit(node) {
|
||||
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
||||
let root = node.tagName;
|
||||
// Lower-case direct tags are intrinsic HTML, not runtime bindings.
|
||||
if (!(ts.isIdentifier(root) && /^[a-z]/.test(root.text))) {
|
||||
while (ts.isPropertyAccessExpression(root)) root = root.expression;
|
||||
const declarations = checker.getSymbolAtLocation(root)?.declarations ?? [];
|
||||
if (declarations.some(isTypeOnlyImport)) {
|
||||
const position = source.getLineAndCharacterOfPosition(node.tagName.getStart(source));
|
||||
findings.push({ path, line: position.line + 1, column: position.character + 1, component: node.tagName.getText(source) });
|
||||
}
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
visit(source);
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
function sourceFiles(directory) {
|
||||
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = resolve(directory, entry.name);
|
||||
return entry.isDirectory() ? sourceFiles(path) : entry.name.endsWith(".tsx") ? [path] : [];
|
||||
});
|
||||
}
|
||||
|
||||
export function checkWorkspace(root = workspaceRoot) {
|
||||
const modules = readdirSync(root, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && entry.name.startsWith("govoplan"))
|
||||
.map((entry) => resolve(root, entry.name, "webui/src")).filter(existsSync);
|
||||
const paths = modules.flatMap(sourceFiles);
|
||||
const findings = findTypeOnlyJsxImports(paths.map((path) => ({ path, source: readFileSync(path, "utf8") })));
|
||||
for (const finding of findings) {
|
||||
console.error(`${relative(root, finding.path)}:${finding.line}:${finding.column}: JSX component ${finding.component} is imported type-only and will be erased at runtime.`);
|
||||
}
|
||||
if (!findings.length) console.log(`JSX runtime-import contract passed: ${paths.length} TSX files across ${modules.length} WebUI modules.`);
|
||||
return findings.length ? 1 : 0;
|
||||
}
|
||||
|
||||
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||
process.exitCode = checkWorkspace();
|
||||
}
|
||||
@@ -677,6 +677,7 @@ run_gitleaks() {
|
||||
prepare_machine_report "$REPORTS_DIR/gitleaks-history-$name.json" || return 2
|
||||
prepare_machine_report "$REPORTS_DIR/gitleaks-worktree-$name.json" || return 2
|
||||
gitleaks git \
|
||||
--redact=100 \
|
||||
--config "$ROOT/.gitleaks.toml" \
|
||||
--report-format json \
|
||||
--report-path "$REPORTS_DIR/gitleaks-history-$name.json" \
|
||||
@@ -687,6 +688,7 @@ run_gitleaks() {
|
||||
# Scan the directory as well so pre-commit audits cover the exact code
|
||||
# under review, while retaining the history scan above.
|
||||
gitleaks dir \
|
||||
--redact=100 \
|
||||
--config "$ROOT/.gitleaks.toml" \
|
||||
--report-format json \
|
||||
--report-path "$REPORTS_DIR/gitleaks-worktree-$name.json" \
|
||||
@@ -696,6 +698,7 @@ run_gitleaks() {
|
||||
else
|
||||
prepare_machine_report "$REPORTS_DIR/gitleaks-$name.json" || return 2
|
||||
gitleaks detect \
|
||||
--redact=100 \
|
||||
--source "$repo" \
|
||||
--config "$ROOT/.gitleaks.toml" \
|
||||
--report-format json \
|
||||
|
||||
@@ -86,6 +86,12 @@ CENTRAL_COMPONENTS = {
|
||||
"CountBadge": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/CountBadge.tsx"
|
||||
),
|
||||
"MultiSelectFilter": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/MultiSelectFilter.tsx"
|
||||
),
|
||||
"ListSelectionFilter": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/ListSelectionFilter.tsx"
|
||||
),
|
||||
"SelectionList": pathlib.Path(
|
||||
"govoplan-core/webui/src/components/SelectionList.tsx"
|
||||
),
|
||||
@@ -190,7 +196,21 @@ REQUIRED_CONSUMERS = {
|
||||
"CountBadge": (
|
||||
pathlib.Path("govoplan-core/webui/src/layout/Titlebar.tsx"),
|
||||
pathlib.Path("govoplan-mail/webui/src/features/mail/MailboxPage.tsx"),
|
||||
),
|
||||
# Search's old count badge was part of a retired module-local filter menu.
|
||||
# Both surfaces must now compose the owning facet adapter and Core dropdown.
|
||||
"SearchFilters": (
|
||||
pathlib.Path("govoplan-search/webui/src/features/search/SearchPage.tsx"),
|
||||
pathlib.Path("govoplan-search/webui/src/components/GlobalSearch.tsx"),
|
||||
),
|
||||
"MultiSelectFilter": (
|
||||
pathlib.Path("govoplan-search/webui/src/components/SearchFilters.tsx"),
|
||||
pathlib.Path("govoplan-notifications/webui/src/features/notifications/NotificationCenterPage.tsx"),
|
||||
pathlib.Path("govoplan-docs/webui/src/features/docs/DocsPage.tsx"),
|
||||
),
|
||||
"ListSelectionFilter": (
|
||||
pathlib.Path("govoplan-core/webui/src/components/MultiSelectFilter.tsx"),
|
||||
pathlib.Path("govoplan-core/webui/src/components/table/DataGrid.tsx"),
|
||||
),
|
||||
"SelectionListItemContent": (
|
||||
pathlib.Path("govoplan-approvals/webui/src/features/approvals/ApprovalsPage.tsx"),
|
||||
|
||||
@@ -39,6 +39,9 @@ def tag_repositories(
|
||||
A release tag is only created for a clean, aligned, non-behind worktree.
|
||||
Both local and remote tags are resolved to commits before mutation so an
|
||||
existing immutable tag can never be moved by this operation.
|
||||
Module-only local candidate tags precede Core's release-lock regeneration;
|
||||
their cross-Core composition gate applies before publication, not creation.
|
||||
Core candidate tags still require a complete aligned release bundle.
|
||||
"""
|
||||
|
||||
workspace = resolve_workspace_root(workspace_root)
|
||||
@@ -47,13 +50,19 @@ def tag_repositories(
|
||||
selected = tuple(dict.fromkeys(repos))
|
||||
results: list[dict[str, object]] = []
|
||||
bundle_issues_by_repo: dict[str, list[str]] = {}
|
||||
for issue in selected_release_webui_bundle_issues(
|
||||
repo_versions={repo: repo_versions.get(repo, "") for repo in selected},
|
||||
workspace=workspace,
|
||||
):
|
||||
bundle_issues_by_repo.setdefault(issue.repo, []).append(
|
||||
f"{issue.source}={issue.actual!r}, expected {issue.expected!r} ({issue.message})"
|
||||
)
|
||||
# Core's final lock is generated from reviewed local module tags. Requiring
|
||||
# that lock before those tags exist makes the documented sequence circular.
|
||||
# This is only a local module staging exception: Core-selected batches and
|
||||
# every publication still run the cross-repository gate, and each selected
|
||||
# repository's own version/lock checks below are always enforced.
|
||||
if push or "govoplan-core" in selected:
|
||||
for issue in selected_release_webui_bundle_issues(
|
||||
repo_versions={repo: repo_versions.get(repo, "") for repo in selected},
|
||||
workspace=workspace,
|
||||
):
|
||||
bundle_issues_by_repo.setdefault(issue.repo, []).append(
|
||||
f"{issue.source}={issue.actual!r}, expected {issue.expected!r} ({issue.message})"
|
||||
)
|
||||
|
||||
if apply:
|
||||
preflight = tag_repositories(
|
||||
|
||||
@@ -269,6 +269,7 @@ def _render_python_version(
|
||||
except SyntaxError as exc:
|
||||
raise VersionMetadataError(f"Python metadata is malformed: {path.name}") from exc
|
||||
values: list[ast.Constant] = []
|
||||
module_version_references = 0
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Call) or _call_name(node.func) != target_name:
|
||||
continue
|
||||
@@ -279,6 +280,28 @@ def _render_python_version(
|
||||
and isinstance(keyword.value.value, str)
|
||||
):
|
||||
values.append(keyword.value)
|
||||
elif (
|
||||
keyword.arg == keyword_name
|
||||
and isinstance(keyword.value, ast.Name)
|
||||
and keyword.value.id == "MODULE_VERSION"
|
||||
):
|
||||
module_version_references += 1
|
||||
if module_version_references:
|
||||
if module_version_references != 1 or values:
|
||||
raise VersionMetadataError(
|
||||
f"Python metadata has multiple {target_name}.{keyword_name} values: {path.name}"
|
||||
)
|
||||
rendered, found = _render_python_assignment(
|
||||
payload,
|
||||
path=path,
|
||||
assignment_name="MODULE_VERSION",
|
||||
version=version,
|
||||
)
|
||||
if not found:
|
||||
raise VersionMetadataError(
|
||||
f"Python metadata has no literal MODULE_VERSION declaration: {path.name}"
|
||||
)
|
||||
return rendered, True
|
||||
if not values:
|
||||
return payload, False
|
||||
if len(values) != 1:
|
||||
|
||||
Reference in New Issue
Block a user