fix(release): prepare aligned 0.1.45 composition and guarded candidate sequencing
This commit is contained in:
@@ -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