Prepare guarded content release after inactive backup installation
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
This commit is contained in:
tegwick 2026-10-31 13:20:46 +01:00
parent 75343cfec1
commit 9c74884641
16 changed files with 972 additions and 20 deletions

View file

@ -43,7 +43,7 @@
| task | RAPPS-WP-0015-T04 | wait | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md |
| task | RAPPS-WP-0015-T05 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md |
| task | RAPPS-WP-0015-T06 | done | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md |
| task | RAPPS-WP-0015-T07 | todo | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md |
| task | RAPPS-WP-0015-T07 | wait | — | workplans/RAPPS-WP-0015-coulomb-personal-content-release.md |
| task | RAPPS-WP-0016-T01 | done | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md |
| task | RAPPS-WP-0016-T02 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md |
| task | RAPPS-WP-0016-T03 | wait | — | workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md |

View file

@ -0,0 +1,25 @@
{
"schema": "coulomb.packaged-content-lifecycle-tests.v1",
"source": "23ffe2cf4b214666754a2420558943438e54251a",
"image_id": "sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed",
"tests": 29,
"failures": 4,
"expected_baseline_regressions": true,
"failed_nodes": [
"test_process_death_during_copy_or_move_is_recoverable[copy_page-during_first_asset]",
"test_process_death_during_copy_or_move_is_recoverable[copy_page-during_second_asset]",
"test_process_death_during_copy_or_move_is_recoverable[transfer_page-during_first_asset]",
"test_process_death_during_copy_or_move_is_recoverable[transfer_page-during_second_asset]"
],
"errors": 0,
"skipped": 0,
"network": "none",
"read_only_root": true,
"application_source_overlay": false,
"test_tooling_injected": true,
"application_modules_from_image_verified": true,
"production_access": false,
"junit_sha256": "edc52bd3a321d028d255b3d478020b3383d94747957ffe2985583b1f0741cb85",
"driver_sha256": "85b65114997c7e7c5323cb77a0043e495ec132506eef07dd394c6a6361380d36",
"test_source_sha256": "84a25a22a3cbda397e39c0508db8ea5aa90e6c467ccfa7da2b568d174d1ec449"
}

View file

@ -0,0 +1,119 @@
{
"schema": "coulomb.content-lifecycle-candidate-build.v1",
"completed_at": "2026-10-31T11:58:26.697591+00:00",
"source": "f306dbfa2604e4815523667740be2925c333b84e",
"tag": "forgejo.coulomb.social/coulomb/coulomb-social:f306dbfa2604",
"image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec",
"registry_digest_independently_verified": true,
"source_revision_label_verified": true,
"build_context": "Clean git archive restricted to Dockerfile, dockerignore, lock, project, manage.py, app and entrypoint.",
"build_inputs": [
{
"path": "Dockerfile",
"sha256": "d0e197a18db8980eb3e51cca842d693e10ab0b4208627bba28691d10719d4a00"
},
{
"path": ".dockerignore",
"sha256": "901c8b95d95549d0e78c1c3d6d0538bf0252c8a77e9409129e0cadfa55c7c56c"
},
{
"path": "pyproject.toml",
"sha256": "5758c56fd1be89b973861ccc5ecbadb495e742ee13ebd7e19a221a0f119c54ee"
},
{
"path": "uv.lock",
"sha256": "e8ebb6fc4b5abd1ac80f1c9012f1983642d0528a8a213cfcc394261606fad887"
},
{
"path": "scripts/docker-entrypoint.sh",
"sha256": "70100256ca41ae6afe77078c810a8b6012472f0a741919f403f32088f98ab463"
}
],
"runtime": {
"previous": {
"python": "3.12.16",
"distributions": {
"h11": "0.16.0",
"httpcore": "1.0.9",
"psycopg": "3.3.4",
"joserfc": "1.7.4",
"anyio": "4.14.2",
"cryptography": "50.0.0",
"idna": "3.18",
"certifi": "2026.7.22",
"packaging": "26.3",
"python-decouple": "3.8",
"psycopg-binary": "3.3.4",
"sqlparse": "0.5.5",
"whitenoise": "6.12.0",
"cffi": "2.1.1",
"markdown": "3.10.3",
"webencodings": "0.5.1",
"pycparser": "3.0",
"bleach": "6.4.0",
"authlib": "1.7.2",
"django": "6.1",
"typing_extensions": "4.16.0",
"gunicorn": "26.0.0",
"asgiref": "3.12.1",
"httpx": "0.28.1",
"dj-database-url": "3.1.2"
}
},
"candidate": {
"python": "3.12.16",
"distributions": {
"h11": "0.16.0",
"httpcore": "1.0.9",
"psycopg": "3.3.4",
"joserfc": "1.7.4",
"anyio": "4.14.2",
"cryptography": "50.0.0",
"idna": "3.18",
"certifi": "2026.7.22",
"packaging": "26.3",
"python-decouple": "3.8",
"psycopg-binary": "3.3.4",
"sqlparse": "0.5.5",
"whitenoise": "6.12.0",
"cffi": "2.1.1",
"markdown": "3.10.3",
"webencodings": "0.5.1",
"pycparser": "3.0",
"bleach": "6.4.0",
"authlib": "1.7.2",
"django": "6.1",
"typing_extensions": "4.16.0",
"gunicorn": "26.0.0",
"asgiref": "3.12.1",
"httpx": "0.28.1",
"dj-database-url": "3.1.2"
}
},
"runtime_packages_equal": true,
"python_versions_equal": true,
"django_check": {
"returncode": 0,
"output": "System check identified no issues (0 silenced)."
},
"migration_drift": {
"returncode": 0,
"output": "No changes detected"
}
},
"source_delta_from_revision_19": [
"coulomb_social/apps/spaces/pageops.py"
],
"new_database_migrations": false,
"production_deployed": false,
"live_release_pins_modified": false,
"source_full_gate_tests": 546,
"packaged_lifecycle_tests": 29,
"previous_image_expected_lifecycle_failures": 4,
"owner_release_task": "RAPPS-WP-0015-T07",
"remaining": [
"Exact current-target release packet, paired rollback evidence and candidate/previous-image restored-store checks.",
"Attended image release and independent native postflight.",
"Productive lifecycle verdict; retention policy already accepted under c3a191f2-ce06-483f-82d7-ef8c54ea0d5e."
]
}

View file

