| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263 |
- """Prepare this release and verified backups without changing the running app."""
- import hashlib
- import json
- import os
- import shutil
- import subprocess
- import tarfile
- from pathlib import Path
- os.umask(0o077)
- root = Path('/opt/chengchuan-relay')
- release = root / 'releases/optional-controls-20260915'
- backup = root / 'backups/before-optional-controls-20260915'
- evidence = root / 'evidence/optional-controls-20260915'
- current = root / 'feature-source-next'
- archive = release / 'source.tar.gz'
- assert hashlib.sha256(archive.read_bytes()).hexdigest() == 'c67137e3567f9b59d7069af74bd52d489edbf7e85625f5c4f1df1a2955ed444f'
- source = release / 'source'
- source.mkdir(exist_ok=False)
- with tarfile.open(archive) as package:
- for entry in package.getmembers():
- assert entry.isfile() and not entry.name.startswith('/') and '..' not in Path(entry.name).parts
- package.extractall(source, filter='data')
- manifest = json.loads((source / 'RELEASE_SOURCE_MANIFEST.json').read_text())['files']
- assert all(hashlib.sha256((source / name).read_bytes()).hexdigest() == value for name, value in manifest.items())
- new_migrations, modified_migrations = [], []
- for path in sorted((source / 'backend/migrations').glob('*.sql')):
- old = current / 'backend/migrations' / path.name
- if not old.exists():
- new_migrations.append(path.name)
- elif path.read_bytes() != old.read_bytes():
- modified_migrations.append(path.name)
- assert not modified_migrations, modified_migrations
- assert new_migrations == ['248_intelligent_assessment_v2.sql'], new_migrations
- shutil.copy2(current / 'deploy/.env', backup / 'app.env')
- shutil.copy2(current / 'deploy/compose.relay.yml', backup / 'compose.relay.yml')
- with (backup / 'source-before.tar.gz').open('xb') as output:
- subprocess.run(['tar', '-czf', '-', '-C', str(current), '.'], stdout=output, check=True)
- with (backup / 'database-before.dump').open('xb') as output:
- subprocess.run(['docker', 'exec', 'chengchuan-relay-postgres-1', 'pg_dump', '-U', 'relay', '-d', 'relay', '-Fc'], stdout=output, check=True)
- with (backup / 'database-before.dump').open('rb') as dump:
- listing = subprocess.check_output(['docker', 'exec', '-i', 'chengchuan-relay-postgres-1', 'pg_restore', '--list'], stdin=dump)
- assert b'TABLE DATA' in listing
- data = subprocess.check_output(['docker', 'inspect', 'chengchuan-relay-app-1'])
- (backup / 'app-inspect.private.json').write_bytes(data)
- app = json.loads(data)[0]
- volume = next(m['Source'] for m in app['Mounts'] if m['Destination'] == '/app/data')
- with (backup / 'app-data.tar.gz').open('xb') as output:
- subprocess.run(['tar', '-czf', '-', '-C', volume, '.'], stdout=output, check=True)
- shutil.copy2(current / 'deploy/.env', source / 'deploy/.env')
- settings = (source / 'deploy/.env').read_text().splitlines()
- settings = [line for line in settings if not line.startswith('RELAY_VERSION=')]
- settings.append('RELAY_VERSION=optional-controls-20260915')
- (source / 'deploy/.env').write_text('\n'.join(settings) + '\n')
- subprocess.run(['docker','compose','--env-file','.env','-f','compose.relay.yml','config','--quiet'],cwd=source/'deploy',check=True)
- result = {'old_image': app['Image'], 'old_tag': app['Config']['Image'], 'release': str(source),
- 'new_migrations': new_migrations, 'modified_migrations': modified_migrations,
- 'source_files_verified': len(manifest), 'backups': {}}
- for name in ['source-before.tar.gz', 'database-before.dump', 'app-data.tar.gz']:
- path = backup / name
- result['backups'][name] = {'bytes': path.stat().st_size, 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()}
- (evidence / 'preparation.json').write_text(json.dumps(result, indent=2))
- print(json.dumps(result, indent=2))
|