Keep orthophoto release evidence immutable
This commit is contained in:
@@ -25,6 +25,9 @@
|
|||||||
- Made official `YYYY.NN` orthophoto Datasets take precedence over rolling
|
- Made official `YYYY.NN` orthophoto Datasets take precedence over rolling
|
||||||
acquisition markers in source-catalog comparison. No API, migration,
|
acquisition markers in source-catalog comparison. No API, migration,
|
||||||
scheduler, browser fetch or automatic refresh was added.
|
scheduler, browser fetch or automatic refresh was added.
|
||||||
|
- Made staged, reviewed and applied release evidence write-once. An idempotent
|
||||||
|
apply validates and reuses the existing evidence instead of changing its
|
||||||
|
timestamp or authorization history.
|
||||||
|
|
||||||
## Sprint 230 Governed orthophoto release preflight (2026-07-17)
|
## Sprint 230 Governed orthophoto release preflight (2026-07-17)
|
||||||
|
|
||||||
|
|||||||
@@ -277,6 +277,9 @@ def test_stage_fetches_one_allowlisted_map_and_writes_reviewable_rgb_evidence(tm
|
|||||||
assert manifest["normalized_geotiff"]["crs"] == "EPSG:31370"
|
assert manifest["normalized_geotiff"]["crs"] == "EPSG:31370"
|
||||||
assert manifest["normalized_geotiff"]["band_count"] == 3
|
assert manifest["normalized_geotiff"]["band_count"] == 3
|
||||||
assert Path(manifest["review_preview"]["path"]).read_bytes().startswith(b"\x89PNG")
|
assert Path(manifest["review_preview"]["path"]).read_bytes().startswith(b"\x89PNG")
|
||||||
|
with pytest.raises(RuntimeError, match="will not be overwritten"):
|
||||||
|
MANAGER.stage_artifacts(args, release_report, request, opener=opener)
|
||||||
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_getmap_redirect_and_response_limits_fail_closed(tmp_path: Path) -> None:
|
def test_getmap_redirect_and_response_limits_fail_closed(tmp_path: Path) -> None:
|
||||||
@@ -397,6 +400,47 @@ def test_official_flight_dates_are_persisted_without_inventing_a_catalog_date()
|
|||||||
MANAGER.parse_flight_dates(["spring 2025"])
|
MANAGER.parse_flight_dates(["spring 2025"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_applied_evidence_is_immutable_on_idempotent_retry(tmp_path: Path) -> None:
|
||||||
|
args, plan_path, plan, staged = staged_plan(tmp_path)
|
||||||
|
manifest = MANAGER.validate_staged_artifacts(args, Path(staged["manifest_path"]))
|
||||||
|
args.approve = True
|
||||||
|
args.reviewer = "Jens"
|
||||||
|
review = MANAGER.build_review_evidence(args, plan_path, plan, manifest)
|
||||||
|
review_path = MANAGER.default_review_path(args, "2025.04")
|
||||||
|
dataset = {
|
||||||
|
"id": "dataset-1",
|
||||||
|
"checksum_sha256": manifest["normalized_geotiff"]["sha256"],
|
||||||
|
}
|
||||||
|
final_report = report(status="current", local="2025.04")
|
||||||
|
final_report["release"]["comparison_status"] = "same"
|
||||||
|
|
||||||
|
path, first = MANAGER.applied_evidence(
|
||||||
|
args,
|
||||||
|
plan_path,
|
||||||
|
plan,
|
||||||
|
review_path,
|
||||||
|
review,
|
||||||
|
dataset,
|
||||||
|
final_report,
|
||||||
|
reused=False,
|
||||||
|
)
|
||||||
|
original_bytes = path.read_bytes()
|
||||||
|
_, second = MANAGER.applied_evidence(
|
||||||
|
args,
|
||||||
|
plan_path,
|
||||||
|
plan,
|
||||||
|
review_path,
|
||||||
|
review,
|
||||||
|
dataset,
|
||||||
|
final_report,
|
||||||
|
reused=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert second == first
|
||||||
|
assert path.read_bytes() == original_bytes
|
||||||
|
assert second["dataset_status"] == "imported"
|
||||||
|
|
||||||
|
|
||||||
def test_release_manager_is_packaged_and_compiled_by_readiness() -> None:
|
def test_release_manager_is_packaged_and_compiled_by_readiness() -> None:
|
||||||
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
dockerfile = (ROOT / "deploy" / "unraid" / "Dockerfile.all-in-one").read_text(encoding="utf-8")
|
||||||
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
readiness = (ROOT / "scripts" / "run_readiness_check.sh").read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -9787,6 +9787,8 @@ Implemented:
|
|||||||
- Made the latest official `YYYY.NN` Dataset authoritative for source-catalog
|
- Made the latest official `YYYY.NN` Dataset authoritative for source-catalog
|
||||||
comparison even when a newer-imported rolling marker also exists. No API,
|
comparison even when a newer-imported rolling marker also exists. No API,
|
||||||
migration, release table, Job type or frontend behavior changed.
|
migration, release table, Job type or frontend behavior changed.
|
||||||
|
- Made stage, review and applied evidence write-once. Exact apply retries reuse
|
||||||
|
the original checksum-bound evidence and never rewrite approval history.
|
||||||
|
|
||||||
Validation so far:
|
Validation so far:
|
||||||
- 38 focused Sprint 222/230/231 tests pass with deprecations treated as errors.
|
- 38 focused Sprint 222/230/231 tests pass with deprecations treated as errors.
|
||||||
|
|||||||
@@ -168,7 +168,9 @@ flight-day preflight identities; review binds a named approval; applied
|
|||||||
evidence binds both to the immutable Dataset id and checksum. Paths outside
|
evidence binds both to the immutable Dataset id and checksum. Paths outside
|
||||||
this root, changed files and changed provider/local state fail closed. Only the
|
this root, changed files and changed provider/local state fail closed. Only the
|
||||||
normalized GeoTIFF enters ordinary Dataset storage through DatasetService.
|
normalized GeoTIFF enters ordinary Dataset storage through DatasetService.
|
||||||
Existing `most_recent_at_*` raster metadata is never rewritten.
|
Existing release evidence and `most_recent_at_*` raster metadata are never
|
||||||
|
rewritten. An exact apply retry validates and returns the original applied
|
||||||
|
evidence rather than replacing it.
|
||||||
|
|
||||||
DHMV II DTM/DSM outputs are also normal raster Dataset files. The provider WCS
|
DHMV II DTM/DSM outputs are also normal raster Dataset files. The provider WCS
|
||||||
returns multipart coverage data; GeoIntel retains response and extracted
|
returns multipart coverage data; GeoIntel retains response and extracted
|
||||||
|
|||||||
@@ -446,6 +446,10 @@ def stage_artifacts(
|
|||||||
geotiff_path = directory / f"orthophoto_{edition}_{selection_key(list(args.bbox))}.tif"
|
geotiff_path = directory / f"orthophoto_{edition}_{selection_key(list(args.bbox))}.tif"
|
||||||
preview_path = directory / "review-preview.png"
|
preview_path = directory / "review-preview.png"
|
||||||
manifest_path = directory / "staged-manifest.json"
|
manifest_path = directory / "staged-manifest.json"
|
||||||
|
if directory.exists() and any(directory.iterdir()):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Governed orthophoto evidence already exists and will not be overwritten: {directory}"
|
||||||
|
)
|
||||||
max_bytes = args.max_response_mb * 1024 * 1024
|
max_bytes = args.max_response_mb * 1024 * 1024
|
||||||
content, content_type, final_url = fetch_map(
|
content, content_type, final_url = fetch_map(
|
||||||
request_identity,
|
request_identity,
|
||||||
@@ -888,6 +892,22 @@ def applied_evidence(
|
|||||||
*,
|
*,
|
||||||
reused: bool,
|
reused: bool,
|
||||||
) -> tuple[Path, dict[str, Any]]:
|
) -> tuple[Path, dict[str, Any]]:
|
||||||
|
path = plan_path.with_name("applied-evidence.json")
|
||||||
|
if path.is_file():
|
||||||
|
existing = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
if existing.get("applied_evidence_sha256") != canonical_sha256(existing, "applied_evidence_sha256"):
|
||||||
|
raise RuntimeError("Existing orthophoto applied evidence checksum is invalid")
|
||||||
|
if (
|
||||||
|
existing.get("project_id") != args.project_id
|
||||||
|
or existing.get("scope") != args.scope
|
||||||
|
or existing.get("edition") != plan["edition"]
|
||||||
|
or existing.get("staged_plan_sha256") != plan["plan_sha256"]
|
||||||
|
or existing.get("review_sha256") != review["review_sha256"]
|
||||||
|
or existing.get("dataset_id") != dataset["id"]
|
||||||
|
or existing.get("dataset_checksum_sha256") != dataset["checksum_sha256"]
|
||||||
|
):
|
||||||
|
raise RuntimeError("Existing orthophoto applied evidence does not match this approved Dataset")
|
||||||
|
return path, existing
|
||||||
payload: dict[str, Any] = {
|
payload: dict[str, Any] = {
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"status": "applied",
|
"status": "applied",
|
||||||
@@ -908,7 +928,6 @@ def applied_evidence(
|
|||||||
"existing_snapshots_retained": True,
|
"existing_snapshots_retained": True,
|
||||||
}
|
}
|
||||||
payload["applied_evidence_sha256"] = canonical_sha256(payload, "applied_evidence_sha256")
|
payload["applied_evidence_sha256"] = canonical_sha256(payload, "applied_evidence_sha256")
|
||||||
path = plan_path.with_name("applied-evidence.json")
|
|
||||||
write_json(path, payload)
|
write_json(path, payload)
|
||||||
return path, payload
|
return path, payload
|
||||||
|
|
||||||
@@ -939,10 +958,12 @@ def main() -> int:
|
|||||||
edition = require_edition(args, report)
|
edition = require_edition(args, report)
|
||||||
if args.action == "stage":
|
if args.action == "stage":
|
||||||
authorization = authorize_stage(args, report)
|
authorization = authorize_stage(args, report)
|
||||||
|
plan_path = governed_path(args, args.plan_path or default_plan_path(args, edition))
|
||||||
|
if plan_path.exists():
|
||||||
|
raise RuntimeError(f"Governed orthophoto plan already exists and will not be overwritten: {plan_path}")
|
||||||
request_identity = map_request(report)
|
request_identity = map_request(report)
|
||||||
staged = stage_artifacts(args, report, request_identity)
|
staged = stage_artifacts(args, report, request_identity)
|
||||||
plan = build_staged_plan(args, report, authorization, staged)
|
plan = build_staged_plan(args, report, authorization, staged)
|
||||||
plan_path = governed_path(args, args.plan_path or default_plan_path(args, edition))
|
|
||||||
write_json(plan_path, plan)
|
write_json(plan_path, plan)
|
||||||
print(
|
print(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
@@ -958,6 +979,10 @@ def main() -> int:
|
|||||||
if args.action == "review":
|
if args.action == "review":
|
||||||
review = build_review_evidence(args, plan_path, plan, manifest)
|
review = build_review_evidence(args, plan_path, plan, manifest)
|
||||||
review_path = governed_path(args, args.review_path or default_review_path(args, edition))
|
review_path = governed_path(args, args.review_path or default_review_path(args, edition))
|
||||||
|
if review_path.exists():
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Governed orthophoto review already exists and will not be overwritten: {review_path}"
|
||||||
|
)
|
||||||
write_json(review_path, review)
|
write_json(review_path, review)
|
||||||
print(json.dumps({"status": "approved", "review_path": str(review_path), **review}, ensure_ascii=False, indent=2))
|
print(json.dumps({"status": "approved", "review_path": str(review_path), **review}, ensure_ascii=False, indent=2))
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
Reference in New Issue
Block a user