@ -0,0 +1,20 @@
{
"schema": "coulomb.packaged-source-provenance.v1",
"image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec",
"network": "none",
"read_only_root": true,
"entrypoint_bypassed": true,
"database_contact": false,
"production_changes": false,
"comparisons": [
{
"commit": "f306dbfa2604e4815523667740be2925c333b84e",
"packaged_file_count": 80,
"source_file_count": 80,
"different_files": 0,
"all_packaged_source_bytes_match": true
}
],
"actual_packaged_manifest_sha256": "b5c8a16b4b3d30cc9d8a7a9f3ed61bd8cb0e182589aceb5e4127fff27a8ae482",
"dependency_or_build_reproducibility_proved": false
}

View file

@ -0,0 +1,65 @@
{
"schema": "coulomb.packaged-content-lifecycle-recovery.v1",
"status": "packaged_recovered_client_export_and_views_verified",
"source": "f306dbfa2604e4815523667740be2925c333b84e",
"image_id": "sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec",
"restore_receipt_sha256": "966d6640e42e2f4940c66d1cd5c8a802cd950de0305572f3c8b720ee3d81ae2b",
"client_export": {
"status": "recovered_client_export_verified",
"schema": "coulomb.client-dataset-export.v1",
"content_files": 85,
"records": {
"members.user": 1,
"members.member": 1,
"spaces.space": 1,
"spaces.spacemembership": 1
},
"admin_events": 0,
"archive_sha256": "e364a61570e0083e729a3f314eb707c57347200438bd98170c77b8f7fcc2b337",
"offboarding_complete": false,
"all_source_tables_compared": 13,
"all_source_files_compared": 86,
"archive_member_hashes_verified": true,
"selected_client_content_complete_locally": true,
"source_sql_and_content_unchanged": true,
"external_bindings_require_review": 0,
"sessions_and_credentials_excluded": true
},
"candidate_application": {
"status": "isolated_application_recovery_verified",
"recovered_spaces_with_content": 1,
"actual_pages_rendered_through_view": 40,
"restored_owner_access_verified": true,
"synthetic_viewer_reads": 1,
"synthetic_viewer_mutation_denials": 1,
"same_tenant_ungranted_page_denials": 40,
"fixture_transaction_rolled_back": true,
"production_access": false,
"external_identity_provider_used": false,
"real_browser_journey_verified": false
},
"previous_image_application": {
"status": "isolated_application_recovery_verified",
"recovered_spaces_with_content": 1,
"actual_pages_rendered_through_view": 40,
"restored_owner_access_verified": true,
"synthetic_viewer_reads": 1,
"synthetic_viewer_mutation_denials": 1,
"same_tenant_ungranted_page_denials": 40,
"fixture_transaction_rolled_back": true,
"production_access": false,
"external_identity_provider_used": false,
"real_browser_journey_verified": false
},
"source_content_files_unchanged": 86,
"all_tables_unchanged_after_both_view_checks": true,
"application_source_overlay": false,
"export_archive_removed": true,
"network_internal": true,
"published_ports": 0,
"production_access": false,
"production_restore": false,
"post_date_correction_data_claimed": true,
"retained_point_captured_at": "2026-10-31T07:13:52Z",
"driver_sha256": "92b66225e5f9bdf03d0a0999b613370c42341a2ceef468f1743fb4301884cb25"
}

View file

@ -0,0 +1,20 @@
{
"schema": "coulomb.packaged-content-lifecycle-tests.v1",
"source": "f306dbfa2604e4815523667740be2925c333b84e",
"image_id": "sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec",
"tests": 29,
"failures": 0,
"expected_baseline_regressions": false,
"failed_nodes": [],
"errors": 0,
"skipped": 0,
"network": "none",
"read_only_root": true,
"application_source_overlay": false,
"test_tooling_injected": true,
"application_modules_from_image_verified": true,
"production_access": false,
"junit_sha256": "f2968ef1048ec9af68a187b967d558db0f6ee76e765a8106fb7fab284953511f",
"driver_sha256": "85b65114997c7e7c5323cb77a0043e495ec132506eef07dd394c6a6361380d36",
"test_source_sha256": "84a25a22a3cbda397e39c0508db8ea5aa90e6c467ccfa7da2b568d174d1ec449"
}

View file

@ -0,0 +1,61 @@
{
"changed_columns": {
"django_session": [
"session_key",
"session_data",
"expire_date"
],
"members_member": [
"updated_at",
"last_login_at"
],
"members_user": [
"last_login"
]
},
"file_changes": [],
"retained_fingerprint": {
"business_tables": 12,
"migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af",
"business_rows": 92,
"business_rows_sha256": "6243955479f7861fcdcd79b62f7f1e9f9a27c05a17bd2c17b792ebc98b2fa7b6",
"content_files": 86,
"content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda",
"global_slug_constraint": true,
"tenant_slug_constraint": true,
"migration_0003_applied": true,
"duplicate_slug_groups": 0
},
"schema": "coulomb.content-release-row-drift.v1",
"changed_tables": [
{
"table": "django_session",
"retained_rows": 42,
"current_rows": 43,
"retained_sha256": "c958e33ec782f9f2fd1b3cceba4a64da0038fde51050f25905ae42de91ca6dde",
"current_sha256": "594e246ef6297d9be9b6e8d9eda076a5b2ab36afb9263606b719f64f51293ba6"
},
{
"table": "members_member",
"retained_rows": 1,
"current_rows": 1,
"retained_sha256": "4d465d2b9d1e6311e505f861f2ed4b67763a545a965f9a7d0c586a3d07e3e9a3",
"current_sha256": "ab82b3f2c5e49f8634c598c9642894747f4b6fcff0f5230ac27f41939feb8640"
},
{
"table": "members_user",
"retained_rows": 1,
"current_rows": 1,
"retained_sha256": "81ccb3a40191e910a68c101896df518a588c00231728934c4c900577f8ae4f12",
"current_sha256": "0fc7a71b4b80a71fbedce527bbd2cae3858cf6a062e5714fc231be13ff987256"
}
],
"all_other_business_tables_equal": true,
"migration_history_equal": true,
"content_equal": true,
"retained_point": "2026-10-31T07:13:52Z",
"current_business_rows": 93,
"current_business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca",
"private_row_values_printed": false,
"production_changes": false
}

View file

