prepare-optional-release.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
  1. """Prepare this release and verified backups without changing the running app."""
  2. import hashlib
  3. import json
  4. import os
  5. import shutil
  6. import subprocess
  7. import tarfile
  8. from pathlib import Path
  9. os.umask(0o077)
  10. root = Path('/opt/chengchuan-relay')
  11. release = root / 'releases/optional-controls-20260915'
  12. backup = root / 'backups/before-optional-controls-20260915'
  13. evidence = root / 'evidence/optional-controls-20260915'
  14. current = root / 'feature-source-next'
  15. archive = release / 'source.tar.gz'
  16. assert hashlib.sha256(archive.read_bytes()).hexdigest() == 'c67137e3567f9b59d7069af74bd52d489edbf7e85625f5c4f1df1a2955ed444f'
  17. source = release / 'source'
  18. source.mkdir(exist_ok=False)
  19. with tarfile.open(archive) as package:
  20. for entry in package.getmembers():
  21. assert entry.isfile() and not entry.name.startswith('/') and '..' not in Path(entry.name).parts
  22. package.extractall(source, filter='data')
  23. manifest = json.loads((source / 'RELEASE_SOURCE_MANIFEST.json').read_text())['files']
  24. assert all(hashlib.sha256((source / name).read_bytes()).hexdigest() == value for name, value in manifest.items())
  25. new_migrations, modified_migrations = [], []
  26. for path in sorted((source / 'backend/migrations').glob('*.sql')):
  27. old = current / 'backend/migrations' / path.name
  28. if not old.exists():
  29. new_migrations.append(path.name)
  30. elif path.read_bytes() != old.read_bytes():
  31. modified_migrations.append(path.name)
  32. assert not modified_migrations, modified_migrations
  33. assert new_migrations == ['248_intelligent_assessment_v2.sql'], new_migrations
  34. shutil.copy2(current / 'deploy/.env', backup / 'app.env')
  35. shutil.copy2(current / 'deploy/compose.relay.yml', backup / 'compose.relay.yml')
  36. with (backup / 'source-before.tar.gz').open('xb') as output:
  37. subprocess.run(['tar', '-czf', '-', '-C', str(current), '.'], stdout=output, check=True)
  38. with (backup / 'database-before.dump').open('xb') as output:
  39. subprocess.run(['docker', 'exec', 'chengchuan-relay-postgres-1', 'pg_dump', '-U', 'relay', '-d', 'relay', '-Fc'], stdout=output, check=True)
  40. with (backup / 'database-before.dump').open('rb') as dump:
  41. listing = subprocess.check_output(['docker', 'exec', '-i', 'chengchuan-relay-postgres-1', 'pg_restore', '--list'], stdin=dump)
  42. assert b'TABLE DATA' in listing
  43. data = subprocess.check_output(['docker', 'inspect', 'chengchuan-relay-app-1'])
  44. (backup / 'app-inspect.private.json').write_bytes(data)
  45. app = json.loads(data)[0]
  46. volume = next(m['Source'] for m in app['Mounts'] if m['Destination'] == '/app/data')
  47. with (backup / 'app-data.tar.gz').open('xb') as output:
  48. subprocess.run(['tar', '-czf', '-', '-C', volume, '.'], stdout=output, check=True)
  49. shutil.copy2(current / 'deploy/.env', source / 'deploy/.env')
  50. settings = (source / 'deploy/.env').read_text().splitlines()
  51. settings = [line for line in settings if not line.startswith('RELAY_VERSION=')]
  52. settings.append('RELAY_VERSION=optional-controls-20260915')
  53. (source / 'deploy/.env').write_text('\n'.join(settings) + '\n')
  54. subprocess.run(['docker','compose','--env-file','.env','-f','compose.relay.yml','config','--quiet'],cwd=source/'deploy',check=True)
  55. result = {'old_image': app['Image'], 'old_tag': app['Config']['Image'], 'release': str(source),
  56. 'new_migrations': new_migrations, 'modified_migrations': modified_migrations,
  57. 'source_files_verified': len(manifest), 'backups': {}}
  58. for name in ['source-before.tar.gz', 'database-before.dump', 'app-data.tar.gz']:
  59. path = backup / name
  60. result['backups'][name] = {'bytes': path.stat().st_size, 'sha256': hashlib.sha256(path.read_bytes()).hexdigest()}
  61. (evidence / 'preparation.json').write_text(json.dumps(result, indent=2))
  62. print(json.dumps(result, indent=2))