Prepare GeoIntel 1.0.0 release candidate
This commit is contained in:
@@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
## Project identity
|
## 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
|
## Required behavior
|
||||||
|
|
||||||
- Read `docs/CODEX_BOOTSTRAP_PROMPT.md` first.
|
- 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/API_CONTRACTS.md` as source of truth for endpoints.
|
||||||
- Use `docs/DATABASE_IMPLEMENTATION_PLAN.md` as source of truth for persistence.
|
- Use `docs/DATABASE_IMPLEMENTATION_PLAN.md` as source of truth for persistence.
|
||||||
- Use `docs/DEFINITION_OF_DONE.md` to decide whether work is complete.
|
- Use `docs/DEFINITION_OF_DONE.md` to decide whether work is complete.
|
||||||
|
|||||||
@@ -87,6 +87,17 @@
|
|||||||
- Classified fixed NGI and RBINS national/maritime editions explicitly. The
|
- Classified fixed NGI and RBINS national/maritime editions explicitly. The
|
||||||
live source-freshness report now records three current sources, zero due
|
live source-freshness report now records three current sources, zero due
|
||||||
sources and zero integrity issues.
|
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,
|
- Added a read-only release-evidence manifest command with Git, migration,
|
||||||
dependency, configuration checksum and optional live endpoint evidence.
|
dependency, configuration checksum and optional live endpoint evidence.
|
||||||
- Replaced the obsolete pre-build status with the current implemented
|
- Replaced the obsolete pre-build status with the current implemented
|
||||||
|
|||||||
@@ -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:
|
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
|
## 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/00-start/START_HERE.md`
|
||||||
- `docs/40-build-launch/SPRINT_1_SCOPE_FREEZE.md`
|
- `docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md`
|
||||||
- `docs/40-build-launch/BUILD_SUCCESS_DEFINITION.md`
|
- `docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md`
|
||||||
- `docs/40-build-launch/BUILD_ORDER_GRAPH.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
|
## Core V1 vertical slice
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
an explicit candidate ceiling and a recent checksum-verified database plus
|
||||||
SHA-256 storage backup mounted read-only under `/app/backups`. See
|
SHA-256 storage backup mounted read-only under `/app/backups`. See
|
||||||
`docs/DATA_OPERATIONS_RUNBOOK.md`.
|
`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`.
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ class Settings(BaseSettings):
|
|||||||
)
|
)
|
||||||
|
|
||||||
app_env: str = Field(default="development", validation_alias="GEOINTEL_ENV")
|
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_sha: str | None = Field(default=None, validation_alias="GEOINTEL_BUILD_SHA")
|
||||||
build_time: str | None = Field(default=None, validation_alias="GEOINTEL_BUILD_TIME")
|
build_time: str | None = Field(default=None, validation_alias="GEOINTEL_BUILD_TIME")
|
||||||
api_prefix: str = Field(default="/api/v1", validation_alias="GEOINTEL_API_PREFIX")
|
api_prefix: str = Field(default="/api/v1", validation_alias="GEOINTEL_API_PREFIX")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "geointel-backend"
|
name = "geointel-backend"
|
||||||
version = "0.1.0"
|
version = "1.0.0rc1"
|
||||||
description = "GeoIntel Kempen backend"
|
description = "GeoIntel Belgium and Belgian North Sea backend"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -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["schema_version"] == 1
|
||||||
assert manifest["release_id"] == "test-rc"
|
assert manifest["release_id"] == "test-rc"
|
||||||
|
assert manifest["version"] == "1.0.0-rc.1"
|
||||||
assert manifest["read_only"] is True
|
assert manifest["read_only"] is True
|
||||||
assert manifest["scope"] == "Belgium and the Belgian North Sea"
|
assert manifest["scope"] == "Belgium and the Belgian North Sea"
|
||||||
assert "DATABASE_URL" not in json.dumps(manifest).replace(
|
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["migrations"]["single_head"] is True
|
||||||
assert payload["files"]["docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md"]["sha256"]
|
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/RC_ROADMAP_BELGIUM_NORTH_SEA.md"]["sha256"]
|
||||||
|
assert payload["files"]["docs/RELEASE_RUNBOOK.md"]["sha256"]
|
||||||
|
|
||||||
|
|
||||||
def test_readiness_gate_compiles_release_evidence_command() -> None:
|
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
|
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")
|
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||||
vision = (ROOT / "docs" / "PRODUCT_VISION.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 "Belgium and the Belgian North Sea" in readme
|
||||||
assert "Mol is de primaire operationele focus" in vision
|
assert "Mol and the Kempen remain deep regression" in readme
|
||||||
assert "broader Kempen" in readme
|
assert "Belgie en de Belgische Noordzee" in vision
|
||||||
|
assert "Mol en de Kempen blijven gouden regressiegebieden" in vision
|
||||||
|
|||||||
@@ -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_SHA=unknown
|
||||||
ARG GEOINTEL_BUILD_TIME=unknown
|
ARG GEOINTEL_BUILD_TIME=unknown
|
||||||
|
ARG GEOINTEL_APP_VERSION=1.0.0-rc.1
|
||||||
|
|
||||||
ENV GEOINTEL_BUILD_SHA="${GEOINTEL_BUILD_SHA}" \
|
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" \
|
LABEL org.opencontainers.image.title="GeoIntel" \
|
||||||
org.opencontainers.image.description="GeoIntel workbench for Belgium and the Belgian North Sea" \
|
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.revision="${GEOINTEL_BUILD_SHA}" \
|
||||||
org.opencontainers.image.created="${GEOINTEL_BUILD_TIME}" \
|
org.opencontainers.image.created="${GEOINTEL_BUILD_TIME}" \
|
||||||
io.geointel.ai.enabled="${GEOINTEL_INSTALL_AI}"
|
io.geointel.ai.enabled="${GEOINTEL_INSTALL_AI}"
|
||||||
|
|||||||
@@ -247,6 +247,11 @@ bash deploy/unraid/run-dockerman-container.sh
|
|||||||
|
|
||||||
## Release identity, fresh install and rollback
|
## 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:
|
Inspect the running immutable revision and retained images:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ if [ -n "${DEPLOY_GEOINTEL_INSTALL_AI:-}" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
GEOINTEL_INSTALL_AI="${GEOINTEL_INSTALL_AI:-false}"
|
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_SHA="$(git rev-parse HEAD)"
|
||||||
GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
GEOINTEL_BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
GEOINTEL_IMAGE_REPOSITORY="${GEOINTEL_IMAGE_REPOSITORY:-geointel-all-in-one}"
|
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"}}' \
|
--format '{{index .Config.Labels "io.geointel.ai.enabled"}}' \
|
||||||
"$GEOINTEL_RELEASE_IMAGE"
|
"$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
|
echo "Immutable release tag has conflicting metadata: ${GEOINTEL_RELEASE_IMAGE}" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
@@ -101,6 +115,7 @@ else
|
|||||||
--build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \
|
--build-arg GEOINTEL_INSTALL_AI="$GEOINTEL_INSTALL_AI" \
|
||||||
--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \
|
--build-arg GEOINTEL_BUILD_SHA="$GEOINTEL_BUILD_SHA" \
|
||||||
--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \
|
--build-arg GEOINTEL_BUILD_TIME="$GEOINTEL_BUILD_TIME" \
|
||||||
|
--build-arg GEOINTEL_APP_VERSION="$GEOINTEL_APP_VERSION" \
|
||||||
-f deploy/unraid/Dockerfile.all-in-one \
|
-f deploy/unraid/Dockerfile.all-in-one \
|
||||||
-t "$GEOINTEL_RELEASE_IMAGE" \
|
-t "$GEOINTEL_RELEASE_IMAGE" \
|
||||||
-t "${GEOINTEL_IMAGE_REPOSITORY}:latest" \
|
-t "${GEOINTEL_IMAGE_REPOSITORY}:latest" \
|
||||||
@@ -128,5 +143,5 @@ fi
|
|||||||
|
|
||||||
echo "Deployed immutable image ${GEOINTEL_RELEASE_IMAGE}."
|
echo "Deployed immutable image ${GEOINTEL_RELEASE_IMAGE}."
|
||||||
docker image inspect \
|
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"
|
"$GEOINTEL_RELEASE_IMAGE"
|
||||||
|
|||||||
+23
-21
@@ -5,13 +5,18 @@ Older handoff files are historical. If documents conflict, follow the precedence
|
|||||||
|
|
||||||
## Current milestone
|
## 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
|
## 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
|
## Non-negotiable product identity
|
||||||
|
|
||||||
@@ -35,21 +40,18 @@ GeoIntel is not primarily:
|
|||||||
Read these files in order before coding:
|
Read these files in order before coding:
|
||||||
|
|
||||||
1. `docs/00-start/START_HERE.md`
|
1. `docs/00-start/START_HERE.md`
|
||||||
2. `docs/governance/GEOINTEL_CONSTITUTION.md`
|
2. `docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md`
|
||||||
3. `docs/governance/ARCHITECTURE_INVARIANTS.md`
|
3. `docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md`
|
||||||
4. `docs/governance/FORBIDDEN_DECISIONS.md`
|
4. `docs/governance/GEOINTEL_CONSTITUTION.md`
|
||||||
5. `docs/governance/DECISION_PRECEDENCE.md`
|
5. `docs/governance/ARCHITECTURE_INVARIANTS.md`
|
||||||
6. `docs/specs/CANONICAL_DOMAIN_MODELS.md`
|
6. `docs/governance/FORBIDDEN_DECISIONS.md`
|
||||||
7. `docs/specs/GIS_STANDARDS.md`
|
7. `docs/API_CONTRACTS.md`
|
||||||
8. `docs/specs/RASTER_STANDARDS.md`
|
8. `docs/DATABASE_IMPLEMENTATION_PLAN.md`
|
||||||
9. `docs/specs/STATE_MACHINES.md`
|
9. `docs/DATA_SPECIFICATION.md`
|
||||||
10. `docs/workflows/GOLDEN_PATHS.md`
|
10. `docs/DATA_SOURCES.md`
|
||||||
11. `docs/build/BUILD_ORDER_DEPENDENCY_GRAPH.md`
|
11. `docs/STORAGE_ARCHITECTURE.md`
|
||||||
12. `docs/build/CODEX_OPERATING_SYSTEM.md`
|
12. `docs/DEFINITION_OF_DONE.md`
|
||||||
13. `docs/40-build-launch/SPRINT_1_SCOPE_FREEZE.md`
|
13. `docs/RELEASE_RUNBOOK.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`
|
|
||||||
|
|
||||||
## Canonical first implementation target
|
## Canonical first implementation target
|
||||||
|
|
||||||
@@ -99,15 +101,15 @@ Codex may not change:
|
|||||||
|
|
||||||
## Conflict resolution
|
## 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.
|
1. Constitution and architecture invariants.
|
||||||
2. Forbidden decisions.
|
2. Forbidden decisions.
|
||||||
3. State machines and canonical models.
|
3. State machines and canonical models.
|
||||||
4. API/database contracts.
|
4. API/database contracts.
|
||||||
5. Build order dependency graph.
|
5. Build order dependency graph.
|
||||||
6. M14 build-launch docs for first-run scope and stop rules.
|
6. Belgium/North Sea scope freeze and RC roadmap.
|
||||||
7. Older milestone handoff documents.
|
7. Older milestone and sprint handoff documents.
|
||||||
|
|
||||||
## Required pass ending
|
## Required pass ending
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ Returns process liveness only. It never queries PostgreSQL.
|
|||||||
{
|
{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"service": "geointel-backend",
|
"service": "geointel-backend",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0-rc.1",
|
||||||
"build_sha": null,
|
"build_sha": null,
|
||||||
"build_time": null
|
"build_time": null
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ degraded. Docker uses `/health/ready`.
|
|||||||
{
|
{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"service": "geointel-backend",
|
"service": "geointel-backend",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0-rc.1",
|
||||||
"database": "ok",
|
"database": "ok",
|
||||||
"postgis": "ok:3.x",
|
"postgis": "ok:3.x",
|
||||||
"migration": "ok:202607160001",
|
"migration": "ok:202607160001",
|
||||||
@@ -105,7 +105,7 @@ envelope. PostGIS and configured YOLO state are derived at runtime.
|
|||||||
"sam": false,
|
"sam": false,
|
||||||
"grb": "bounded",
|
"grb": "bounded",
|
||||||
"sentinel": "planned",
|
"sentinel": "planned",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0-rc.1",
|
||||||
"build_sha": null,
|
"build_sha": null,
|
||||||
"providers": []
|
"providers": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
"redis": "ok",
|
"redis": "ok",
|
||||||
"storage": "ok"
|
"storage": "ok"
|
||||||
},
|
},
|
||||||
"version": "0.1.0"
|
"version": "1.0.0-rc.1"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
1. `docs/SPECIFICATION_FREEZE_M0.md`
|
||||||
2. `docs/V1_SCOPE_FREEZE.md`
|
2. `docs/V1_SCOPE_FREEZE.md`
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ backward-compatible alias.
|
|||||||
{
|
{
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"service": "geointel-backend",
|
"service": "geointel-backend",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0-rc.1",
|
||||||
"database": "ok",
|
"database": "ok",
|
||||||
"postgis": "ok:3.x",
|
"postgis": "ok:3.x",
|
||||||
"migration": "ok:202607160001",
|
"migration": "ok:202607160001",
|
||||||
|
|||||||
+14
-5
@@ -1,6 +1,7 @@
|
|||||||
# Product Vision
|
# 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
|
## Geen klassieke GIS-viewer
|
||||||
|
|
||||||
@@ -28,19 +29,27 @@ GeoIntel moet bewijzen dat de ontwikkelaar de volledige keten begrijpt:
|
|||||||
8. kwaliteit meten
|
8. kwaliteit meten
|
||||||
9. resultaten exporteren
|
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
|
- gebouwdetectie en infrastructuuranalyse in Mol
|
||||||
- vergelijkende validatie in Geel
|
- vergelijkende validatie in Geel
|
||||||
- verstedelijking rond Turnhout
|
- verstedelijking rond Turnhout
|
||||||
- natuurfragmentatie rond Kasterlee of Retie
|
- 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
|
## Richting
|
||||||
|
|
||||||
De definitieve richting is:
|
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.
|
||||||
|
|||||||
@@ -534,6 +534,8 @@ maritime freshness evidence classifies all three fixed official editions as
|
|||||||
|
|
||||||
## RC-11 - Final release package
|
## RC-11 - Final release package
|
||||||
|
|
||||||
|
**State: in progress.**
|
||||||
|
|
||||||
### Work
|
### Work
|
||||||
|
|
||||||
- rerun fresh-install and upgrade proof using the release image;
|
- rerun fresh-install and upgrade proof using the release image;
|
||||||
|
|||||||
+13
-3
@@ -1,6 +1,16 @@
|
|||||||
# GeoIntel Docs Index
|
# 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 foundation
|
||||||
- PRODUCT_VISION.md
|
- PRODUCT_VISION.md
|
||||||
@@ -37,13 +47,13 @@ Start here when preparing an implementation pass.
|
|||||||
- UI_PAGE_SPECIFICATIONS.md
|
- UI_PAGE_SPECIFICATIONS.md
|
||||||
- COMPONENT_BREAKDOWN.md
|
- COMPONENT_BREAKDOWN.md
|
||||||
|
|
||||||
## Execution
|
## Historical execution
|
||||||
- CODEX_MASTER_PROMPT.md
|
- CODEX_MASTER_PROMPT.md
|
||||||
- CODEX_EXECUTION_PLAN.md
|
- CODEX_EXECUTION_PLAN.md
|
||||||
- CODEX_BUILD_PLAN.md
|
- CODEX_BUILD_PLAN.md
|
||||||
- CODEX_EXECUTION_LOG.md
|
- CODEX_EXECUTION_LOG.md
|
||||||
- ACCEPTANCE_CRITERIA.md
|
- ACCEPTANCE_CRITERIA.md
|
||||||
- TODO.md
|
- TODO.md (historical implementation archive)
|
||||||
- IMPLEMENTATION_BACKLOG.md
|
- IMPLEMENTATION_BACKLOG.md
|
||||||
- DEVELOPMENT_RULES.md
|
- DEVELOPMENT_RULES.md
|
||||||
- AGENTS.md
|
- AGENTS.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:<release-commit>-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:<release-commit>-ai \
|
||||||
|
artifacts/releases/v1.0.0-rc.1/geointel-sbom.spdx.json
|
||||||
|
bash scripts/scan_container_image.sh \
|
||||||
|
geointel-all-in-one:<release-commit>-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:<release-commit>-ai \
|
||||||
|
--image-id sha256:<image-id> \
|
||||||
|
--image-revision <release-commit> \
|
||||||
|
--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`.
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "geointel-frontend",
|
"name": "geointel-frontend",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0-rc.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "geointel-frontend",
|
"name": "geointel-frontend",
|
||||||
"version": "0.1.0",
|
"version": "1.0.0-rc.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"maplibre-gl": "^4.7.1",
|
"maplibre-gl": "^4.7.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "geointel-frontend",
|
"name": "geointel-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.1.0",
|
"version": "1.0.0-rc.1",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
|
|||||||
@@ -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<VERSION> 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())
|
||||||
@@ -21,6 +21,7 @@ from typing import Any, Sequence
|
|||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
BACKEND = ROOT / "backend"
|
BACKEND = ROOT / "backend"
|
||||||
DEFAULT_HASHED_FILES = (
|
DEFAULT_HASHED_FILES = (
|
||||||
|
"VERSION",
|
||||||
"AGENTS.md",
|
"AGENTS.md",
|
||||||
"backend/pyproject.toml",
|
"backend/pyproject.toml",
|
||||||
"backend/alembic.ini",
|
"backend/alembic.ini",
|
||||||
@@ -31,6 +32,7 @@ DEFAULT_HASHED_FILES = (
|
|||||||
"deploy/unraid/Dockerfile.all-in-one",
|
"deploy/unraid/Dockerfile.all-in-one",
|
||||||
"docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md",
|
"docs/RC_SCOPE_FREEZE_BELGIUM_NORTH_SEA.md",
|
||||||
"docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md",
|
"docs/RC_ROADMAP_BELGIUM_NORTH_SEA.md",
|
||||||
|
"docs/RELEASE_RUNBOOK.md",
|
||||||
)
|
)
|
||||||
DEPENDENCIES = (
|
DEPENDENCIES = (
|
||||||
"alembic",
|
"alembic",
|
||||||
@@ -227,6 +229,7 @@ def build_manifest(args: argparse.Namespace) -> dict[str, Any]:
|
|||||||
return {
|
return {
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"release_id": args.release_id,
|
"release_id": args.release_id,
|
||||||
|
"version": (ROOT / "VERSION").read_text(encoding="utf-8").strip(),
|
||||||
"captured_at": utc_now(),
|
"captured_at": utc_now(),
|
||||||
"read_only": True,
|
"read_only": True,
|
||||||
"scope": "Belgium and the Belgian North Sea",
|
"scope": "Belgium and the Belgian North Sea",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ fi
|
|||||||
echo "== GeoIntel run readiness check =="
|
echo "== GeoIntel run readiness check =="
|
||||||
|
|
||||||
"$PYTHON_BIN" -m py_compile scripts/capture_release_evidence.py
|
"$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" -m py_compile scripts/verify_python_lock.py
|
||||||
"$PYTHON_BIN" scripts/verify_python_lock.py
|
"$PYTHON_BIN" scripts/verify_python_lock.py
|
||||||
"$PYTHON_BIN" -m py_compile scripts/verify_security_exceptions.py
|
"$PYTHON_BIN" -m py_compile scripts/verify_security_exceptions.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')
|
raise SystemExit('CODEX_START.md does not point to M14 first-day prompt')
|
||||||
|
|
||||||
readme = (ROOT / 'README.md').read_text(encoding='utf-8')
|
readme = (ROOT / 'README.md').read_text(encoding='utf-8')
|
||||||
if 'M14 — Build Launch Package' not in readme:
|
active_terms = [
|
||||||
raise SystemExit('README.md does not identify M14 as current milestone')
|
'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')
|
||||||
|
|||||||
Reference in New Issue
Block a user