@ -0,0 +1,62 @@
{
"schema": "coulomb.content-release-target-observation.v1",
"observed_at": "2026-10-31T12:07:24.322343+00:00",
"cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786",
"deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe",
"pvc_uid": "9888abeb-9d52-4c70-a23d-b5494780d9c3",
"baseline_revision": 19,
"baseline_helm_manifest_sha256": "93cb277abd1aea0657be482e458dcfb268fe0bfbd45d90aa1faedcc7f5bce499",
"baseline_spec_sha256": "42e091e52d5dae3c1f986e4898c62057fa80846adfce049a75d8fbd4681180ea",
"candidate_spec_sha256": "c65cb691bab7054ae25de58cdb75933d383621ec4e049d2daa1b1bf52dce0bba",
"old_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed",
"candidate_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec",
"sole_ready_pvc_writer": true,
"existing_unmanaged_metadata_preserved": true,
"candidate_image_only_server_preview_verified": true,
"data": {
"business_tables": 12,
"migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af",
"business_rows": 93,
"business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca",
"content_files": 86,
"content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda",
"global_slug_constraint": true,
"tenant_slug_constraint": true,
"migration_0003_applied": true,
"duplicate_slug_groups": 0,
"actual_owner_pages_rendered": 40,
"unsaved_outsider_page_denials": 40,
"production_identity_or_grant_created": false
},
"matches_verified_revision19_data": false,
"installed_helper_sha256": {
"/opt/coulomb-social-backup/paired-backup.py": "43e119acb6a4933b4e144c349a091e074e1a91ab61dbe15a65c8264c1d47661a",
"/opt/coulomb-social-backup/snapshot-content-and-db.py": "dcc8dd2a3989f279597595f9260b31b24fcc9b6dc787f04c17ef0491c53d830d",
"/opt/railiance-platform/scripts/primary_application_archive.py": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d"
},
"timers": {
"coulomb-social-paired-backup.timer": {
"LoadState": "not-found",
"ActiveState": "inactive",
"UnitFileState": ""
},
"coulomb-social-backup-freshness.timer": {
"LoadState": "not-found",
"ActiveState": "inactive",
"UnitFileState": ""
}
},
"prerequisites": {
"custody": {
"exists": false
},
"installation": {
"exists": false
},
"route": {
"exists": false
}
},
"production_data_modified": false,
"release_executed": false
}

View file

@ -0,0 +1,58 @@
{
"schema": "coulomb.content-lifecycle-validation.v1",
"recorded_at": "2026-10-31T11:54:24.215512+00:00",
"status": "source_fix_verified",
"workplan_task": "CSOC-WP-0013-T05",
"bug": "A process exit during attachment creation left bytes absent from the copy/move cleanup journal; the orphan blocked retry.",
"regression_observed_before_fix": {
"baseline": "f70708839cd5ca43310f6891c8d94aaff915139c",
"operations": [
"copy_page",
"transfer_page"
],
"interruption": "partial first attachment flushed, immediate os._exit(86)",
"result": "both failed because the partial destination survived recovery"
},
"fix": [
"Refuse existing attachment entries before recording cleanup authority.",
"Persist intended new attachment name before exclusive creation under the content writer lock.",
"Flush recovery and attachment directory entries before destination publication."
],
"focused_cases": {
"total": 29,
"abrupt_child_process_exit": 14,
"existing_destination_collision": 8,
"prior_retention_and_recovery": 7
},
"full_gate": {
"command": "make check",
"tests": 546,
"failures": 0,
"errors": 0,
"skipped": 0,
"warnings": 69,
"junit_sha256": "dd3d0433c8109f00f6dddd119cfdf8281915f94da2da1f5e6009c31cfe76c65b",
"log_sha256": "eb59a394c95c5307c17069874640d1655ef61c5aa7bb426ef6d21db4929f748e"
},
"files": [
{
"path": "coulomb_social/apps/spaces/pageops.py",
"sha256": "055ef25b5efc75798cf17ffa68739de4d4099d2cab4d041e7c7eacdfaf43803e"
},
{
"path": "tests/test_content_recovery.py",
"sha256": "84a25a22a3cbda397e39c0508db8ea5aa90e6c467ccfa7da2b568d174d1ec449"
}
],
"policy": "User accepted retain-all default and separately reviewed manual cutoff; keep live page and all history of deleted pages.",
"live_purge_authorized": false,
"live_purge_executed": false,
"production_deployed": false,
"productive_acceptance": false,
"limits": [
"Child process termination on synthetic local files is proved; physical power-loss durability is not experimentally proved.",
"Cooperating application/import writers must use the content writer lock.",
"Release delivery and productive copy/move review remain open."
],
"policy_decision_id": "c3a191f2-ce06-483f-82d7-ef8c54ea0d5e"
}

View file

