From 79a9f9dbfcbb0c5503cb3df722e6d18c52a9abdc Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 18 Jul 2026 07:59:38 +0200 Subject: [PATCH] Prepare GeoIntel 1.0.0 release candidate --- AGENTS.md | 6 +- CHANGELOG.md | 11 + README.md | 25 +- VERSION | 1 + backend/README.md | 9 + backend/app/core/config.py | 5 +- backend/pyproject.toml | 4 +- backend/tests/test_rc11_release_package.py | 132 ++++++++ backend/tests/test_rc_release_evidence.py | 2 + .../tests/test_sprint177_mol_primary_focus.py | 9 +- deploy/unraid/Dockerfile.all-in-one | 5 +- deploy/unraid/README.md | 5 + deploy/unraid/deploy-release.sh | 19 +- docs/00-start/START_HERE.md | 44 +-- docs/API_CONTRACTS.md | 6 +- docs/API_EXAMPLE_RESPONSES.md | 2 +- docs/CODEX_BOOTSTRAP_PROMPT.md | 7 +- docs/HEALTHCHECK_CONTRACTS.md | 2 +- docs/PRODUCT_VISION.md | 19 +- docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md | 2 + docs/README.md | 16 +- docs/RELEASE_RUNBOOK.md | 182 +++++++++++ frontend/package-lock.json | 4 +- frontend/package.json | 2 +- scripts/build_release_package.py | 290 ++++++++++++++++++ scripts/capture_release_evidence.py | 3 + scripts/run_readiness_check.sh | 1 + scripts/validate_m14_launch_assets.py | 15 +- 28 files changed, 764 insertions(+), 64 deletions(-) create mode 100644 VERSION create mode 100644 backend/tests/test_rc11_release_package.py create mode 100644 docs/RELEASE_RUNBOOK.md create mode 100644 scripts/build_release_package.py diff --git a/AGENTS.md b/AGENTS.md index 72c9ff4e..d9450a2a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,12 +2,14 @@ ## Project identity -GeoIntel Kempen is a GeoAI Workbench, not a generic CRUD app and not a generic dashboard. +GeoIntel is a GeoAI Workbench for Belgium and the Belgian North Sea, not a +generic CRUD app and not a generic dashboard. Mol and the Kempen remain golden +regression areas, not the product boundary. ## Required behavior - Read `docs/CODEX_BOOTSTRAP_PROMPT.md` first. -- Respect `docs/V1_SCOPE_FREEZE.md`. +- Respect `docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md`. - Use `docs/API_CONTRACTS.md` as source of truth for endpoints. - Use `docs/DATABASE_IMPLEMENTATION_PLAN.md` as source of truth for persistence. - Use `docs/DEFINITION_OF_DONE.md` to decide whether work is complete. diff --git a/CHANGELOG.md b/CHANGELOG.md index 249cb4ca..42ee4a5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,17 @@ - Classified fixed NGI and RBINS national/maritime editions explicitly. The live source-freshness report now records three current sources, zero due sources and zero integrity issues. +- Assigned semantic release version `1.0.0-rc.1` consistently to backend + health, frontend package metadata and the OCI image version label. +- Added a fail-closed release-package builder that requires a clean tagged + revision, exact image revision, evidence inventory, SHA-256 checksums and a + verified detached SSH signature. +- Replaced stale Mol/Kempen product-boundary navigation with the active + Belgium/North Sea scope while retaining Mol and the Kempen as golden + regression areas. +- Added the final release runbook for immutable deployment, fresh install, + backup, isolated restore/upgrade, rollback, browser journeys, SBOM, + vulnerability policy, signed manifest and safe shutdown. - Added a read-only release-evidence manifest command with Git, migration, dependency, configuration checksum and optional live endpoint evidence. - Replaced the obsolete pre-build status with the current implemented diff --git a/README.md b/README.md index b2004496..c5893704 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ -# GeoIntel Kempen +# GeoIntel Belgium and the Belgian North Sea -GeoIntel Kempen is a GeoAI Workbench for the Belgian Kempen. It is designed as a portfolio-grade project combining GIS, remote sensing, raster/vector processing, computer vision, QA/QC and geospatial exports. +GeoIntel is a map-first GeoAI Workbench for Belgium and the Belgian North Sea. +It combines governed official-source coverage, raster/vector processing, +historical comparison, computer vision, QA/QC and geospatial exports. -The primary operating focus is Mol. New workbench contexts, AOIs and operator samples start there, while the broader Kempen remains fully supported for cross-area validation and regional interoperability. The live operator environment can provision a complete `Mol Municipality Workbench` with the official NIS `13025` municipality boundary and every intersecting GRB GBG building; smaller Mol zones remain analysis and validation contexts instead of the default municipal map. +Mol and the Kempen remain deep regression and model-validation references. The +release scope is all of Belgium plus legally labelled Belgian maritime zones; +source coverage remains explicit per theme and jurisdiction. GeoIntel is not a generic dashboard or chatbot. The core product is: @@ -10,17 +14,18 @@ GeoIntel is not a generic dashboard or chatbot. The core product is: ## Current milestone -**M14 — Build Launch Package** +**v1.0.0-rc.1 - Belgium/North Sea release candidate** -The canonical start point is now: +The canonical release controls are: -- `CODEX_START.md` - `docs/00-start/START_HERE.md` -- `docs/40-build-launch/SPRINT_1_SCOPE_FREEZE.md` -- `docs/40-build-launch/BUILD_SUCCESS_DEFINITION.md` -- `docs/40-build-launch/BUILD_ORDER_GRAPH.md` +- `docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md` +- `docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md` +- `docs/RELEASE_RUNBOOK.md` +- `docs/DEFINITION_OF_DONE.md` -Older M0-M13 handoff files are retained as historical preparation artifacts. The M14 build launch docs, M13 optimization docs, M12 final run-readiness docs, M11 governance docs and canonical specs take precedence. +Older milestone and sprint handoff files remain historical evidence. They do +not override the active national/maritime scope freeze or RC roadmap. ## Core V1 vertical slice diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..9c218192 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0-rc.1 diff --git a/backend/README.md b/backend/README.md index 5c76d8a8..e7ce614b 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1934,3 +1934,12 @@ evidence are protected by default. Apply mode requires an exact confirmation, an explicit candidate ceiling and a recent checksum-verified database plus SHA-256 storage backup mounted read-only under `/app/backups`. See `docs/DATA_OPERATIONS_RUNBOOK.md`. + +## Release candidate operations + +The semantic release version is stored in the repository `VERSION` file and +is exposed by health responses plus the OCI image version label. Fresh +install, checksum-verified backup, isolated restore/upgrade, rollback, +Belgium/North Sea browser journeys, SBOM, vulnerability evidence, SSH-signed +release manifest and final verification commands are defined in +`docs/RELEASE_RUNBOOK.md`. diff --git a/backend/app/core/config.py b/backend/app/core/config.py index b5c1d6e6..5ead79c2 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -11,7 +11,10 @@ class Settings(BaseSettings): ) app_env: str = Field(default="development", validation_alias="GEOINTEL_ENV") - app_version: str = Field(default="0.1.0") + app_version: str = Field( + default="1.0.0-rc.1", + validation_alias="GEOINTEL_APP_VERSION", + ) build_sha: str | None = Field(default=None, validation_alias="GEOINTEL_BUILD_SHA") build_time: str | None = Field(default=None, validation_alias="GEOINTEL_BUILD_TIME") api_prefix: str = Field(default="/api/v1", validation_alias="GEOINTEL_API_PREFIX") diff --git a/backend/pyproject.toml b/backend/pyproject.toml index db926864..6290d01d 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "geointel-backend" -version = "0.1.0" -description = "GeoIntel Kempen backend" +version = "1.0.0rc1" +description = "GeoIntel Belgium and Belgian North Sea backend" readme = "README.md" requires-python = ">=3.11" dependencies = [ diff --git a/backend/tests/test_rc11_release_package.py b/backend/tests/test_rc11_release_package.py new file mode 100644 index 00000000..fdab4c75 --- /dev/null +++ b/backend/tests/test_rc11_release_package.py @@ -0,0 +1,132 @@ +from __future__ import annotations + +import importlib.util +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + + +ROOT = Path(__file__).resolve().parents[2] +SCRIPT = ROOT / "scripts" / "build_release_package.py" + + +def load_script(): + spec = importlib.util.spec_from_file_location("build_release_package", SCRIPT) + assert spec is not None and spec.loader is not None + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def test_release_version_is_consistent_across_runtime_packages() -> None: + version = (ROOT / "VERSION").read_text(encoding="utf-8").strip() + config = (ROOT / "backend" / "app" / "core" / "config.py").read_text( + encoding="utf-8" + ) + pyproject = (ROOT / "backend" / "pyproject.toml").read_text(encoding="utf-8") + frontend = json.loads( + (ROOT / "frontend" / "package.json").read_text(encoding="utf-8") + ) + package_lock = json.loads( + (ROOT / "frontend" / "package-lock.json").read_text(encoding="utf-8") + ) + + assert version == "1.0.0-rc.1" + assert f'default="{version}"' in config + assert "GEOINTEL_APP_VERSION" in config + assert 'version = "1.0.0rc1"' in pyproject + assert frontend["version"] == version + assert package_lock["version"] == version + assert package_lock["packages"][""]["version"] == version + + +def test_release_image_carries_semantic_version_identity() -> None: + dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text( + encoding="utf-8" + ) + deploy = (ROOT / "deploy" / "unraid" / "deploy-release.sh").read_text( + encoding="utf-8" + ) + + assert "ARG GEOINTEL_APP_VERSION=1.0.0-rc.1" in dockerfile + assert 'org.opencontainers.image.version="${GEOINTEL_APP_VERSION}"' in dockerfile + assert "GEOINTEL_APP_VERSION=\"$(tr -d '[:space:]' < VERSION)\"" in deploy + assert "--build-arg GEOINTEL_APP_VERSION=" in deploy + assert "stored_version" in deploy + + +@pytest.mark.skipif(shutil.which("ssh-keygen") is None, reason="ssh-keygen unavailable") +def test_release_package_signature_and_checksums_fail_closed(tmp_path: Path) -> None: + module = load_script() + key = tmp_path / "release-key" + result = subprocess.run( + ["ssh-keygen", "-q", "-t", "ed25519", "-N", "", "-f", str(key)], + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, result.stderr + + package = tmp_path / "package" + package.mkdir() + evidence = package / "readiness.txt" + evidence.write_text("passed\n", encoding="utf-8") + identity = "geointel-release" + namespace = "geointel-release" + (package / module.SIGNERS_NAME).write_text( + f"{identity} {module.public_key(key)}\n", + encoding="utf-8", + ) + manifest = { + "schema_version": 1, + "release_id": "v1.0.0-rc.1", + "version": "1.0.0-rc.1", + "scope": "Belgium and the Belgian North Sea", + "signature": {"identity": identity, "namespace": namespace}, + "evidence": [ + { + "path": evidence.name, + "size_bytes": evidence.stat().st_size, + "sha256": module.sha256(evidence), + } + ], + } + manifest_path = package / module.MANIFEST_NAME + manifest_path.write_text(json.dumps(manifest) + "\n", encoding="utf-8") + module.run( + ( + "ssh-keygen", + "-Y", + "sign", + "-f", + str(key), + "-n", + namespace, + str(manifest_path), + ) + ) + module.write_checksums(package) + + verified = module.verify_package(package) + assert verified["release_id"] == "v1.0.0-rc.1" + + evidence.write_text("tampered\n", encoding="utf-8") + with pytest.raises(RuntimeError, match="Checksum mismatch"): + module.verify_package(package) + + +def test_release_package_cli_requires_tagged_clean_revision() -> None: + source = SCRIPT.read_text(encoding="utf-8") + readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text( + encoding="utf-8" + ) + + assert 'run(("git", "status", "--porcelain=v1"))' in source + assert 'run(("git", "rev-list", "-n", "1", release_id))' in source + assert "Image revision must equal the tagged Git commit" in source + assert "ssh-keygen" in source + assert "verify_checksums(package_dir)" in source + assert "py_compile scripts/build_release_package.py" in readiness diff --git a/backend/tests/test_rc_release_evidence.py b/backend/tests/test_rc_release_evidence.py index 28bc67c9..c9671de5 100644 --- a/backend/tests/test_rc_release_evidence.py +++ b/backend/tests/test_rc_release_evidence.py @@ -34,6 +34,7 @@ def test_release_evidence_manifest_is_secret_free_and_read_only(tmp_path: Path) assert manifest["schema_version"] == 1 assert manifest["release_id"] == "test-rc" + assert manifest["version"] == "1.0.0-rc.1" assert manifest["read_only"] is True assert manifest["scope"] == "Belgium and the Belgian North Sea" assert "DATABASE_URL" not in json.dumps(manifest).replace( @@ -72,6 +73,7 @@ def test_release_evidence_cli_writes_single_head_manifest(tmp_path: Path) -> Non assert payload["migrations"]["single_head"] is True assert payload["files"]["docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md"]["sha256"] assert payload["files"]["docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md"]["sha256"] + assert payload["files"]["docs/RELEASE_RUNBOOK.md"]["sha256"] def test_readiness_gate_compiles_release_evidence_command() -> None: diff --git a/backend/tests/test_sprint177_mol_primary_focus.py b/backend/tests/test_sprint177_mol_primary_focus.py index f9c85e4c..7bff66e8 100644 --- a/backend/tests/test_sprint177_mol_primary_focus.py +++ b/backend/tests/test_sprint177_mol_primary_focus.py @@ -49,10 +49,11 @@ def test_operator_workflows_put_mol_first_and_name_future_projects() -> None: assert 'QUALITY_SAMPLE_SLUG="${sample_slug}"' in multi_matrix -def test_product_docs_record_mol_primary_focus_without_dropping_kempen_scope() -> None: +def test_product_docs_record_national_scope_and_mol_regression_focus() -> None: readme = (ROOT / "README.md").read_text(encoding="utf-8") vision = (ROOT / "docs" / "PRODUCT_VISION.md").read_text(encoding="utf-8") - assert "primary operating focus is Mol" in readme - assert "Mol is de primaire operationele focus" in vision - assert "broader Kempen" in readme + assert "Belgium and the Belgian North Sea" in readme + assert "Mol and the Kempen remain deep regression" in readme + assert "Belgie en de Belgische Noordzee" in vision + assert "Mol en de Kempen blijven gouden regressiegebieden" in vision diff --git a/deploy/unraid/Dockerfile.all-in-one b/deploy/unraid/Dockerfile.all-in-one index 2e1d022e..fbaa2f86 100644 --- a/deploy/unraid/Dockerfile.all-in-one +++ b/deploy/unraid/Dockerfile.all-in-one @@ -164,12 +164,15 @@ RUN chmod +x /usr/local/bin/geointel-all-in-one-start /usr/local/bin/gosu \ ARG GEOINTEL_BUILD_SHA=unknown ARG GEOINTEL_BUILD_TIME=unknown +ARG GEOINTEL_APP_VERSION=1.0.0-rc.1 ENV GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}" \ - GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}" + GEOINTEL_BUILD_TIME="${GEOINTEL_BUILD_TIME}" \ + GEOINTEL_APP_VERSION="${GEOINTEL_APP_VERSION}" LABEL org.opencontainers.image.title="GeoIntel" \ org.opencontainers.image.description="GeoIntel workbench for Belgium and the Belgian North Sea" \ + org.opencontainers.image.version="${GEOINTEL_APP_VERSION}" \ org.opencontainers.image.revision="${GEOINTEL_BUILD_SHA}" \ org.opencontainers.image.created="${GEOINTEL_BUILD_TIME}" \ io.geointel.ai.enabled="${GEOINTEL_INSTALL_AI}" diff --git a/deploy/unraid/README.md b/deploy/unraid/README.md index 59f46b0a..fc3d1140 100644 --- a/deploy/unraid/README.md +++ b/deploy/unraid/README.md @@ -247,6 +247,11 @@ bash deploy/unraid/run-dockerman-container.sh ## Release identity, fresh install and rollback +The complete final-release order, including backup/restore, browser journeys, +SBOM, vulnerability policy, SSH-signed manifest and checksums, is in +`docs/RELEASE_RUNBOOK.md`. `VERSION` is the canonical semantic version and is +also written to the image's `org.opencontainers.image.version` label. + Inspect the running immutable revision and retained images: ```bash diff --git a/deploy/unraid/deploy-release.sh b/deploy/unraid/deploy-release.sh index b8fd436c..ed17cb7a 100644 --- a/deploy/unraid/deploy-release.sh +++ b/deploy/unraid/deploy-release.sh @@ -16,6 +16,11 @@ if [ -n "${DEPLOY_GEOINTEL_INSTALL_AI:-}" ]; then fi GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}" +GEOINTEL_APP_VERSION="$(tr -d '[:space:]' < VERSION)" +if ! [[ "$GEOINTEL_APP_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "Invalid semantic version in VERSION: ${GEOINTEL_APP_VERSION}" >&2 + exit 2 +fi GEOINTEL_BUILD_SHA="$(git rev-parse HEAD)" GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)" GEOINTEL_IMAGE_REPOSITORY="${GEOINTEL_IMAGE_REPOSITORY:-geointel-all-in-one}" @@ -90,7 +95,16 @@ if docker image inspect "$GEOINTEL_RELEASE_IMAGE" >/dev/null 2>&1; then --format '{{index .Config.Labels "io.geointel.ai.enabled"}}' \ "$GEOINTEL_RELEASE_IMAGE" )" - if [ "$stored_revision" != "$GEOINTEL_BUILD_SHA" ] || [ "$stored_ai" != "$GEOINTEL_INSTALL_AI" ]; then + stored_version="$( + docker image inspect \ + --format '{{index .Config.Labels "org.opencontainers.image.version"}}' \ + "$GEOINTEL_RELEASE_IMAGE" + )" + if ( + [ "$stored_revision" != "$GEOINTEL_BUILD_SHA" ] || + [ "$stored_ai" != "$GEOINTEL_INSTALL_AI" ] || + [ "$stored_version" != "$GEOINTEL_APP_VERSION" ] + ); then echo "Immutable release tag has conflicting metadata: ${GEOINTEL_RELEASE_IMAGE}" >&2 exit 2 fi @@ -101,6 +115,7 @@ else --build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \ --build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \ --build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \ + --build-arg GEOINTEL_APP_VERSION="$GEOINTEL_APP_VERSION" \ -f deploy/unraid/Dockerfile.all-in-one \ -t "$GEOINTEL_RELEASE_IMAGE" \ -t "${GEOINTEL_IMAGE_REPOSITORY}:latest" \ @@ -128,5 +143,5 @@ fi echo "Deployed immutable image ${GEOINTEL_RELEASE_IMAGE}." docker image inspect \ - --format 'revision={{index .Config.Labels "org.opencontainers.image.revision"}} created={{index .Config.Labels "org.opencontainers.image.created"}}' \ + --format 'version={{index .Config.Labels "org.opencontainers.image.version"}} revision={{index .Config.Labels "org.opencontainers.image.revision"}} created={{index .Config.Labels "org.opencontainers.image.created"}}' \ "$GEOINTEL_RELEASE_IMAGE" diff --git a/docs/00-start/START_HERE.md b/docs/00-start/START_HERE.md index 6603df23..1e98e4fc 100644 --- a/docs/00-start/START_HERE.md +++ b/docs/00-start/START_HERE.md @@ -5,13 +5,18 @@ Older handoff files are historical. If documents conflict, follow the precedence ## Current milestone -**M14 — Build Launch Package** +**v1.0.0-rc.1 - Belgium/North Sea release candidate** -The repository is no longer only a documentation bundle. It is now a specification-controlled engineering repo for building GeoIntel Kempen as a GeoAI Workbench. +The implementation is in final release-candidate acceptance for Belgium and +the Belgian North Sea. Mol and the Kempen remain golden regression areas, not +the product boundary. ## Product one-liner -GeoIntel Kempen is a GeoAI Workbench for the Belgian Kempen that processes raster data, vector data and AI outputs into geospatially correct detections, segmentations, QA/QC metrics and exports. +GeoIntel is a map-first GeoAI Workbench for Belgium and the Belgian North Sea +that processes governed raster data, vector data and AI outputs into +geospatially correct analysis, detections, segmentations, QA/QC metrics and +exports. ## Non-negotiable product identity @@ -35,21 +40,18 @@ GeoIntel is not primarily: Read these files in order before coding: 1. `docs/00-start/START_HERE.md` -2. `docs/governance/GEOINTEL_CONSTITUTION.md` -3. `docs/governance/ARCHITECTURE_INVARIANTS.md` -4. `docs/governance/FORBIDDEN_DECISIONS.md` -5. `docs/governance/DECISION_PRECEDENCE.md` -6. `docs/specs/CANONICAL_DOMAIN_MODELS.md` -7. `docs/specs/GIS_STANDARDS.md` -8. `docs/specs/RASTER_STANDARDS.md` -9. `docs/specs/STATE_MACHINES.md` -10. `docs/workflows/GOLDEN_PATHS.md` -11. `docs/build/BUILD_ORDER_DEPENDENCY_GRAPH.md` -12. `docs/build/CODEX_OPERATING_SYSTEM.md` -13. `docs/40-build-launch/SPRINT_1_SCOPE_FREEZE.md` -14. `docs/40-build-launch/BUILD_SUCCESS_DEFINITION.md` -15. `docs/40-build-launch/CODEX_STOP_RULES.md` -16. `prompts/codex/m14/CODEX_FIRST_DAY_MASTER_PROMPT.md` +2. `docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md` +3. `docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md` +4. `docs/governance/GEOINTEL_CONSTITUTION.md` +5. `docs/governance/ARCHITECTURE_INVARIANTS.md` +6. `docs/governance/FORBIDDEN_DECISIONS.md` +7. `docs/API_CONTRACTS.md` +8. `docs/DATABASE_IMPLEMENTATION_PLAN.md` +9. `docs/DATA_SPECIFICATION.md` +10. `docs/DATA_SOURCES.md` +11. `docs/STORAGE_ARCHITECTURE.md` +12. `docs/DEFINITION_OF_DONE.md` +13. `docs/RELEASE_RUNBOOK.md` ## Canonical first implementation target @@ -99,15 +101,15 @@ Codex may not change: ## Conflict resolution -If any older document conflicts with this M14 launch layer, follow this order: +If any older document conflicts with the active RC layer, follow this order: 1. Constitution and architecture invariants. 2. Forbidden decisions. 3. State machines and canonical models. 4. API/database contracts. 5. Build order dependency graph. -6. M14 build-launch docs for first-run scope and stop rules. -7. Older milestone handoff documents. +6. Belgium/North Sea scope freeze and RC roadmap. +7. Older milestone and sprint handoff documents. ## Required pass ending diff --git a/docs/API_CONTRACTS.md b/docs/API_CONTRACTS.md index ac7d0be8..57b193ef 100644 --- a/docs/API_CONTRACTS.md +++ b/docs/API_CONTRACTS.md @@ -55,7 +55,7 @@ Returns process liveness only. It never queries PostgreSQL. { "status": "ok", "service": "geointel-backend", - "version": "0.1.0", + "version": "1.0.0-rc.1", "build_sha": null, "build_time": null } @@ -75,7 +75,7 @@ degraded. Docker uses `/health/ready`. { "status": "ok", "service": "geointel-backend", - "version": "0.1.0", + "version": "1.0.0-rc.1", "database": "ok", "postgis": "ok:3.x", "migration": "ok:202607160001", @@ -105,7 +105,7 @@ envelope. PostGIS and configured YOLO state are derived at runtime. "sam": false, "grb": "bounded", "sentinel": "planned", - "version": "0.1.0", + "version": "1.0.0-rc.1", "build_sha": null, "providers": [] } diff --git a/docs/API_EXAMPLE_RESPONSES.md b/docs/API_EXAMPLE_RESPONSES.md index a71fe1a6..4b19ad2a 100644 --- a/docs/API_EXAMPLE_RESPONSES.md +++ b/docs/API_EXAMPLE_RESPONSES.md @@ -21,7 +21,7 @@ "redis": "ok", "storage": "ok" }, - "version": "0.1.0" + "version": "1.0.0-rc.1" } ``` diff --git a/docs/CODEX_BOOTSTRAP_PROMPT.md b/docs/CODEX_BOOTSTRAP_PROMPT.md index 343cd1ef..d0fee4a6 100644 --- a/docs/CODEX_BOOTSTRAP_PROMPT.md +++ b/docs/CODEX_BOOTSTRAP_PROMPT.md @@ -1,6 +1,9 @@ -# Codex Bootstrap Prompt — GeoIntel Kempen +# Codex Bootstrap Prompt - GeoIntel Belgium and the Belgian North Sea -You are building GeoIntel Kempen, a GeoAI Workbench for the Belgian Kempen region. The repository already contains the specification set. Read these documents before editing code: +You are building GeoIntel, a GeoAI Workbench for Belgium and the Belgian North +Sea. Mol and the Kempen are golden regression areas, not the product boundary. +The repository already contains the specification set. Read these documents +before editing code: 1. `docs/SPECIFICATION_FREEZE_M0.md` 2. `docs/V1_SCOPE_FREEZE.md` diff --git a/docs/HEALTHCHECK_CONTRACTS.md b/docs/HEALTHCHECK_CONTRACTS.md index da33574d..f4a0461f 100644 --- a/docs/HEALTHCHECK_CONTRACTS.md +++ b/docs/HEALTHCHECK_CONTRACTS.md @@ -20,7 +20,7 @@ backward-compatible alias. { "status": "ok", "service": "geointel-backend", - "version": "0.1.0", + "version": "1.0.0-rc.1", "database": "ok", "postgis": "ok:3.x", "migration": "ok:202607160001", diff --git a/docs/PRODUCT_VISION.md b/docs/PRODUCT_VISION.md index 4f205017..2eeed3bf 100644 --- a/docs/PRODUCT_VISION.md +++ b/docs/PRODUCT_VISION.md @@ -1,6 +1,7 @@ # Product Vision -GeoIntel Kempen bestaat om open en lokale geospatiale data om te zetten in bruikbare, controleerbare GeoAI-resultaten. +GeoIntel bestaat om open en officiele geospatiale data voor Belgie en de +Belgische Noordzee om te zetten in bruikbare, controleerbare GeoAI-resultaten. ## Geen klassieke GIS-viewer @@ -28,19 +29,27 @@ GeoIntel moet bewijzen dat de ontwikkelaar de volledige keten begrijpt: 8. kwaliteit meten 9. resultaten exporteren -## Regionale identiteit +## Geografische identiteit -De Kempen vormen de afgebakende regio. Dit maakt het project concreet, realistisch en demo-baar. Voorbeelden: +De productscope omvat heel Belgie en de juridisch correct gelabelde Belgische +maritieme zones. De dekking is een federatie van nationale, Vlaamse, Waalse, +Brusselse en maritieme broncontracten; ontbrekende of gedeeltelijke dekking +blijft zichtbaar. Voorbeelden: - gebouwdetectie en infrastructuuranalyse in Mol - vergelijkende validatie in Geel - verstedelijking rond Turnhout - natuurfragmentatie rond Kasterlee of Retie -Mol is de primaire operationele focus. De standaardkaart, nieuwe AOI-context en operator-volgorde vertrekken daarom vanuit Mol. De bredere Kempen blijft bewust onderdeel van de productscope voor onafhankelijke validatie, overdraagbaarheid en regionale vergelijking. +Mol en de Kempen blijven gouden regressiegebieden voor diepgaande validatie, +modelkwaliteit en historische vergelijkbaarheid. Ze zijn niet langer de +productgrens. Nieuwe kaartselecties kunnen overal binnen Belgie en de +Belgische Noordzee liggen en behouden hun eigen bronautoriteit en beperkingen. ## Richting De definitieve richting is: -> GeoIntel Kempen is een GeoAI Workbench waarmee je open geodata, luchtfoto's en satellietbeelden verwerkt tot detecties, segmentaties, veranderingen en controleerbare GIS-lagen. +> GeoIntel is een GeoAI Workbench voor Belgie en de Belgische Noordzee waarmee +> je open geodata, luchtfoto's en satellietbeelden verwerkt tot analyse, +> detecties, segmentaties, veranderingen en controleerbare GIS-lagen. diff --git a/docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md b/docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md index 2312bc7d..ef198a49 100644 --- a/docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md +++ b/docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md @@ -534,6 +534,8 @@ maritime freshness evidence classifies all three fixed official editions as ## RC-11 - Final release package +**State: in progress.** + ### Work - rerun fresh-install and upgrade proof using the release image; diff --git a/docs/README.md b/docs/README.md index 42d8fc93..b6feccca 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,16 @@ # GeoIntel Docs Index -Start here when preparing an implementation pass. +Start with these active release controls: + +- `00-start/START_HERE.md` +- `RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md` +- `RC_ROADMAP_BELGIUM_NORTH_SEA.md` +- `RELEASE_RUNBOOK.md` +- `DEFINITION_OF_DONE.md` + +The milestone and sprint documents below are retained as historical design and +implementation evidence. `TODO.md` is an implementation archive, not the +active release board. ## Product foundation - PRODUCT_VISION.md @@ -37,13 +47,13 @@ Start here when preparing an implementation pass. - UI_PAGE_SPECIFICATIONS.md - COMPONENT_BREAKDOWN.md -## Execution +## Historical execution - CODEX_MASTER_PROMPT.md - CODEX_EXECUTION_PLAN.md - CODEX_BUILD_PLAN.md - CODEX_EXECUTION_LOG.md - ACCEPTANCE_CRITERIA.md -- TODO.md +- TODO.md (historical implementation archive) - IMPLEMENTATION_BACKLOG.md - DEVELOPMENT_RULES.md - AGENTS.md diff --git a/docs/RELEASE_RUNBOOK.md b/docs/RELEASE_RUNBOOK.md new file mode 100644 index 00000000..edf4d3e7 --- /dev/null +++ b/docs/RELEASE_RUNBOOK.md @@ -0,0 +1,182 @@ +# GeoIntel Release Runbook + +## Scope + +This runbook releases GeoIntel for Belgium and the Belgian North Sea. Mol and +the Kempen remain regression references. A successful release never implies +that every theme is operational in every jurisdiction; the coverage API and +source provenance remain authoritative. + +The repository version is stored in `VERSION`. The current release candidate +is `v1.0.0-rc.1`. + +## Mandatory preconditions + +- clean `main` worktree at the commit being released; +- secure non-default PostGIS password in the Tower `.env`; +- existing local AI model only when the AI image is enabled; +- recent checksum-verified backup with SHA-256 storage inventory; +- Docker, `ssh-keygen`, Python 3.11, Node 20 and Bash available; +- one Alembic head and no unsupported metric represented as successful. + +No command in this runbook downloads AI weights or implicitly deletes +application data. + +## Repository gate + +```bash +python -m compileall backend/app +cd backend && python -m pytest +cd ../frontend && npm run test:unit && npm run typecheck && npm run build +cd .. +bash scripts/run_readiness_check.sh +cd backend && python -m alembic heads +python -m alembic upgrade head --sql +cd .. +bash -n scripts/live_migration_smoke.sh +docker compose config +``` + +## Immutable deployment + +On the Codex workstation: + +```powershell +.\scripts\deploy_tower.ps1 +``` + +On Tower: + +```bash +cd /mnt/user/appdata/geointel +docker inspect --format \ + '{{index .Config.Labels "org.opencontainers.image.version"}} {{index .Config.Labels "org.opencontainers.image.revision"}}' \ + geointel +curl -fsS http://127.0.0.1:1202/health/ready +bash scripts/live_migration_smoke.sh +``` + +## Backup and recovery proof + +Create an immutable backup. The SHA-256 inventory can take several minutes on +large storage: + +```bash +bash scripts/backup_release_state.sh \ + --container geointel \ + --output-root /mnt/user/appdata/geointel/backups \ + --release-id v1.0.0-rc.1 \ + --storage-path /mnt/user/appdata/geointel/storage \ + --models-path /mnt/user/appdata/geointel/models \ + --inventory-mode sha256 +``` + +Verify and restore only into an automatically generated temporary database: + +```bash +bash scripts/verify_release_backup.sh \ + --backup-dir /mnt/user/appdata/geointel/backups/v1.0.0-rc.1 \ + --container geointel +bash scripts/restore_release_backup_smoke.sh \ + --backup-dir /mnt/user/appdata/geointel/backups/v1.0.0-rc.1 \ + --container geointel \ + --confirm-isolated-restore +bash scripts/verify_release_upgrade_smoke.sh \ + --backup-dir /mnt/user/appdata/geointel/backups/v1.0.0-rc.1 \ + --container geointel \ + --confirm-isolated-upgrade +``` + +The restore and upgrade scripts refuse the production database name and remove +their generated verification database. + +## Fresh install, browser and data operations + +```bash +bash scripts/verify_release_fresh_install.sh \ + geointel-all-in-one:-ai +bash scripts/run_rc8_release_journeys.sh \ + http://127.0.0.1:1202 artifacts/releases/v1.0.0-rc.1/rc8 +bash scripts/run_rc9_ux_audit.sh \ + http://127.0.0.1:1202 artifacts/releases/v1.0.0-rc.1/rc9 +bash scripts/run_rc10_data_operations_audit.sh \ + artifacts/releases/v1.0.0-rc.1/rc10 +``` + +The RC10 command is read-only and runs cleanup in dry-run mode only. + +## Supply-chain evidence + +```bash +bash scripts/audit_python_dependencies.sh +cd frontend && npm audit --audit-level=high +cd .. +bash scripts/generate_container_sbom.sh \ + geointel-all-in-one:-ai \ + artifacts/releases/v1.0.0-rc.1/geointel-sbom.spdx.json +bash scripts/scan_container_image.sh \ + geointel-all-in-one:-ai \ + artifacts/releases/v1.0.0-rc.1/container-vulnerabilities.json +``` + +The complete vulnerability report remains evidence. The executable policy +gate fails on reachable fixed HIGH/CRITICAL findings. + +## Rollback proof + +The rollback command reuses persistent paths and never downgrades Alembic: + +```bash +bash deploy/unraid/rollback-dockerman-container.sh +curl -fsS http://127.0.0.1:1202/health/ready +bash deploy/unraid/deploy-release.sh +curl -fsS http://127.0.0.1:1202/health/ready +``` + +For a future backward-incompatible migration, restore the verified pre-release +backup instead of running an older image against a newer schema. + +## Tag and signed package + +Create an SSH-signed Git tag at the accepted clean commit. Use a configured +release key; never add the private key to the repository: + +```bash +git -c gpg.format=ssh \ + -c user.signingkey=/secure/path/release-key \ + tag -s v1.0.0-rc.1 -m "GeoIntel v1.0.0-rc.1" +git push origin v1.0.0-rc.1 +``` + +Place the collected evidence files in an ignored package directory and create +the detached SSH signature plus complete checksum inventory: + +```bash +python scripts/build_release_package.py build \ + --output-dir artifacts/releases/v1.0.0-rc.1 \ + --release-id v1.0.0-rc.1 \ + --image-name geointel-all-in-one:-ai \ + --image-id sha256: \ + --image-revision \ + --signing-key /secure/path/release-key +python scripts/build_release_package.py verify \ + --package-dir artifacts/releases/v1.0.0-rc.1 +``` + +The builder refuses a dirty worktree, a tag not pointing at `HEAD`, a mismatched +image revision, missing evidence, symlinks, checksum drift or an invalid +signature. + +## Safe cleanup and shutdown + +Stop only the temporary smoke container by its generated name; normal scripts +already clean it automatically. To stop GeoIntel without deleting persistent +data: + +```bash +docker stop geointel +``` + +Do not use broad Docker volume pruning. Application cleanup remains dry-run by +default and requires the exact confirmation, recent backup and delete ceiling +documented in `DATA_OPERATIONS_RUNBOOK.md`. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 808ea6fd..e1941a10 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "geointel-frontend", - "version": "0.1.0", + "version": "1.0.0-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "geointel-frontend", - "version": "0.1.0", + "version": "1.0.0-rc.1", "dependencies": { "maplibre-gl": "^4.7.1", "react": "^18.2.0", diff --git a/frontend/package.json b/frontend/package.json index beca3ea5..ca8fa326 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "geointel-frontend", "private": true, - "version": "0.1.0", + "version": "1.0.0-rc.1", "type": "module", "scripts": { "start": "vite", diff --git a/scripts/build_release_package.py b/scripts/build_release_package.py new file mode 100644 index 00000000..fe6e3b30 --- /dev/null +++ b/scripts/build_release_package.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +"""Build or verify a checksummed and SSH-signed GeoIntel release package.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import re +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Sequence + + +ROOT = Path(__file__).resolve().parents[1] +MANIFEST_NAME = "release-manifest.json" +SIGNATURE_NAME = f"{MANIFEST_NAME}.sig" +SIGNERS_NAME = "allowed_signers" +CHECKSUMS_NAME = "CHECKSUMS.sha256" +GENERATED_NAMES = {MANIFEST_NAME, SIGNATURE_NAME, SIGNERS_NAME, CHECKSUMS_NAME} +RELEASE_ID_RE = re.compile(r"^v[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$") + + +def run( + command: Sequence[str], + *, + input_bytes: bytes | None = None, + cwd: Path = ROOT, +) -> subprocess.CompletedProcess[bytes]: + result = subprocess.run( + list(command), + cwd=cwd, + input=input_bytes, + capture_output=True, + check=False, + ) + if result.returncode != 0: + stderr = result.stderr.decode("utf-8", errors="replace").strip() + raise RuntimeError(f"{' '.join(command)} failed: {stderr}") + return result + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def package_files(package_dir: Path, *, include_generated: bool) -> list[Path]: + result: list[Path] = [] + for path in sorted(package_dir.rglob("*"), key=lambda item: item.as_posix()): + if path.is_symlink(): + raise RuntimeError(f"Release packages may not contain symlinks: {path}") + if not path.is_file(): + continue + relative = path.relative_to(package_dir) + if not include_generated and relative.as_posix() in GENERATED_NAMES: + continue + result.append(path) + return result + + +def public_key(signing_key: Path) -> str: + public_path = Path(f"{signing_key}.pub") + if public_path.is_file(): + value = public_path.read_text(encoding="utf-8").strip() + else: + value = run(("ssh-keygen", "-y", "-f", str(signing_key))).stdout.decode( + "utf-8" + ).strip() + if not value.startswith(("ssh-ed25519 ", "ssh-rsa ", "ecdsa-sha2-")): + raise RuntimeError("Unsupported or invalid SSH public signing key") + return " ".join(value.split()[:2]) + + +def write_checksums(package_dir: Path) -> None: + lines = [] + for path in package_files(package_dir, include_generated=True): + relative = path.relative_to(package_dir).as_posix() + if relative == CHECKSUMS_NAME: + continue + lines.append(f"{sha256(path)} {relative}") + (package_dir / CHECKSUMS_NAME).write_text( + "\n".join(lines) + "\n", + encoding="utf-8", + newline="\n", + ) + + +def verify_checksums(package_dir: Path) -> None: + checksum_path = package_dir / CHECKSUMS_NAME + if not checksum_path.is_file(): + raise RuntimeError(f"Missing {CHECKSUMS_NAME}") + expected_paths: set[str] = set() + for line in checksum_path.read_text(encoding="utf-8").splitlines(): + digest, separator, relative = line.partition(" ") + if not separator or not re.fullmatch(r"[0-9a-f]{64}", digest): + raise RuntimeError(f"Invalid checksum line: {line!r}") + candidate = (package_dir / relative).resolve() + try: + candidate.relative_to(package_dir) + except ValueError as exc: + raise RuntimeError(f"Checksum path escapes package: {relative}") from exc + if not candidate.is_file() or candidate.is_symlink(): + raise RuntimeError(f"Checksummed release file is unavailable: {relative}") + if sha256(candidate) != digest: + raise RuntimeError(f"Checksum mismatch: {relative}") + expected_paths.add(relative) + actual_paths = { + path.relative_to(package_dir).as_posix() + for path in package_files(package_dir, include_generated=True) + if path.name != CHECKSUMS_NAME + } + if expected_paths != actual_paths: + raise RuntimeError( + "Checksum inventory differs from package contents: " + f"missing={sorted(actual_paths - expected_paths)}, " + f"unexpected={sorted(expected_paths - actual_paths)}" + ) + + +def verify_package(package_dir: Path) -> dict[str, object]: + package_dir = package_dir.expanduser().resolve() + verify_checksums(package_dir) + manifest_path = package_dir / MANIFEST_NAME + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + identity = str(manifest["signature"]["identity"]) + namespace = str(manifest["signature"]["namespace"]) + run( + ( + "ssh-keygen", + "-Y", + "verify", + "-f", + str(package_dir / SIGNERS_NAME), + "-I", + identity, + "-n", + namespace, + "-s", + str(package_dir / SIGNATURE_NAME), + ), + input_bytes=manifest_path.read_bytes(), + ) + if manifest.get("scope") != "Belgium and the Belgian North Sea": + raise RuntimeError("Unexpected release scope") + if manifest.get("release_id") != f"v{manifest.get('version')}": + raise RuntimeError("Release id and semantic version differ") + return manifest + + +def build_package(args: argparse.Namespace) -> dict[str, object]: + package_dir = args.output_dir.expanduser().resolve() + package_dir.mkdir(parents=True, exist_ok=True) + version = (ROOT / "VERSION").read_text(encoding="utf-8").strip() + release_id = args.release_id or f"v{version}" + if not RELEASE_ID_RE.fullmatch(release_id) or release_id != f"v{version}": + raise RuntimeError("Release id must equal v and be valid SemVer") + + commit = run(("git", "rev-parse", "HEAD")).stdout.decode().strip() + dirty = run(("git", "status", "--porcelain=v1")).stdout.decode().strip() + if dirty: + raise RuntimeError("Release package requires a clean Git worktree") + tag_commit = run(("git", "rev-list", "-n", "1", release_id)).stdout.decode().strip() + if tag_commit != commit: + raise RuntimeError(f"Tag {release_id} does not point to HEAD") + if args.image_revision != commit: + raise RuntimeError("Image revision must equal the tagged Git commit") + + evidence = [] + for path in package_files(package_dir, include_generated=False): + evidence.append( + { + "path": path.relative_to(package_dir).as_posix(), + "size_bytes": path.stat().st_size, + "sha256": sha256(path), + } + ) + if not evidence: + raise RuntimeError("At least one release evidence file is required") + + signing_key = args.signing_key.expanduser().resolve() + if not signing_key.is_file(): + raise RuntimeError(f"SSH signing key is unavailable: {signing_key}") + identity = args.identity + namespace = "geointel-release" + (package_dir / SIGNERS_NAME).write_text( + f"{identity} {public_key(signing_key)}\n", + encoding="utf-8", + newline="\n", + ) + manifest: dict[str, object] = { + "schema_version": 1, + "release_id": release_id, + "version": version, + "scope": "Belgium and the Belgian North Sea", + "created_at": datetime.now(timezone.utc).isoformat(), + "git": { + "commit": commit, + "tag": release_id, + "clean": True, + }, + "image": { + "name": args.image_name, + "id": args.image_id, + "revision": args.image_revision, + }, + "signature": { + "algorithm": "SSH", + "identity": identity, + "namespace": namespace, + }, + "evidence": evidence, + } + manifest_path = package_dir / MANIFEST_NAME + manifest_path.write_text( + json.dumps(manifest, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + newline="\n", + ) + signature_path = package_dir / SIGNATURE_NAME + if signature_path.exists(): + signature_path.unlink() + run( + ( + "ssh-keygen", + "-Y", + "sign", + "-f", + str(signing_key), + "-n", + namespace, + str(manifest_path), + ) + ) + if not signature_path.is_file(): + raise RuntimeError("ssh-keygen did not create the detached signature") + write_checksums(package_dir) + return verify_package(package_dir) + + +def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + subparsers = parser.add_subparsers(dest="command", required=True) + build = subparsers.add_parser("build") + build.add_argument("--output-dir", type=Path, required=True) + build.add_argument("--release-id") + build.add_argument("--image-name", required=True) + build.add_argument("--image-id", required=True) + build.add_argument("--image-revision", required=True) + build.add_argument( + "--signing-key", + type=Path, + default=os.environ.get("GEOINTEL_RELEASE_SIGNING_KEY"), + required=not bool(os.environ.get("GEOINTEL_RELEASE_SIGNING_KEY")), + ) + build.add_argument("--identity", default="geointel-release") + verify = subparsers.add_parser("verify") + verify.add_argument("--package-dir", type=Path, required=True) + return parser.parse_args(argv) + + +def main(argv: Sequence[str] | None = None) -> int: + args = parse_args(argv) + try: + if args.command == "build": + manifest = build_package(args) + print( + f"Built and verified signed release package " + f"{manifest['release_id']} at {args.output_dir.resolve()}" + ) + else: + manifest = verify_package(args.package_dir) + print( + f"Verified signed release package " + f"{manifest['release_id']} at {args.package_dir.resolve()}" + ) + except (OSError, KeyError, ValueError, RuntimeError, json.JSONDecodeError) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/capture_release_evidence.py b/scripts/capture_release_evidence.py index 44d81371..b7fca38e 100644 --- a/scripts/capture_release_evidence.py +++ b/scripts/capture_release_evidence.py @@ -21,6 +21,7 @@ from typing import Any, Sequence ROOT = Path(__file__).resolve().parents[1] BACKEND = ROOT / "backend" DEFAULT_HASHED_FILES = ( + "VERSION", "AGENTS.md", "backend/pyproject.toml", "backend/alembic.ini", @@ -31,6 +32,7 @@ DEFAULT_HASHED_FILES = ( "deploy/unraid/Dockerfile.all-in-one", "docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md", "docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md", + "docs/RELEASE_RUNBOOK.md", ) DEPENDENCIES = ( "alembic", @@ -227,6 +229,7 @@ def build_manifest(args: argparse.Namespace) -> dict[str, Any]: return { "schema_version": 1, "release_id": args.release_id, + "version": (ROOT / "VERSION").read_text(encoding="utf-8").strip(), "captured_at": utc_now(), "read_only": True, "scope": "Belgium and the Belgian North Sea", diff --git a/scripts/run_readiness_check.sh b/scripts/run_readiness_check.sh index 842b656a..b5064582 100755 --- a/scripts/run_readiness_check.sh +++ b/scripts/run_readiness_check.sh @@ -40,6 +40,7 @@ fi echo "== GeoIntel run readiness check ==" "$PYTHON_BIN" -m py_compile scripts/capture_release_evidence.py +"$PYTHON_BIN" -m py_compile scripts/build_release_package.py "$PYTHON_BIN" -m py_compile scripts/verify_python_lock.py "$PYTHON_BIN" scripts/verify_python_lock.py "$PYTHON_BIN" -m py_compile scripts/verify_security_exceptions.py diff --git a/scripts/validate_m14_launch_assets.py b/scripts/validate_m14_launch_assets.py index 8c7f005b..da2612f0 100644 --- a/scripts/validate_m14_launch_assets.py +++ b/scripts/validate_m14_launch_assets.py @@ -39,7 +39,16 @@ if 'prompts/codex/m14/CODEX_FIRST_DAY_MASTER_PROMPT.md' not in start: raise SystemExit('CODEX_START.md does not point to M14 first-day prompt') readme = (ROOT / 'README.md').read_text(encoding='utf-8') -if 'M14 — Build Launch Package' not in readme: - raise SystemExit('README.md does not identify M14 as current milestone') +active_terms = [ + 'v1.0.0-rc.1', + 'docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md', + 'docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md', +] +missing_active_terms = [term for term in active_terms if term not in readme] +if missing_active_terms: + raise SystemExit( + 'README.md is missing active release controls: ' + + ', '.join(missing_active_terms) + ) -print('M14 launch assets OK') +print('Historical M14 assets and active RC controls OK')