Prepare GeoIntel 1.0.0 release candidate
GeoIntel release gates / Compile, test, contracts and builds (push) Canceled after 0s
GeoIntel release gates / Python and npm vulnerability policy (push) Canceled after 0s
GeoIntel release gates / GIS image, SBOM and container scan (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-18 07:59:38 +02:00
parent 213b57ce2e
commit 79a9f9dbfc
28 changed files with 764 additions and 64 deletions
+9
View File
@@ -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`.
+4 -1
View File
@@ -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")
+2 -2
View File
@@ -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 = [
+132
View File
@@ -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
@@ -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:
@@ -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