@ -0,0 +1,175 @@
#!/usr/bin/env python3
"""Node-side quiesced coulomb-social snapshot. Binary output stays private.
Run on railiance01 after reviewed live approval. Does not restore production.
Default is server dry-run; --execute briefly pauses the app and captures a
paired pg_dump/content archive. A separate isolated restore must validate it.
"""
from __future__ import annotations
import argparse
import fcntl
import hashlib
import json
import os
import signal
import stat
import subprocess
import sys
import time
from pathlib import Path
NS = 'coulomb-social'
IMAGE = ('forgejo.coulomb.social/coulomb/coulomb-social@sha256:'
'd3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec')
CLUSTER_UID = 'a553c742-0115-43d4-99a4-a5ca56fe0786'
LOCK_PATH = Path('/var/lib/coulomb-social-backup/snapshot.lock')
class Refused(RuntimeError):
pass
def run(args: list[str], *, payload: bytes | None = None, timeout: int = 180) -> bytes:
p = subprocess.run(args, input=payload, capture_output=True, timeout=timeout)
if p.returncode:
raise Refused('command_failed')
return p.stdout
def k(*args: str, payload: bytes | None = None) -> bytes:
return run(['kubectl', *args], payload=payload)
def document(*args: str) -> dict:
return json.loads(k(*args, '-o', 'json'))
def private_write(path: Path, value: bytes) -> None:
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
with os.fdopen(fd, 'wb') as f:
f.write(value)
def snapshot() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--execute', action='store_true')
parser.add_argument('--destination', type=Path, required=True)
args = parser.parse_args()
os.environ['KUBECONFIG'] = '/etc/rancher/k3s/k3s.yaml'
started = time.monotonic()
if document('get', 'namespace', 'kube-system')['metadata']['uid'] != CLUSTER_UID:
raise Refused('wrong_cluster')
dep = document('get', 'deployment', 'coulomb-social', '-n', NS)
spec = dep['spec']['template']['spec']
if dep['spec']['replicas'] != 1 or spec['containers'][0]['image'] != IMAGE:
raise Refused('unexpected_release')
if document('get', 'pvc', 'coulomb-social-content', '-n', NS)['status']['phase'] != 'Bound':
raise Refused('pvc_not_bound')
cluster = document('get', 'cluster', 'apps-pg', '-n', 'databases')
primary = cluster['status']['currentPrimary']
name = 'csoc-paired-snapshot-' + time.strftime('%Y%m%d%H%M%S', time.gmtime())
pod = {
'apiVersion': 'v1', 'kind': 'Pod',
'metadata': {'name': name, 'namespace': NS, 'labels': {'csoc-recovery': name}},
'spec': {
'restartPolicy': 'Never', 'automountServiceAccountToken': False,
'securityContext': {'runAsNonRoot': True, 'runAsUser': 10001,
'runAsGroup': 10001, 'fsGroup': 10001,
'fsGroupChangePolicy': 'OnRootMismatch'},
'containers': [{'name': 'snapshot', 'image': IMAGE,
'command': ['python', '-c', 'import time;time.sleep(1800)'],
'resources': {'requests': {'cpu': '10m', 'memory': '64Mi'},
'limits': {'cpu': '100m', 'memory': '256Mi'}},
'securityContext': {'allowPrivilegeEscalation': False,
'readOnlyRootFilesystem': True,
'capabilities': {'drop': ['ALL']}},
'volumeMounts': [{'name': 'content', 'mountPath': '/snapshot', 'readOnly': True}]}],
'volumes': [{'name': 'content', 'persistentVolumeClaim':
{'claimName': 'coulomb-social-content', 'readOnly': True}}],
},
}
body = json.dumps(pod).encode()
k('create', '--dry-run=server', '-f', '-', payload=body)
if not args.execute:
print(json.dumps({'status': 'reviewed', 'server_dry_run': True,
'image_pinned': True, 'pvc_read_only': True,
'db_scope': 'coulomb_social_db', 'replicas_restored_to': 1}))
return 0
dest = args.destination
if not dest.is_absolute() or dest.exists():
raise Refused('destination_must_be_new_absolute_directory')
dest.mkdir(mode=0o700)
paused = False
created = False
receipt = {'status': 'incomplete', 'app': 'coulomb-social',
'database': 'coulomb_social_db', 'image': IMAGE,
'started_at': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())}
try:
paused = True
k('scale', 'deployment/coulomb-social', '-n', NS, '--replicas=0')
k('wait', '--for=delete', 'pod', '-n', NS,
'-l', 'app.kubernetes.io/instance=coulomb-social', '--timeout=120s')
created = True
k('create', '-f', '-', payload=body)
k('wait', '--for=condition=Ready', 'pod/' + name, '-n', NS, '--timeout=120s')
dump = k('exec', '-n', 'databases', primary, '-c', 'postgres', '--',
'pg_dump', '-U', 'postgres', '-d', 'coulomb_social_db', '-Fc')
if not dump.startswith(b'PGDMP'):
raise Refused('invalid_pg_dump')
private_write(dest / 'database.dump', dump)
archive_code = ('import io,sys,tarfile; b=io.BytesIO(); '
't=tarfile.open(fileobj=b,mode="w:gz"); '
't.add("/snapshot",arcname="content"); '
't.close();sys.stdout.buffer.write(b.getvalue())')
content = k('exec', '-n', NS, name, '--', 'python', '-c', archive_code)
private_write(dest / 'content.tar.gz', content)
receipt.update(status='captured', writers_quiesced=True,
database_sha256=hashlib.sha256(dump).hexdigest(),
content_sha256=hashlib.sha256(content).hexdigest(),
database_bytes=len(dump), content_bytes=len(content))
finally:
try:
if created:
k('delete', 'pod', name, '-n', NS, '--wait=true', '--timeout=120s')
finally:
if paused:
k('scale', 'deployment/coulomb-social', '-n', NS, '--replicas=1')
k('rollout', 'status', 'deployment/coulomb-social', '-n', NS, '--timeout=180s')
receipt['app_resumed'] = True
receipt['elapsed_seconds'] = round(time.monotonic() - started, 2)
private_write(dest / 'receipt.json', json.dumps(receipt, indent=2).encode())
print(json.dumps(receipt))
return 0
def main() -> int:
if '--execute' not in sys.argv:
return snapshot()
LOCK_PATH.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
parent = LOCK_PATH.parent.lstat()
if (not stat.S_ISDIR(parent.st_mode) or parent.st_uid != os.geteuid()
or parent.st_mode & 0o077):
raise Refused('lock_directory_not_private')
fd = os.open(LOCK_PATH, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW, 0o600)
with os.fdopen(fd, 'w') as lock:
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
raise Refused('snapshot_already_running') from None
def terminate(signum, frame):
raise Refused('snapshot_interrupted')
previous = signal.signal(signal.SIGTERM, terminate)
try:
return snapshot()
finally:
signal.signal(signal.SIGTERM, previous)
if __name__ == '__main__':
try:
sys.exit(main())
except Exception as exc:
# Never expose child output, kubectl objects, credentials or raw exception messages.
print(json.dumps({'status': 'failed', 'failure_type': type(exc).__name__}))
sys.exit(1)

View file

@ -0,0 +1,50 @@
# Production overrides for coulomb-social.
# Non-secret values only. Secrets live in K8s Secret coulomb-social-env.
image:
repository: forgejo.coulomb.social/coulomb/coulomb-social
tag: "f306dbfa2604"
digest: "sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec"
# Public host: app.coulomb.social (parallel to Bubble on apex coulomb.social).
# Apex cutover is deferred until data + self-registration are ready.
env:
DJANGO_SETTINGS_MODULE: coulomb_social.settings.prod
ALLOWED_HOSTS: app.coulomb.social,localhost
CSRF_TRUSTED_ORIGINS: https://app.coulomb.social
CONTENT_ROOT: /app/var/content
DEFAULT_TENANT_ID: "tenant:coulomb"
OIDC_ENABLED: "true"
OIDC_ISSUER: https://kc.coulomb.social
OIDC_CLIENT_ID: coulomb-social
OIDC_REDIRECT_URI: https://app.coulomb.social/auth/callback/
OIDC_SCOPES: "openid profile email groups"
USER_ENGINE_BASE_URL: https://users.coulomb.social
USER_ENGINE_APPLICATION_ID: coulomb-social
USER_ENGINE_EXPECTED_AUDIENCE: user-engine-portal
NETKINGDOM_REGISTRATION_URL: https://users.92-205-62-239.nip.io/register
probes:
hostHeader: app.coulomb.social
path: /healthz
# One writable local-path volume; Recreate prevents concurrent old/new writers.
replicaCount: 1
deploymentStrategy: Recreate
persistence:
content:
enabled: true
storageClass: local-path
size: 2Gi
mountPath: /app/var/content
retain: true
# Wave-2 right-sizing (CSOC-WP-0017-T01, 2026-10-21 node budget): 7d p95
# 0.4m CPU / 146Mi. Limits stay at the chart defaults (1000m/1Gi).
resources:
requests:
cpu: 15m
memory: 176Mi
limits:
cpu: 1000m
memory: 1Gi

