From afa1058062f83f1fcddfc37d4ab5ed6b5975e37e Mon Sep 17 00:00:00 2001 From: tegwick Date: Sat, 3 Oct 2026 21:58:57 +0200 Subject: [PATCH] Record repaired backup lane and stage bounded consumer refresh packet Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a1029d-2c93-7500-a0e0-e3e089668b29 --- ...WP-0042-backup-lane-repair-2026-10-03.json | 36 +++ k8s/backup-release/README.md | 30 +++ k8s/backup-release/bundle.yaml | 208 ++++++++++++++++++ k8s/backup-release/worker-patch.yaml | 29 +++ ...form-backup-restore-evidence-scheduling.md | 20 +- 5 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 docs/evidence/ACTIVITY-WP-0042-backup-lane-repair-2026-10-03.json create mode 100644 k8s/backup-release/README.md create mode 100644 k8s/backup-release/bundle.yaml create mode 100644 k8s/backup-release/worker-patch.yaml diff --git a/docs/evidence/ACTIVITY-WP-0042-backup-lane-repair-2026-10-03.json b/docs/evidence/ACTIVITY-WP-0042-backup-lane-repair-2026-10-03.json new file mode 100644 index 0000000..7a76e0a --- /dev/null +++ b/docs/evidence/ACTIVITY-WP-0042-backup-lane-repair-2026-10-03.json @@ -0,0 +1,36 @@ +{ + "artifact": "root-custody-history-20261003T195656Z-a52ec01b2a4a4c28ba21a52eb29e7cb7.json.age", + "ciphertext_sha256": "577498be3c6566e9622fe0d87ea3ad55063941acfab3aeb0accfc4a05dd21200", + "coordinates_only_updated": true, + "current_live_baseline_matches": true, + "decrypted_history_matches": true, + "digest": "33a2ae086385028db1b654aa5646b7a98bcde1c5878c804e19315dadc3f87248", + "escrow_and_unrelated_fields_preserved": true, + "finished_at": "2026-10-03T19:56:59.023904+00:00", + "historical_config_present": true, + "historical_credential_status": "historical-owner-and-revocation-unverified", + "historical_share_matches_current_lane": false, + "history_entries": 1, + "independent_escrow_decryption_verified": true, + "independent_escrow_recipient_matches_current_lane": true, + "kv_version": 1, + "lane_cas_noop": false, + "lane_kv_version": 3, + "local_historical_rclone_used": false, + "ok": true, + "provider": "existing-governed-nextcloud-backup", + "provider_roundtrip_verified_before_cas": true, + "published_lane_kv_version": 4, + "readback_http_status": 200, + "record_id": "nk-custody:platform-root", + "recorded_at": "2026-10-03T19:56:48.310198+00:00", + "replacement_share_created": true, + "replacement_share_id": 23, + "revision": 1, + "runtime_consumers_refreshed": false, + "scope": "current-owner-read-and-governed-history-provider-roundtrip", + "stage": "complete", + "status": "verified", + "unknown_shares_and_old_backups_preserved": true, + "upload_http_status": 201 +} diff --git a/k8s/backup-release/README.md b/k8s/backup-release/README.md new file mode 100644 index 0000000..ce95e79 --- /dev/null +++ b/k8s/backup-release/README.md @@ -0,0 +1,30 @@ +# Reviewed candidate backup client refresh + +Prepared from railiance-platform source `f1a780d830a118eecdc0ada1885e7b8f95b51072`; +immutable Forgejo ConfigMap `backup-verified-debb9e4c59c9` and its existing mount +patch are review artifacts. This directory is excluded from ordinary GitOps +kustomization and has not been applied. It preserves existing env and other mounts. + +Before runtime promotion: + +1. Repair repo-manager historical `custodian-WP-*` identifier compatibility without + renumbering those owner records. Exact primary projection is currently unproved. +2. Synchronize approved railiance-platform source on railiance01, so CNPG's + existing hostPath receives the corrected public DAV header. This ConfigMap + updates Forgejo only; it does not update CNPG. +3. Confirm ESO's refreshed lane after KV4 publication, using safe metadata and + privately compared credential equality if needed, never printed Secret bodies. +4. Review the immutable bundle and scoped mount patch, then incorporate the new + ConfigMap and volume reference into the adopted `k8s/gitops` source. Use reviewed + ArgoCD sync rather than the old direct deployment patch helper. Keep the old + immutable bundle until no rollback ReplicaSet references it. +5. Verify worker recreation after source/ESO refresh, run + `python3 scripts/inspect_backup_delivery.py --platform-root ~/railiance-platform`, + and require both mounted source comparisons true. EnvFrom needs a new process; + ESO Ready alone is insufficient. +6. Obtain a separately reviewed existing bounded backup/provider receipt before + declaring activity consumer acceptance. Do not automatically fire schedules. + +No image update, worker restart, schedule or provider mutation has occurred. +Current source-owned error-boundary hardening from T06 remains separate; this +packet does not claim it was loaded by the existing image. diff --git a/k8s/backup-release/bundle.yaml b/k8s/backup-release/bundle.yaml new file mode 100644 index 0000000..45987a6 --- /dev/null +++ b/k8s/backup-release/bundle.yaml @@ -0,0 +1,208 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: backup-verified-debb9e4c59c9 + namespace: activity-core + labels: + railiance.io/workplan: RPF-WP-0029 +immutable: true +data: + tools__cmd__forgejo-backup: "#!/usr/bin/env bash\n# tools/cmd/forgejo-backup \u2014\ + \ Forgejo Option A backup (RAIL-HO-WP-0005 T04/T09)\n#\n# Daily: forgejo dump\ + \ (blob state) + pg_dump (forgejo-db) \u2192 age \u2192 Nextcloud forgejo/\n#\ + \ Retention: 14 daily + 4 weekly uploads (Sunday); local cache keep 7 per type.\n\ + #\n# Env:\n# KUBECONFIG (default ~/.kube/config-hosteurope)\n\ + # RAILIANCE_BACKUP_NC_TOKEN Nextcloud WebDAV token\n# RAILIANCE_BACKUP_DRY_RUN=1\ + \ skip upload and remote side effects\nset -euo pipefail\n\n\nROOT=\"$(cd \"\ + $(dirname \"${BASH_SOURCE[0]}\")/../..\" && pwd)\"\n# shellcheck source=lib/railiance-print.sh\n\ + source \"${ROOT}/lib/railiance-print.sh\"\n# shellcheck source=lib/railiance-backup-common.sh\n\ + source \"${ROOT}/lib/railiance-backup-common.sh\"\n\nKUBECONFIG=\"${KUBECONFIG:-${HOME}/.kube/config-hosteurope}\"\ + \nexport KUBECONFIG\nDRY_RUN=\"${RAILIANCE_BACKUP_DRY_RUN:-0}\"\n\nFORGEJO_NAMESPACE=\"\ + ${FORGEJO_NAMESPACE:-forgejo}\"\nFORGEJO_RELEASE=\"${FORGEJO_RELEASE:-forgejo}\"\ + \nFORGEJO_DB_NAMESPACE=\"${FORGEJO_DB_NAMESPACE:-databases}\"\nFORGEJO_DB_CLUSTER=\"\ + ${FORGEJO_DB_CLUSTER:-forgejo-db}\"\n\nkubectl_get_pod() {\n local ns=\"$1\"\ + \ selector=\"$2\" attempt out\n for attempt in 1 2 3 4 5 6 7 8 9 10; do\n \ + \ out=\"$(kubectl get pods -n \"${ns}\" -l \"${selector}\" \\\n -o jsonpath='{.items[0].metadata.name}'\ + \ 2>/dev/null || true)\"\n if [[ -n \"${out}\" ]]; then\n echo \"${out}\"\ + \n return 0\n fi\n sleep $((attempt * 3))\n done\n return 1\n}\n\n\ + BACKUP_DIR=\"${XDG_CACHE_HOME:-$HOME/.cache}/railiance/backups/forgejo\"\nTS=\"\ + $(date -u +%Y%m%dT%H%M%SZ)\"\nDOW=\"$(date -u +%u)\" # 7 = Sunday\n\nDUMP_PLAIN=\"\ + ${BACKUP_DIR}/forgejo-dump-${TS}.zip\"\nDUMP_AGE=\"${DUMP_PLAIN}.age\"\nDB_PLAIN=\"\ + ${BACKUP_DIR}/forgejo-db-${TS}.sql\"\nDB_AGE=\"${DB_PLAIN}.age\"\n\ncleanup_plain()\ + \ {\n rm -f \"${DUMP_PLAIN}\" \"${DB_PLAIN}\"\n}\ntrap cleanup_plain EXIT\n\n\ + if [[ \"${DRY_RUN}\" != \"1\" ]]; then\n railiance_backup_require_openbao_lane\n\ + fi\nrailiance_backup_require_tools\n\nFORGEJO_DB_POD=\"${FORGEJO_DB_POD:-$(kubectl_get_pod\ + \ \"${FORGEJO_DB_NAMESPACE}\" \\\n \"cnpg.io/cluster=${FORGEJO_DB_CLUSTER}\"\ + \ || true)}\"\nif [[ -z \"${FORGEJO_DB_POD}\" ]]; then\n bad \"preflight\" \"\ + forgejo-db pod not found (cluster ${FORGEJO_DB_CLUSTER})\"\n exit 1\nfi\n\nif\ + \ ! kubectl wait --for=condition=Ready \"pod/${FORGEJO_DB_POD}\" \\\n -n \"${FORGEJO_DB_NAMESPACE}\"\ + \ --timeout=180s >/dev/null 2>&1; then\n bad \"preflight\" \"forgejo-db pod not\ + \ Ready (check: make forgejo-db-status)\"\n exit 1\nfi\n\nPROD_POD=\"$(kubectl_get_pod\ + \ \"${FORGEJO_NAMESPACE}\" \\\n \"app.kubernetes.io/instance=${FORGEJO_RELEASE}\"\ + \ || true)\"\nif [[ -z \"${PROD_POD}\" ]]; then\n bad \"preflight\" \"Forgejo\ + \ app pod not found in ${FORGEJO_NAMESPACE}\"\n exit 1\nfi\n\nmkdir -p \"${BACKUP_DIR}\"\ + \nprint_hdr \"Forgejo backup \u2014 ${TS}\"\nok \"kubeconfig\" \"${KUBECONFIG}\"\ + \nok \"forgejo pod\" \"${FORGEJO_NAMESPACE}/${PROD_POD}\"\nok \"forgejo-db\" \"\ + ${FORGEJO_DB_NAMESPACE}/${FORGEJO_DB_POD}\"\n\n# 1. Application dump (repos, packages,\ + \ attachments, LFS, avatars)\nok \"forgejo dump\" \"running in pod\u2026\"\npython3\ + \ \"${ROOT}/scripts/capture_forgejo_archive.py\" \\\n --namespace \"${FORGEJO_NAMESPACE}\"\ + \ --pod \"${PROD_POD}\" --output \"${DUMP_PLAIN}\"\nok \"forgejo dump\" \"$(du\ + \ -h \"${DUMP_PLAIN}\" | awk '{print $1}') $(basename \"${DUMP_PLAIN}\")\"\n\n\ + # 2. PostgreSQL logical dump (plain SQL to stdout; CNPG root FS is read-only)\n\ + ok \"pg_dump\" \"database forgejo\u2026\"\nkubectl exec -n \"${FORGEJO_DB_NAMESPACE}\"\ + \ \"${FORGEJO_DB_POD}\" -c postgres -- \\\n pg_dump -U postgres -d forgejo --no-owner\ + \ --no-acl > \"${DB_PLAIN}\"\nok \"pg_dump\" \"$(du -h \"${DB_PLAIN}\" | awk '{print\ + \ $1}') $(basename \"${DB_PLAIN}\")\"\n\n# 3. Encrypt\nrailiance_backup_encrypt\ + \ \"${DUMP_PLAIN}\" \"${DUMP_AGE}\"\nrailiance_backup_encrypt \"${DB_PLAIN}\"\ + \ \"${DB_AGE}\"\nok \"age\" \"encrypted dump + db\"\n\nif [[ \"${DRY_RUN}\" ==\ + \ \"1\" ]]; then\n warn \"dry-run\" \"skipping Nextcloud upload\"\nelse\n railiance_backup_nc_upload\ + \ \"${DUMP_AGE}\" \"forgejo-dump-${TS}.zip.age\"\n ok \"upload\" \"forgejo-dump-${TS}.zip.age\"\ + \n railiance_backup_nc_upload \"${DB_AGE}\" \"forgejo-db-${TS}.sql.age\"\n ok\ + \ \"upload\" \"forgejo-db-${TS}.sql.age\"\n\n if [[ \"${DOW}\" == \"7\" ]]; then\n\ + \ railiance_backup_nc_upload \"${DUMP_AGE}\" \"forgejo-dump-weekly-${TS}.zip.age\"\ + \n railiance_backup_nc_upload \"${DB_AGE}\" \"forgejo-db-weekly-${TS}.sql.age\"\ + \n ok \"weekly\" \"Sunday copies uploaded\"\n fi\n\n railiance_backup_record_success\ + \ \"${BACKUP_DIR}\" \"${TS}\"\nfi\n\n# 4. Local cache prune (keep 7 per type)\n\ + railiance_backup_prune_local \"${BACKUP_DIR}\" 'forgejo-dump-*.zip.age' 7\nrailiance_backup_prune_local\ + \ \"${BACKUP_DIR}\" 'forgejo-db-*.sql.age' 7\nrailiance_backup_prune_local \"\ + ${BACKUP_DIR}\" 'forgejo-dump-*.zip' 3\nrailiance_backup_prune_local \"${BACKUP_DIR}\"\ + \ 'forgejo-db-*.sql' 3\nok \"cache\" \"local copies pruned\"\n\necho\nok \"done\"\ + \ \"Forgejo backup complete \u2014 ${TS}\"\nif [[ \"${DRY_RUN}\" != \"1\" ]];\ + \ then\n echo \" Dump: ${RAILIANCE_BACKUP_NC_PREFIX}/forgejo-dump-${TS}.zip.age\"\ + \n echo \" DB: ${RAILIANCE_BACKUP_NC_PREFIX}/forgejo-db-${TS}.sql.age\"\ + \n echo\n echo \" Decrypt with ~/.config/age/railiance-backup.key\"\n echo\ + \ \" Restore drill: railiance-infra tools/forgejo-restore-drill.sh\"\nfi" + scripts__capture_forgejo_archive.py: "#!/usr/bin/env python3\n\"\"\"Capture only\ + \ a completed, byte-identical and CRC-verified Forgejo ZIP.\"\"\"\nimport argparse\n\ + import hashlib\nfrom pathlib import Path\nimport secrets\nimport subprocess\n\ + import time\nimport zipfile\n\n\nESSENTIALS_FLAGS = ['--skip-package-data', '--skip-log',\ + \ '--skip-index', '--skip-repo-archives']\n\n\ndef validate_archive(path, profile=\"\ + full\"):\n with zipfile.ZipFile(path) as archive:\n if sum(i.file_size\ + \ for i in archive.infolist()) > 20*1024**3:\n raise ValueError('archive\ + \ exceeds recovery size bound')\n names = set(archive.namelist())\n \ + \ if 'forgejo-db.sql' not in names or not any(n.startswith('repos/') for n\ + \ in names):\n raise ValueError('required archive content missing')\n\ + \ if profile == 'essentials' and any(n.startswith(('data/packages/', 'data/repo-archive/',\ + \ 'data/indexers/')) and not n.endswith('/') for n in names):\n raise\ + \ ValueError('excluded data present in essentials archive')\n if archive.testzip()\ + \ is not None:\n raise ValueError('archive checksum failure')\n\n\n\ + def capture(namespace, pod, destination, profile=\"full\"):\n if profile not\ + \ in (\"full\", \"essentials\"): raise ValueError(\"unknown archive profile\"\ + )\n k = ['kubectl', 'exec', '--request-timeout=120s', '-n', namespace, pod,\ + \ '-c', 'gitea', '--']\n def call(args, timeout=150):\n r = subprocess.run(k\ + \ + args, capture_output=True, timeout=timeout)\n if r.returncode: raise\ + \ ValueError('capture command failed')\n return r.stdout\n remote =\ + \ '/tmp/wp0029-backup-' + secrets.token_hex(12)\n completed = False\n try:\n\ + \ # Completion belongs to this exact process, not a global pgrep or file\ + \ existence.\n flags = ' '.join(ESSENTIALS_FLAGS) if profile == 'essentials'\ + \ else ''\n command = f'umask 077; forgejo dump {flags} -f {remote}.zip\ + \ >{remote}.log 2>&1; result=$?; printf \"%s\" \"$result\" >{remote}.exit'\n \ + \ call(['sh','-c', 'nohup sh -c \"$1\" >/dev/null 2>&1 /dev/null 2>&1 || return 0\n\ + \ bao kv metadata get \"${RAILIANCE_BACKUP_BAO_PATH}\" >/dev/null 2>&1 || return\ + \ 0\n : \"${RAILIANCE_BACKUP_NC_TOKEN:=$(bao kv get -field=NC_WEBDAV_TOKEN \"\ + ${RAILIANCE_BACKUP_BAO_PATH}\" 2>/dev/null)}\"\n : \"${RAILIANCE_BACKUP_NC_WEBDAV_URL:=$(bao\ + \ kv get -field=NC_WEBDAV_URL \"${RAILIANCE_BACKUP_BAO_PATH}\" 2>/dev/null)}\"\ + \n}\n\nrailiance_backup_require_openbao_lane() {\n railiance_backup_load_openbao_lane\n\ + \ if [[ -z \"${RAILIANCE_BACKUP_NC_TOKEN:-}\" ]]; then\n echo \"ERROR: governed\ + \ backup credential unavailable (CCR-2026-0004)\" >&2\n return 1\n fi\n #\ + \ Build a file-drop URL only after the required token is present.\n : \"${RAILIANCE_BACKUP_NC_WEBDAV_URL:=https://nx4069.your-storageshare.de/public.php/dav/filesdrop/${RAILIANCE_BACKUP_NC_TOKEN}}\"\ + \n}\n\nrailiance_backup_require_tools() {\n # Prefer vendored static binaries\ + \ (activity-core hostPath / offline runners).\n local root vendor\n root=\"\ + $(cd \"$(dirname \"${BASH_SOURCE[0]}\")/..\" && pwd)\"\n vendor=\"${root}/tools/vendor/bin\"\ + \n if [[ -d \"$vendor\" ]]; then\n export PATH=\"${vendor}:${PATH}\"\n fi\n\ + \ if [[ -n \"${RAILIANCE_BACKUP_AGE_BIN:-}\" && -x \"${RAILIANCE_BACKUP_AGE_BIN}\"\ + \ ]]; then\n export PATH=\"$(dirname \"${RAILIANCE_BACKUP_AGE_BIN}\"):${PATH}\"\ + \n fi\n local t\n for t in age kubectl; do\n command -v \"$t\" >/dev/null\ + \ 2>&1 || { echo \"ERROR: missing required tool: $t (run tools/cmd/install-cnpg-backup-vendor-tools)\"\ + \ >&2; exit 1; }\n done\n # curl optional \u2014 upload falls back to python3\ + \ urllib (activity-core worker has no curl)\n if ! command -v curl >/dev/null\ + \ 2>&1 && ! command -v python3 >/dev/null 2>&1; then\n echo \"ERROR: need curl\ + \ or python3 for Nextcloud upload\" >&2\n exit 1\n fi\n}\n\nrailiance_backup_nc_upload()\ + \ {\n local file=\"$1\" remote_name=\"$2\"\n local dest=\"${RAILIANCE_BACKUP_NC_WEBDAV_URL%/}/${RAILIANCE_BACKUP_NC_PREFIX}/${remote_name}\"\ + \n if [[ \"$dest\" != https://* || \"$dest\" == *$'\\n'* || \"$dest\" == *$'\\\ + r'* ||\n \"${RAILIANCE_BACKUP_NC_TOKEN}\" == *$'\\n'* || \"${RAILIANCE_BACKUP_NC_TOKEN}\"\ + \ == *$'\\r'* ||\n \"$file\" == *$'\\n'* || \"$file\" == *$'\\r'* ]]; then\n\ + \ echo \"ERROR: invalid backup upload input\" >&2\n return 1\n fi\n if\ + \ command -v curl >/dev/null 2>&1; then\n # Credentials and credential-bearing\ + \ URLs travel over stdin, never argv.\n local config_url config_token config_file\ + \ status\n config_url=\"${dest//\\\\/\\\\\\\\}\"; config_url=\"${config_url//\\\ + \"/\\\\\\\"}\"\n config_token=\"${RAILIANCE_BACKUP_NC_TOKEN//\\\\/\\\\\\\\\ + }\"; config_token=\"${config_token//\\\"/\\\\\\\"}\"\n config_file=\"${file//\\\ + \\/\\\\\\\\}\"; config_file=\"${config_file//\\\"/\\\\\\\"}\"\n status=\"$(printf\ + \ 'url = \"%s\"\\nuser = \"%s:\"\\nupload-file = \"%s\"\\n' \\\n \"$config_url\"\ + \ \"$config_token\" \"$config_file\" | \\\n curl --disable --silent --fail\ + \ --proto '=https' --max-time 600 \\\n --header 'X-Requested-With: XMLHttpRequest'\ + \ \\\n --output /dev/null --write-out '%{http_code}' --config - 2>/dev/null)\"\ + \ \\\n || { echo \"ERROR: Nextcloud upload failed\" >&2; return 1; }\n \ + \ case \"$status\" in\n 200|201|204) ;;\n *) echo \"ERROR: Nextcloud\ + \ upload failed\" >&2; return 1 ;;\n esac\n return 0\n fi\n # Python fallback\ + \ (worker image)\n RAILIANCE_BACKUP_NC_TOKEN=\"${RAILIANCE_BACKUP_NC_TOKEN}\"\ + \ \\\n RAILIANCE_BACKUP_UPLOAD_URL=\"${dest}\" \\\n RAILIANCE_BACKUP_UPLOAD_FILE=\"\ + ${file}\" \\\n python3 - <<'PY' || { echo \"ERROR: Nextcloud upload failed\"\ + \ >&2; return 1; }\nimport os, urllib.request, urllib.error, base64, sys\nurl\ + \ = os.environ[\"RAILIANCE_BACKUP_UPLOAD_URL\"]\npath = os.environ[\"RAILIANCE_BACKUP_UPLOAD_FILE\"\ + ]\ntoken = os.environ[\"RAILIANCE_BACKUP_NC_TOKEN\"]\nclass NoRedirect(urllib.request.HTTPRedirectHandler):\n\ + \ def redirect_request(self, req, fp, code, msg, headers, newurl):\n \ + \ raise urllib.error.HTTPError(req.full_url, code, 'redirect refused', headers,\ + \ fp)\ntry:\n with open(path, 'rb') as source:\n chunks = iter(lambda:\ + \ source.read(1024 * 1024), b'')\n req = urllib.request.Request(url, data=chunks,\ + \ method=\"PUT\")\n req.add_header(\"Authorization\", \"Basic \" + base64.b64encode(f\"\ + {token}:\".encode()).decode())\n req.add_header(\"X-Requested-With\", \"\ + XMLHttpRequest\")\n req.add_header(\"Content-Type\", \"application/octet-stream\"\ + )\n req.add_header(\"Content-Length\", str(os.fstat(source.fileno()).st_size))\n\ + \ with urllib.request.build_opener(NoRedirect()).open(req, timeout=600)\ + \ as resp:\n if resp.status not in (200, 201, 204):\n \ + \ sys.exit(1)\nexcept Exception:\n # URLs may themselves contain credentials:\ + \ never emit exception text.\n sys.exit(1)\nPY\n}\n\nrailiance_backup_encrypt()\ + \ {\n local src=\"$1\" dest=\"$2\"\n age -r \"${RAILIANCE_BACKUP_AGE_PUBLIC_KEY}\"\ + \ -o \"$dest\" \"$src\"\n}\n\nrailiance_backup_prune_local() {\n local dir=\"\ + $1\" pattern=\"$2\" keep=\"${3:-7}\"\n find \"$dir\" -maxdepth 1 -name \"$pattern\"\ + \ -type f 2>/dev/null \\\n | sort -r | tail -n +\"$((keep + 1))\" | xargs -r\ + \ rm -f\n}\n\nrailiance_backup_record_success() {\n local stamp_dir=\"$1\" ts=\"\ + $2\"\n mkdir -p \"$stamp_dir\"\n echo \"$ts\" > \"${stamp_dir}/.last-success\"\ + \n echo \"$ts\" >> \"${stamp_dir}/success-log\"\n}\n" + lib__railiance-print.sh: "#!/usr/bin/env bash\n# lib/railiance-print.sh \u2014 shared\ + \ pretty printers for platform operator tools\nset -euo pipefail\n\nprint_hdr()\ + \ { printf \"\\n%s\\n\" \"$1\"; printf \"%0.s-\" $(seq 1 \"${#1}\"); echo; }\n\ + ok() { printf \" \u2705 %-12s %s\\n\" \"$1\" \"$2\"; }\nwarn() { printf \"\ + \ \u26A0\uFE0F %-12s %s\\n\" \"$1\" \"$2\"; }\nbad() { printf \" \u274C %-12s\ + \ %s\\n\" \"$1\" \"$2\"; }" + entrypoint: '#!/bin/sh + + exec /opt/railiance-backup-verified/tools/cmd/forgejo-backup "$@" + + ' diff --git a/k8s/backup-release/worker-patch.yaml b/k8s/backup-release/worker-patch.yaml new file mode 100644 index 0000000..46a8beb --- /dev/null +++ b/k8s/backup-release/worker-patch.yaml @@ -0,0 +1,29 @@ +spec: + template: + spec: + volumes: + - name: backup-verified + configMap: + name: backup-verified-debb9e4c59c9 + defaultMode: 365 + items: + - key: tools__cmd__forgejo-backup + path: tools/cmd/forgejo-backup + - key: scripts__capture_forgejo_archive.py + path: scripts/capture_forgejo_archive.py + - key: lib__railiance-backup-common.sh + path: lib/railiance-backup-common.sh + - key: lib__railiance-print.sh + path: lib/railiance-print.sh + - key: entrypoint + path: entrypoint + containers: + - name: worker + volumeMounts: + - name: backup-verified + mountPath: /opt/railiance-backup-verified + readOnly: true + - name: backup-verified + mountPath: /opt/railiance-platform/tools/cmd/forgejo-backup + subPath: entrypoint + readOnly: true diff --git a/workplans/ACTIVITY-WP-0042-platform-backup-restore-evidence-scheduling.md b/workplans/ACTIVITY-WP-0042-platform-backup-restore-evidence-scheduling.md index a8190a6..26c3658 100644 --- a/workplans/ACTIVITY-WP-0042-platform-backup-restore-evidence-scheduling.md +++ b/workplans/ACTIVITY-WP-0042-platform-backup-restore-evidence-scheduling.md @@ -267,7 +267,7 @@ id: ACTIVITY-WP-0042-T08 status: wait depends_on: [RPF-WP-0049-T05] priority: high -blocking_reason: "Platform must supply an authenticated replacement-lane/CAS and encrypted history upload/readback receipt; then a reviewed scoped worker bundle/hostPath and env refresh is required." +blocking_reason: "Provider prerequisite verified at lane KV4; scoped consumer bundle/hostPath and env refresh remain unperformed. Exact primary projection also blocked by existing historical identifier compatibility in repo-manager." state_hub_task_id: "020d19be-0576-5052-ad35-14b1d5a48fe1" ``` @@ -316,3 +316,21 @@ CAS from lane version 3 to 4; accept only the actual verified publication receipt, not that forecast. `runtime_consumers_refreshed: false` correctly leaves this T08 pending. Shared header source is `50da37c`; no broader backup readiness follows from initial root-history provider proof. + + +2026-10-03T19:56:59Z supersedes the missing replacement-lane prerequisite: +producer repair succeeded, lane CAS3->4 changed only token/URL, existing +escrow/unrelated fields preserved, encrypted upload201/readback200 and +independent decryption verified. Non-secret receipt retained in +`docs/evidence/ACTIVITY-WP-0042-backup-lane-repair-2026-10-03.json`. +`runtime_consumers_refreshed: false`; T08 remains waiting solely on reviewed +consumer delivery and its acceptance. Root-history provider proof is complete. + +Concrete review packet `k8s/backup-release/README.md` and its source-owned +immutable Forgejo bundle/worker patch are staged outside ordinary GitOps +rendering. No runtime apply/restart occurred. CNPG requires separate primary +checkout delivery. `rmgr sync --path . --push` currently fails preflight on +existing historic `custodian-WP-*` identifiers, before projection; source Git +and local Hub sync succeed. Do not renumber grandfathered IDs or claim exact +primary projection. This existing repo-manager compatibility issue must be +resolved by its owner before that projection-based rollout path.