#!/usr/bin/env python3 """Validate the version and checked-in artifacts for a LinkLog release.""" import json import re import sys import zipfile from pathlib import Path ROOT = Path(__file__).resolve().parents[2] SETTINGS_PATH = ROOT / 'backend' / 'app' / 'core' / 'config.py' SIGNED_DIR = ROOT / 'XPI' / 'signed' MANIFEST_PATH = ROOT / 'webextension' / 'manifest.json' VERSION_RE = re.compile(r'\d+\.\d+\.\d+') def fail(message: str) -> None: raise SystemExit(f'release validation failed: {message}') def version_key(version: str) -> tuple[int, int, int]: return tuple(int(part) for part in version.split('.')) def find_latest_signed_xpi() -> tuple[str, Path]: candidates = [] for xpi_path in SIGNED_DIR.glob('LinkLog-*.xpi'): match = re.fullmatch(r'LinkLog-(\d+\.\d+\.\d+)\.xpi', xpi_path.name) if match: candidates.append((match.group(1), xpi_path)) if not candidates: fail(f'no signed plugin artifacts found in {SIGNED_DIR.relative_to(ROOT)}') return max(candidates, key=lambda candidate: version_key(candidate[0])) def main() -> None: settings = SETTINGS_PATH.read_text() match = re.search(r"version: str = os\.getenv\('LINKLOG_VERSION', '([^']+)'\)", settings) if not match: fail('backend version default could not be found') backend_version = match.group(1) if not VERSION_RE.fullmatch(backend_version): fail(f'backend version {backend_version} is not a valid three-part version') extension_version, xpi_path = find_latest_signed_xpi() source_manifest = json.loads(MANIFEST_PATH.read_text()) if source_manifest.get('version') != extension_version: fail( f'webextension/manifest.json version {source_manifest.get("version")!r} does not match ' f'the latest signed XPI version {extension_version!r}' ) with zipfile.ZipFile(xpi_path) as archive: try: packaged_manifest = json.loads(archive.read('manifest.json')) except KeyError: fail('signed XPI does not contain manifest.json') if packaged_manifest.get('version') != extension_version: fail('signed XPI manifest version does not match its filename') gecko_settings = packaged_manifest.get('browser_specific_settings', {}).get('gecko', {}) data_permissions = gecko_settings.get('data_collection_permissions') if data_permissions != {'required': ['websiteActivity'], 'optional': []}: fail('Firefox data_collection_permissions must require websiteActivity and have no optional categories') if archive.testzip() is not None: fail('signed XPI contains a corrupt member') signed_xpi = xpi_path.relative_to(ROOT) if len(sys.argv) == 3 and sys.argv[1] == '--github-output': with Path(sys.argv[2]).open('a') as output: print(f'backend_version={backend_version}', file=output) print(f'plugin_version={extension_version}', file=output) print(f'signed_xpi={signed_xpi}', file=output) elif len(sys.argv) != 1: fail('usage: validate_release.py [--github-output ]') print(f'validated LinkLog backend release {backend_version}') print(f'plugin_version={extension_version}') print(f'signed_xpi={signed_xpi}') if __name__ == '__main__': main()