View file

@ -0,0 +1,69 @@
{
"schema": "coulomb.reviewed-image-release.v1",
"proposal_id": "CSOC-CONTENT-LIFECYCLE-20261031",
"scope": "ADMINISTER @ realm:kubernetes/railiance01; supervised founder execution only",
"operation": "image_only_existing_migration_0003",
"cluster_uid": "a553c742-0115-43d4-99a4-a5ca56fe0786",
"baseline_revision": 19,
"baseline_helm_manifest_sha256": "93cb277abd1aea0657be482e458dcfb268fe0bfbd45d90aa1faedcc7f5bce499",
"candidate_render_sha256": "1606725273089b83689850ad1c25d1834faa15e00c8cf8aa24e1053273af71bc",
"deployment_uid": "2dbe661a-d232-4477-bbd1-833813077fbe",
"pvc_uid": "9888abeb-9d52-4c70-a23d-b5494780d9c3",
"baseline_spec_sha256": "42e091e52d5dae3c1f986e4898c62057fa80846adfce049a75d8fbd4681180ea",
"candidate_spec_sha256": "c65cb691bab7054ae25de58cdb75933d383621ec4e049d2daa1b1bf52dce0bba",
"old_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:522bcd7fce92436765a60c1e4afa2598f3cc40913352153ff1c4319a99ca92ed",
"candidate_image": "forgejo.coulomb.social/coulomb/coulomb-social@sha256:d3f2f5bb90f0ba79f4e1d8098d2eb06f8940b8789e5ecb851914bbab364597ec",
"product_source": "f306dbfa2604e4815523667740be2925c333b84e",
"expected_data": {
"business_tables": 12,
"migration_rows_sha256": "43471516387c0e2a3d35971033ed6af330e2963efe4da93293d97633825d04af",
"business_rows": 93,
"business_rows_sha256": "7953b37b02b80d89ca6869dab6b2b0b996541de20ca0dd1b294a5f25572fb2ca",
"content_files": 86,
"content_files_sha256": "ed535246f4e8f46fcd7ce01f936127996aff873c6936b297f7272194b594aeda",
"global_slug_constraint": true,
"tenant_slug_constraint": true,
"migration_0003_applied": true,
"duplicate_slug_groups": 0
},
"public_recipient": "age1qm30jk8q0cezey652dk2dhcr3d9vs528v6lg8mavl6d4q3e3vgnqmrmyy3",
"installed_helper_hashes": {
"/opt/coulomb-social-backup/paired-backup.py": "d161c742d1a6a182460a57d0a1719591ab58fc94006f55169ad3d74442a93630",
"/opt/coulomb-social-backup/snapshot-content-and-db.py": "dcc8dd2a3989f279597595f9260b31b24fcc9b6dc787f04c17ef0491c53d830d",
"/opt/coulomb-social-backup/mirror-paired-backup.py": "9ceb26d9083af016c4b787907e4dce7bdf437c782fb2d15eb28d9e334e6e431c",
"/opt/coulomb-social-backup/check-paired-backup-health.py": "2d40ec6e4c59ea7fe1d46065d197b9a87ca6811f023cc6f3e20e4184a3490566",
"/opt/coulomb-social-backup/backup_capacity.py": "cb699a79e67b129239b5aa5d6f3b0d4ffaa9773a8b073227ad17fdecb5b9d893",
"/opt/railiance-platform/scripts/application_recovery_identities.py": "20f56255ed6eeec4aafe33f142bc3ee03f623d4957a7b0cabb741d340f15f79d",
"/opt/railiance-platform/scripts/application_recovery_mirror.py": "d48ab95e89b9eeae36269d853ebe4c44c22e1f57d4e14224f03d0b9aeeb34178",
"/opt/railiance-platform/scripts/application_recovery_resources.py": "ab70ec341e738272730eb0daf76840d0bca807caeb3c4e4c2357f162fbed4498",
"/opt/railiance-platform/scripts/primary_application_archive.py": "a3f0a61e869a6012edb4b5288be14e3b0c4a9a1273b090320ab146d12e5aa94d",
"/opt/coulomb-social-backup/backup_alerts.py": "07522437d4276b92f0a44149adc6c9e81bb18dbe59ce0f1f9e58a48347700d0e",
"/opt/coulomb-social-backup/notify-backup-alert.py": "0bd53bc5ad22d8bcb0326fb18a17737744b06416fe785dad077308e45f051d5a",
"/opt/coulomb-social-backup/observe-backup-alerts.py": "17caf4fa65283242f618b039ff1d018801c9890ea2a9178912cbdd4b50ee2664"
},
"clipboard_static_sha256": "347eabe0c8d93bf439df02b5a08a7ea71004600e6ff4e8a42cecbee52a9f0413",
"fresh_backup_before_helm": true,
"requires_dataset_equality_before_and_after": true,
"node_request_percent_limit": 90,
"helm_atomic_timeout_seconds": 300,
"rollback": "Restore Helm revision19/image522bcd7f and verified snapshot pin; preserve all post-capture data. No production data restore or schema downgrade.",
"approval_status": "prepared_not_admitted",
"recovery_rehearsal_scope": "Retained independently recovered 2026-10-31T07:13:52Z point; candidate and previous image preserve all 13 source tables and 86 files. Live observation now has93 business rows versus92 in that point; exact fresh paired backup/readback is required before Helm. Independent restore of the new point remains consumer follow-through. Read-only drift comparison confines differences to sessions and user/member login timestamps; all content and migration history match the retained point.",
"rehearsal_evidence_sha256": {
"content-lifecycle-candidate-build-2026-10-31.json": "83ad2ec41c85f8bb5087a250fdb91126ed92f12351cdc72b353c6f0e1a26452b",
"content-lifecycle-candidate-provenance-2026-10-31.json": "35ed909e0a2fa5c57cea1fcc4fbebb877eae559ca9dc753c3da00f84806d7b73",
"content-lifecycle-candidate-tests-2026-10-31.json": "27a75cfee6bbda41f377b73d6a6c7ecde0c54806c113f1be5223dd2f2cf39b3b",
"content-lifecycle-baseline-tests-2026-10-31.json": "de14e4e10337990ccec1c483f7d943bdc7e11b03aa813aff47d00a9b38a07b65",
"content-lifecycle-candidate-recovery-2026-10-31.json": "8d15172e5e330d01f153336e67f6bffb4363d571530c31ea05db872ca10807a9",
"content-lifecycle-validation-2026-10-31.json": "cbb093af470588ca6e8eb3ec5bc1ccee0be537ab37a46625b6ac267c17343d99",
"content-lifecycle-release-target-2026-10-31.json": "e9b50be3c8284c5a9ae96d75b800cbc4090580fa811c2ecd098d73ce8cbee74e",
"content-lifecycle-release-row-drift-2026-10-31.json": "b10f65bf55e900199ae6037767f32e8901aaccb6f7f3a86461f13dd02ec8efce"
},
"new_resources_committed": 0,
"autopilot_or_eur_budget_claimed": false,
"values_file": "release/content-lifecycle-values.yaml",
"snapshot_file": "release/content-lifecycle-snapshot.py",
"evidence_directory": "release/content-lifecycle-evidence",
"prerequisite_installation_digest": "sha256:8f442bf0ebed8bbf3b7f227b52081df52613f3f5f6874c4a59511cfc2ba06d42",
"requires_inactive_backup_units": true
}

