fix(release): make deployment backup and rollback immutable

This commit is contained in:
Jens
2026-08-30 06:00:43 +02:00
parent a0884d64c9
commit c272220277
47 changed files with 3035 additions and 430 deletions
+28 -7
View File
@@ -89,9 +89,9 @@ The normal project lifecycle archive path does not need destructive
confirmation because it changes only `status=archived` and preserves all
data.
## Destructive apply gate
## Recoverable quarantine gate
First create a fresh backup with a SHA-256 storage inventory on the host:
First create a fresh backup with a byte-complete SHA-256 storage snapshot on the host:
```bash
bash scripts/backup_release_state.sh \
@@ -112,7 +112,8 @@ bash scripts/verify_release_backup.sh \
```
The Unraid runtime mounts `GEOINTEL_BACKUPS_PATH` read-only at `/app/backups`.
Only after reviewing the dry run may an operator execute:
Only after reviewing the dry run may an operator move the exact candidates to
a protected quarantine:
```bash
docker exec geointel python /app/scripts/cleanup_storage_artifacts.py \
@@ -120,14 +121,34 @@ docker exec geointel python /app/scripts/cleanup_storage_artifacts.py \
--max-delete <reviewed-candidate-count> \
--backup-dir /app/backups/<release-id> \
--backup-max-age-hours 24 \
--confirm DELETE_STORAGE_ARTIFACTS \
--confirm QUARANTINE_STORAGE_ARTIFACTS \
--apply
```
The command re-runs the audit immediately before deletion. It refuses the
The command re-runs the audit immediately before quarantine. It refuses the
operation when the exact token is absent, the candidate count exceeds the
operator limit, the backup is stale/incomplete, checksums differ, the storage
inventory is not SHA-256, or the path is outside the cleanup allowlist.
operator limit, the backup is stale/incomplete, snapshot checksums differ, or
the path is outside the cleanup allowlist.
Every moved byte is hashed and retained below
`operator-evidence/cleanup-quarantine/<operation-id>/files/`; an atomic
manifest records its original path, quarantine path, size, checksum and
backup identity. Cleanup uses a backup-to-quarantine hard-link state machine:
the manifest records `planned`, `linked` and `quarantined` transitions so each
crash window can be reconciled without losing the retained inode.
Restore a reviewed quarantine without overwriting any path:
```bash
docker exec geointel python /app/scripts/restore_storage_quarantine.py \
--storage-root /app/storage \
--manifest /app/storage/operator-evidence/cleanup-quarantine/<operation-id>/manifest.json \
--confirm RESTORE_QUARANTINED_ARTIFACTS
```
Restore validates every retained checksum and never replaces an existing
destination. It uses an exclusive hard link and reconciles both-file and
one-file interruption states before updating the manifest, so an interrupted
restore can be resumed and verified.
The older demo-export cleanup has the same gate and uses confirmation token
`DELETE_DEMO_EXPORTS`.