Versioning consistency and bump both the XPI and backend to version 0.2.0
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate Firefox update metadata from LinkLog signed XPI artifacts."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SIGNED_DIR = ROOT / 'XPI' / 'signed'
|
||||
MANIFEST_PATH = ROOT / 'webextension' / 'manifest.json'
|
||||
UPDATES_PATH = ROOT / 'webextension' / 'updates.json'
|
||||
ABOUT_TEMPLATE_PATH = ROOT / 'frontend' / 'templates' / 'about.html'
|
||||
RAW_BASE_URL = 'https://git.kolkman.org/olaf/Link-Log/raw/branch/main'
|
||||
XPI_NAME_RE = re.compile(r'LinkLog-(\d+\.\d+\.\d+)\.xpi')
|
||||
ABOUT_XPI_URL_RE = re.compile(rf'{re.escape(RAW_BASE_URL)}/XPI/signed/LinkLog-\d+\.\d+\.\d+\.xpi')
|
||||
|
||||
|
||||
def fail(message: str) -> None:
|
||||
raise SystemExit(f'update metadata generation failed: {message}')
|
||||
|
||||
|
||||
def version_key(version: str) -> tuple[int, int, int]:
|
||||
return tuple(int(part) for part in version.split('.'))
|
||||
|
||||
|
||||
def read_packaged_manifest(xpi_path: Path) -> dict:
|
||||
try:
|
||||
with zipfile.ZipFile(xpi_path) as archive:
|
||||
if archive.testzip() is not None:
|
||||
fail(f'{xpi_path.relative_to(ROOT)} contains a corrupt member')
|
||||
return json.loads(archive.read('manifest.json'))
|
||||
except (OSError, KeyError, json.JSONDecodeError, zipfile.BadZipFile) as error:
|
||||
fail(f'could not read {xpi_path.relative_to(ROOT)}: {error}')
|
||||
|
||||
|
||||
def sha256_digest(xpi_path: Path) -> str:
|
||||
with xpi_path.open('rb') as xpi_file:
|
||||
return hashlib.file_digest(xpi_file, 'sha256').hexdigest()
|
||||
|
||||
|
||||
def update_about_plugin_link(xpi_path: Path) -> None:
|
||||
about_template = ABOUT_TEMPLATE_PATH.read_text()
|
||||
latest_xpi_url = f'{RAW_BASE_URL}/{xpi_path.relative_to(ROOT).as_posix()}'
|
||||
updated_template, replacements = ABOUT_XPI_URL_RE.subn(latest_xpi_url, about_template)
|
||||
if replacements != 1:
|
||||
fail(f'expected one signed XPI link in {ABOUT_TEMPLATE_PATH.relative_to(ROOT)}; found {replacements}')
|
||||
ABOUT_TEMPLATE_PATH.write_text(updated_template)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
source_manifest = json.loads(MANIFEST_PATH.read_text())
|
||||
addon_id = source_manifest.get('browser_specific_settings', {}).get('gecko', {}).get('id')
|
||||
if not addon_id:
|
||||
fail('webextension/manifest.json is missing browser_specific_settings.gecko.id')
|
||||
|
||||
releases = []
|
||||
for xpi_path in SIGNED_DIR.glob('LinkLog-*.xpi'):
|
||||
match = XPI_NAME_RE.fullmatch(xpi_path.name)
|
||||
if not match:
|
||||
continue
|
||||
version = match.group(1)
|
||||
manifest = read_packaged_manifest(xpi_path)
|
||||
if manifest.get('version') != version:
|
||||
fail(f'{xpi_path.relative_to(ROOT)} manifest version does not match its filename')
|
||||
gecko = manifest.get('browser_specific_settings', {}).get('gecko', {})
|
||||
if gecko.get('id') != addon_id:
|
||||
fail(f'{xpi_path.relative_to(ROOT)} add-on id does not match webextension/manifest.json')
|
||||
strict_min_version = gecko.get('strict_min_version')
|
||||
if not strict_min_version:
|
||||
fail(f'{xpi_path.relative_to(ROOT)} is missing browser_specific_settings.gecko.strict_min_version')
|
||||
releases.append((version, xpi_path, strict_min_version, sha256_digest(xpi_path)))
|
||||
|
||||
if not releases:
|
||||
fail(f'no signed LinkLog release artifacts found in {SIGNED_DIR.relative_to(ROOT)}')
|
||||
|
||||
releases.sort(key=lambda release: version_key(release[0]), reverse=True)
|
||||
updates = [
|
||||
{
|
||||
'version': version,
|
||||
'update_link': f'{RAW_BASE_URL}/{xpi_path.relative_to(ROOT).as_posix()}',
|
||||
'update_hash': f'sha256:{digest}',
|
||||
'applications': {
|
||||
'gecko': {
|
||||
'strict_min_version': strict_min_version,
|
||||
},
|
||||
},
|
||||
}
|
||||
for version, xpi_path, strict_min_version, digest in releases
|
||||
]
|
||||
UPDATES_PATH.write_text(json.dumps({'addons': {addon_id: {'updates': updates}}}, indent=2) + '\n')
|
||||
update_about_plugin_link(releases[0][1])
|
||||
print(f'updated {UPDATES_PATH.relative_to(ROOT)} with {len(updates)} signed release(s)')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate the version and checked-in artifacts for a LinkLog release."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
@@ -9,9 +10,12 @@ from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SETTINGS_PATH = ROOT / 'backend' / 'app' / 'core' / 'config.py'
|
||||
VERSION_FILE = ROOT / 'frontend' / 'version.json'
|
||||
SIGNED_DIR = ROOT / 'XPI' / 'signed'
|
||||
MANIFEST_PATH = ROOT / 'webextension' / 'manifest.json'
|
||||
UPDATES_PATH = ROOT / 'webextension' / 'updates.json'
|
||||
ABOUT_TEMPLATE_PATH = ROOT / 'frontend' / 'templates' / 'about.html'
|
||||
RAW_BASE_URL = 'https://git.kolkman.org/olaf/Link-Log/raw/branch/main'
|
||||
VERSION_RE = re.compile(r'\d+\.\d+\.\d+')
|
||||
|
||||
|
||||
@@ -34,12 +38,62 @@ def find_latest_signed_xpi() -> tuple[str, Path]:
|
||||
return max(candidates, key=lambda candidate: version_key(candidate[0]))
|
||||
|
||||
|
||||
def validate_self_update(source_manifest: dict, extension_version: str, xpi_path: Path) -> None:
|
||||
gecko_settings = source_manifest.get('browser_specific_settings', {}).get('gecko', {})
|
||||
addon_id = gecko_settings.get('id')
|
||||
strict_min_version = gecko_settings.get('strict_min_version')
|
||||
if not addon_id:
|
||||
fail('webextension/manifest.json is missing browser_specific_settings.gecko.id')
|
||||
|
||||
updates_data = json.loads(UPDATES_PATH.read_text())
|
||||
addon_entry = updates_data.get('addons', {}).get(addon_id)
|
||||
if not addon_entry:
|
||||
fail(f'{UPDATES_PATH.relative_to(ROOT)} has no entry for add-on id {addon_id!r}')
|
||||
|
||||
entry = next((u for u in addon_entry.get('updates', []) if u.get('version') == extension_version), None)
|
||||
if entry is None:
|
||||
fail(
|
||||
f'{UPDATES_PATH.relative_to(ROOT)} has no update entry for version {extension_version!r}; '
|
||||
'add one alongside the signed XPI so the self-update mechanism can find it'
|
||||
)
|
||||
|
||||
expected_link = f'{RAW_BASE_URL}/{xpi_path.relative_to(ROOT).as_posix()}'
|
||||
if entry.get('update_link') != expected_link:
|
||||
fail(
|
||||
f'{UPDATES_PATH.relative_to(ROOT)} update_link {entry.get("update_link")!r} does not match '
|
||||
f'the expected raw signed XPI URL {expected_link!r}'
|
||||
)
|
||||
|
||||
with xpi_path.open('rb') as xpi_file:
|
||||
expected_hash = f'sha256:{hashlib.file_digest(xpi_file, "sha256").hexdigest()}'
|
||||
if entry.get('update_hash') != expected_hash:
|
||||
fail(
|
||||
f'{UPDATES_PATH.relative_to(ROOT)} update_hash {entry.get("update_hash")!r} does not match '
|
||||
f'the SHA-256 hash of {xpi_path.relative_to(ROOT)}'
|
||||
)
|
||||
|
||||
entry_min_version = entry.get('applications', {}).get('gecko', {}).get('strict_min_version')
|
||||
if entry_min_version != strict_min_version:
|
||||
fail(
|
||||
f'{UPDATES_PATH.relative_to(ROOT)} applications.gecko.strict_min_version {entry_min_version!r} '
|
||||
f'does not match webextension/manifest.json strict_min_version {strict_min_version!r}'
|
||||
)
|
||||
|
||||
|
||||
def validate_about_plugin_link(xpi_path: Path) -> None:
|
||||
expected_link = f'{RAW_BASE_URL}/{xpi_path.relative_to(ROOT).as_posix()}'
|
||||
if expected_link not in ABOUT_TEMPLATE_PATH.read_text():
|
||||
fail(
|
||||
f'{ABOUT_TEMPLATE_PATH.relative_to(ROOT)} does not link to the latest signed XPI '
|
||||
f'{xpi_path.relative_to(ROOT)}'
|
||||
)
|
||||
|
||||
|
||||
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)
|
||||
version_data = json.loads(VERSION_FILE.read_text())
|
||||
backend_version = version_data.get('version')
|
||||
if not backend_version:
|
||||
fail(f'version could not be found in {VERSION_FILE.relative_to(ROOT)}')
|
||||
if not VERSION_RE.fullmatch(backend_version):
|
||||
fail(f'backend version {backend_version} is not a valid three-part version')
|
||||
|
||||
@@ -64,6 +118,9 @@ def main() -> None:
|
||||
if archive.testzip() is not None:
|
||||
fail('signed XPI contains a corrupt member')
|
||||
|
||||
validate_self_update(source_manifest, extension_version, xpi_path)
|
||||
validate_about_plugin_link(xpi_path)
|
||||
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user