View file

@ -236,3 +236,129 @@ def test_known_candidate_helper_can_be_rolled_back(tmp_path, monkeypatch):
# A completed inverse can be checked again without changing it.
instance.restore_pin(b"old pin")
assert pin.read_bytes() == b"old pin"
def test_missing_installation_refuses_before_execution_journal(tmp_path, monkeypatch):
root, _ = capsule(tmp_path)
plan = json.loads((root / 'manifest.json').read_text())
plan['prerequisite_installation_digest'] = 'sha256:' + 'a' * 64
(root / 'manifest.json').write_text(json.dumps(plan))
digest = 'sha256:' + release.sha(release.canonical(plan))
monkeypatch.setattr(release.os, 'geteuid', lambda: 0)
monkeypatch.setattr(release, 'JOURNAL_ROOT', tmp_path / 'journal')
monkeypatch.setattr(release, 'INSTALL_RECEIPT', tmp_path / 'missing.json')
with pytest.raises(release.Refused, match='installation_required_before_image'):
release.Release(root, digest)
assert not (tmp_path / 'journal').exists()
@pytest.fixture
def installed(monkeypatch):
instance = object.__new__(release.Release)
digest = 'sha256:' + 'a' * 64
instance.plan = {'prerequisite_installation_digest': digest}
receipt = {'schema': 'coulomb.backup-source-supervised-installation.v1',
'proposal_digest': digest, 'status': 'sources_installed_units_inactive_verified',
'verified_targets': 19, 'timers_activated': False}
path = SimpleNamespace(parents=[], lstat=lambda: SimpleNamespace(
st_mode=stat.S_IFREG | 0o600, st_uid=0, st_nlink=1),
read_bytes=lambda: json.dumps(receipt).encode())
monkeypatch.setattr(release, 'INSTALL_RECEIPT', path)
state = {'active': 'inactive', 'enabled': 'disabled', 'jobs': b''}
def command(arguments):
if arguments[1] == 'list-jobs':
return SimpleNamespace(stdout=state['jobs'])
return SimpleNamespace(stdout=(f"LoadState=loaded\nActiveState={state['active']}\n"
f"UnitFileState={state['enabled']}\n").encode())
instance.command = Mock(side_effect=command)
return instance, receipt, state
def test_completed_installer_and_inactive_units_admit_release_prerequisite(installed):
instance, _, _ = installed
instance.verify_installation_prerequisite()
assert instance.command.call_count == 5
@pytest.mark.parametrize('field,value', [
('status', 'installation_requires_inspection'), ('verified_targets', 18),
('proposal_digest', 'sha256:' + 'b' * 64), ('timers_activated', True),
])
def test_wrong_or_incomplete_installation_never_checks_or_changes_units(installed, field, value):
instance, receipt, _ = installed
receipt[field] = value
with pytest.raises(release.Refused, match='verified_inactive_backup_installation'):
instance.verify_installation_prerequisite()
instance.command.assert_not_called()
@pytest.mark.parametrize('field,value', [('active', 'active'), ('enabled', 'enabled'),
('jobs', b'17 coulomb-social-paired-backup.service start waiting\n'),
('jobs', b'unrecognized job format\n')])
def test_active_enabled_or_queued_backup_refuses_image_release(installed, field, value):
instance, _, state = installed
state[field] = value
with pytest.raises(release.Refused):
instance.verify_installation_prerequisite()
assert all(c.args[0][1] in {'show', 'list-jobs'} for c in instance.command.call_args_list)
def test_an_explicit_release_plan_seals_its_own_artifacts(tmp_path, monkeypatch):
monkeypatch.setattr(launcher, 'ROOT', tmp_path)
paths = ['tools/coulomb-image-release.py', 'tools/coulomb_image_release_node.py',
'tools/coulomb_image_probe.py', 'release/next-values.yaml',
'release/next-snapshot.py', 'charts/coulomb-social/Chart.yaml',
'release/next-evidence/proof.json']
for name in paths:
path = tmp_path / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(('synthetic ' + name).encode())
plan = {'values_file': paths[3], 'snapshot_file': paths[4],
'evidence_directory': 'release/next-evidence',
'rehearsal_evidence_sha256': {'proof.json': release.sha((tmp_path / paths[-1]).read_bytes())}}
path = tmp_path / 'release/next.json'
path.write_text(json.dumps(plan))
prepared, digest, content = launcher.prepare(path)
assert set(content) == set(paths) | {'manifest.json'}
assert digest == 'sha256:' + release.sha(release.canonical(prepared))
plan['values_file'] = '../outside.yaml'
path.write_text(json.dumps(plan))
with pytest.raises(RuntimeError, match='release_paths'):
launcher.prepare(path)
@pytest.mark.parametrize('name', ['release/missing.yaml', '../outside.yaml', '/outside.yaml'])
def test_node_refuses_unsealed_artifact_reference_before_journal(tmp_path, monkeypatch, name):
root, _ = capsule(tmp_path)
plan = json.loads((root / 'manifest.json').read_text())
plan['values_file'] = name
(root / 'manifest.json').write_text(json.dumps(plan))
monkeypatch.setattr(release.os, 'geteuid', lambda: 0)
monkeypatch.setattr(release, 'JOURNAL_ROOT', tmp_path / 'journal')
with pytest.raises(release.Refused, match='reviewed_release_artifact'):
release.Release(root, 'sha256:' + release.sha(release.canonical(plan)))
assert not (tmp_path / 'journal').exists()
def test_initializer_refusal_is_structured_and_never_calls_execute(monkeypatch, capsys):
monkeypatch.setattr(release, 'Release', Mock(side_effect=release.Refused(
'inactive_backup_installation_required_before_image_release')))
monkeypatch.setattr(release.argparse.ArgumentParser, 'parse_args', lambda _: SimpleNamespace(
capsule=Path('/synthetic'), expect_digest='sha256:' + 'a' * 64,
execute=False, preflight=True))
with pytest.raises(SystemExit):
release.main()
result = json.loads(capsys.readouterr().out)
assert result['execution_started'] is False
assert result['failure_reason'] == 'inactive_backup_installation_required_before_image_release'
def test_custom_snapshot_artifact_is_used_for_recognized_pin_rollback(tmp_path, monkeypatch):
path = tmp_path / 'snapshot.py'
path.write_bytes(b'new reviewed pin')
monkeypatch.setattr(release, 'SNAPSHOT', path)
instance = object.__new__(release.Release)
instance.plan = {'snapshot_file': 'release/next-snapshot.py',
'files': {'release/next-snapshot.py': release.sha(path.read_bytes())}}
instance.restore_pin(b'previous pin')
assert path.read_bytes() == b'previous pin'

View file

@ -26,20 +26,32 @@ def canonical(value):
return json.dumps(value, sort_keys=True, separators=(",", ":")).encode()
def prepare():
plan = json.loads(PLAN.read_text())
def prepare(plan_path=None):
plan_path = plan_path or PLAN
if plan_path.is_symlink() or not plan_path.resolve().is_relative_to(ROOT.resolve()):
raise RuntimeError("reviewed_plan_inside_owner_repository_required")
plan = json.loads(plan_path.read_text())
values = plan.get("values_file", "release/identity-export-values.yaml")
snapshot = plan.get("snapshot_file", "release/identity-export-snapshot.py")
evidence = plan.get("evidence_directory", "release/identity-export-evidence")
for name in (values, snapshot, evidence):
if (not isinstance(name, str) or Path(name).is_absolute()
or ".." in Path(name).parts or not name.startswith("release/")):
raise RuntimeError("reviewed_release_paths_required")
if not all((ROOT / name).is_file() for name in (values, snapshot)):
raise RuntimeError("reviewed_release_artifacts_missing")
paths = [
ROOT / name
for name in (
"tools/coulomb-image-release.py",
"tools/coulomb_image_release_node.py",
"tools/coulomb_image_probe.py",
"release/identity-export-values.yaml",
"release/identity-export-snapshot.py",
values,
snapshot,
)
]
paths += sorted((ROOT / "charts/coulomb-social").rglob("*"))
paths += sorted((ROOT / "release/identity-export-evidence").glob("*.json"))
paths += sorted((ROOT / evidence).glob("*.json"))
paths = [p for p in paths if p.is_file()]
if any(p.is_symlink() or not p.resolve().is_relative_to(ROOT) for p in paths):
raise RuntimeError("regular_reviewed_files_required")
@ -48,7 +60,7 @@ def prepare():
name: hashlib.sha256(data).hexdigest() for name, data in content.items()
}
for name, expected in plan["rehearsal_evidence_sha256"].items():
if plan["files"].get("release/identity-export-evidence/" + name) != expected:
if plan["files"].get(evidence + "/" + name) != expected:
raise RuntimeError("reviewed_candidate_evidence_changed")
digest = "sha256:" + hashlib.sha256(canonical(plan)).hexdigest()
content["manifest.json"] = canonical(plan)
@ -61,10 +73,11 @@ def main():
parser.add_argument("--execute", action="store_true")
parser.add_argument("--expect-digest")
parser.add_argument("--output", type=Path)
parser.add_argument("--plan", type=Path, default=PLAN)
args = parser.parse_args()
if args.preflight and args.execute:
raise SystemExit("choose read-only preflight or attended execution")
plan, digest, content = prepare()
plan, digest, content = prepare(args.plan)
if args.expect_digest and args.expect_digest != digest:
raise SystemExit("reviewed proposal changed; do not execute")
if not args.preflight and not args.execute:

View file

@ -22,6 +22,9 @@ from pathlib import Path
JOURNAL_ROOT = Path("/var/lib/coulomb-social-backup/releases")
SNAPSHOT = Path("/opt/coulomb-social-backup/snapshot-content-and-db.py")
INSTALL_RECEIPT = Path(
"/var/lib/coulomb-social-backup-installations/20261031bacc0001/receipt.json"
)
def canonical(value):
@ -88,6 +91,12 @@ class Release:
raise Refused("attended_founder_root_execution_required")
self.env = dict(os.environ, KUBECONFIG="/etc/rancher/k3s/k3s.yaml")
self.read_only = read_only
for field in ("values_file", "snapshot_file"):
if field in self.plan:
name = self.plan[field]
if (not isinstance(name, str) or not name.startswith("release/")
or ".." in Path(name).parts or name not in frozen):
raise Refused("reviewed_release_artifact_required")
self.receipt = {
"schema": "coulomb.guarded-image-release.v1",
"proposal_digest": expected,
@ -97,6 +106,7 @@ class Release:
"timers_activated": False,
}
self.journal = JOURNAL_ROOT / expected.removeprefix("sha256:")
self.verify_installation_prerequisite()
if read_only:
return
if self.journal.exists():
@ -215,7 +225,52 @@ class Release:
or actual.get("migration_0003_applied") is not True):
raise Refused("already_migrated_baseline_required")
def artifact_name(self, field):
return self.plan.get(field, {
"values_file": "release/identity-export-values.yaml",
"snapshot_file": "release/identity-export-snapshot.py",
}[field])
def verify_installation_prerequisite(self):
expected = self.plan.get("prerequisite_installation_digest")
if expected is None:
return
if not re.fullmatch(r"sha256:[a-f0-9]{64}", expected):
raise Refused("reviewed_installation_digest_required")
try:
info = INSTALL_RECEIPT.lstat()
except FileNotFoundError:
raise Refused("inactive_backup_installation_required_before_image_release") from None
if (not stat.S_ISREG(info.st_mode) or info.st_uid != 0
or info.st_mode & 0o077 or info.st_nlink != 1
or any(p.is_symlink() for p in INSTALL_RECEIPT.parents)):
raise Refused("private_installation_receipt_required")
receipt = json.loads(INSTALL_RECEIPT.read_bytes())
if (receipt.get("schema") != "coulomb.backup-source-supervised-installation.v1"
or receipt.get("proposal_digest") != expected
or receipt.get("status") != "sources_installed_units_inactive_verified"
or receipt.get("verified_targets") != 19
or receipt.get("timers_activated") is not False):
raise Refused("verified_inactive_backup_installation_required")
for name in ("paired-backup", "backup-freshness"):
for suffix in ("service", "timer"):
unit = f"coulomb-social-{name}.{suffix}"
raw = self.command(["systemctl", "show", unit,
"--property=LoadState,ActiveState,UnitFileState"]).stdout
state = dict(line.split("=", 1) for line in raw.decode().splitlines())
if (state.get("LoadState") != "loaded" or state.get("ActiveState") != "inactive"
or state.get("UnitFileState") not in {"disabled", "static"}):
raise Refused("backup_units_must_remain_inactive_for_image_release")
jobs = self.command(["systemctl", "list-jobs", "--no-legend", "--no-pager",
"--plain"]).stdout.decode().splitlines()
for line in jobs:
fields = line.split()
if (len(fields) != 4 or not fields[0].isdecimal()
or fields[1].startswith("coulomb-social-")):
raise Refused("queued_backup_job_requires_review")
def preflight(self):
self.verify_installation_prerequisite()
self.verify_installed_helpers()
if (
self.kdoc("get", "namespace", "kube-system")["metadata"]["uid"]
@ -301,7 +356,7 @@ class Release:
"coulomb-social",
str(self.root / "charts/coulomb-social"),
"-f",
str(self.root / "release/identity-export-values.yaml"),
str(self.root / self.artifact_name("values_file")),
"-n",
"coulomb-social",
]
@ -319,7 +374,7 @@ class Release:
"coulomb-social",
str(self.root / "charts/coulomb-social"),
"-f",
str(self.root / "release/identity-export-values.yaml"),
str(self.root / self.artifact_name("values_file")),
"-n",
"coulomb-social",
"--dry-run=server",
@ -386,7 +441,7 @@ class Release:
path = SNAPSHOT
if (
sha(path.read_bytes())
== self.plan["files"]["release/identity-export-snapshot.py"]
== self.plan["files"][self.artifact_name("snapshot_file")]
):
self.replace_pin(path, old)
elif sha(path.read_bytes()) != sha(old):
@ -424,7 +479,7 @@ class Release:
"coulomb-social",
str(self.root / "charts/coulomb-social"),
"-f",
str(self.root / "release/identity-export-values.yaml"),
str(self.root / self.artifact_name("values_file")),
"-n",
"coulomb-social",
"--atomic",
@ -461,7 +516,7 @@ class Release:
):
raise Refused("snapshot_pin_drifted")
self.replace_pin(
path, (self.root / "release/identity-export-snapshot.py").read_bytes()
path, (self.root / self.artifact_name("snapshot_file")).read_bytes()
)
self.command(
[
@ -526,8 +581,9 @@ def main():
return
if args.execute and args.preflight:
raise SystemExit("choose read-only preflight or attended execution")
release = Release(args.capsule, args.expect_digest, read_only=args.preflight)
release = None
try:
release = Release(args.capsule, args.expect_digest, read_only=args.preflight)
if args.preflight:
release.preflight()
print(
@ -542,11 +598,15 @@ def main():
return
print(json.dumps(release.execute()))
except Exception as error: # noqa: BLE001 -- private child diagnostics are suppressed
release.receipt.update(
status="release_refused_or_failed", failure_type=type(error).__name__
)
release.save()
print(json.dumps(release.receipt))
receipt = release.receipt if release is not None else {
"proposal_digest": args.expect_digest, "execution_started": False,
}
receipt.update(status="release_refused_or_failed", failure_type=type(error).__name__)
if isinstance(error, Refused):
receipt["failure_reason"] = str(error)
if release is not None:
release.save()
print(json.dumps(receipt))
raise SystemExit(1) from None

View file

@ -428,9 +428,13 @@ T04/T09. No cluster operation occurred; future release proposals bind the new so
```task
id: RAPPS-WP-0015-T07
status: todo
status: wait
priority: high
needs_human: false
wait_kind: external
blocking_reason: "Prepared image packet c9c32316 waits for CSOC-WP-0012-T08 inactive installation8f442bf0; native preflight confirms its receipt is absent."
depends_on:
- CSOC-WP-0012-T08
state_hub_task_id: "8f448934-bdfe-539f-8cab-a739ec752491"
```
@ -453,3 +457,28 @@ its durable return. Current verified revision19 remains authoritative until that
return; no image deploy is implied by publication. Productive review stays with
CSOC-WP-0013-T05/T03. Retention policy c3a191f2 is accepted; no cutoff or purge was
authorized. Do not replay the completed identity image release.
2026-10-31 preparation return: exact20-member proposal
`sha256:c9c3231620721ef3ea9b2bf22fc6c62fe3ed8efb91386cf7ec96acc6959dae08`
is prepared with a separate plan/values/snapshot/evidence set. The existing launcher
accepts an explicit reviewed plan; historical frozen capsules stay intact.40
controller tests, Ruff and Helm lint pass. Native image-only server preview preserves
the complete current specification and unmanaged metadata. Candidate/previous-image
checks on the independent07:13 recovery point render40owner pages, deny40outsiders,
verify85export files, and preserve all13tables/86files. Current target remains
revision19;93business rows reflect a new session plus login timestamp changes.
Content and migration history match the retained point; private values are not exposed.
Ordering is material: the pending installer8f442bf0 pins snapshotdcc8dd2a. Deploying
this image first would stale that reviewed packet. The new controller therefore
requires that exact completed installation receipt and currently inactive/disabled
backup units before creating its release journal, then pins all12 installed Python
helpers. Native full preflight correctly refuses the absent installation receipt;
it is not a pass and no image execution occurred. Historical custodyfa2763d1,
route4d848106 and installer8f442bf0 packets are unchanged. After their returns,
re-run the full image preflight before admitting execution; finish native service
proof/schedule activation afterward. Consumer review commands and exact scope:
coulomb-social/docs/usecases/sharing-portability-review.md, lifecycle release section.
Evidence: content-lifecycle-release-{target,row-drift,preflight,proposal,preparation}
JSON files in the consumer docs/release directory. T07 is now external wait for T08;
product policy is settled, and productive lifecycle acceptance remains open.