aa8c9c6f12
release / release (push) Waiting to run
Reinstalling the package was slow because setup rebuilds pysim's prerequisites (pyscard compiles from source on Linux), while a clone updates with a plain `git pull`. - tools/update.py (+ update.sh / update.bat in the package, and the release workflow shipping them) updates a packaged install in place: it downloads the latest release package, extracts it (zip-slip guard, permission bits restored - zipfile drops them), replaces the app files over the app directory and re-installs the app into the existing .venv. The .venv and simple_lastrun.log are not in the package, so pysim and its prerequisites survive: seconds, not a rebuild. --check / --version / --zip (offline) / --no-pip / --force; the same version is a no-op. - CHANGELOG.md (Keep a Changelog, from the first packaged release; earlier history is the git log). The release workflow now uses the version's CHANGELOG section as the release body (tools/changelog_section.py, falling back to generated notes). - The READMEs document the update path and the `git clone --depth 1` option for the clone path; setup prints the update hint. - Version strings + the PWA cache bumped together (simple-v458). Verified: 1007 Python + 953 frontend; the release-package test covers the new files; the updater was exercised end to end (a v3.31.2 install with a real venv updated to the 3.32.0 package: files replaced, the venv marker kept, pip metadata 3.32.0, exec bits kept, a second run a no-op, --check correct).
118 lines
4.6 KiB
Python
118 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
"""The release package (``tools/build_release.py`` → simple-<ver>.zip).
|
|
|
|
The zip is the READMEs' "download the release package" path. It must carry
|
|
everything needed to run the app and none of the development-only files (the
|
|
tests, the frontend sources/build configuration, git metadata), and its PWA
|
|
files must cover what the service worker precaches - otherwise a newly added
|
|
asset would silently be missing from a release.
|
|
"""
|
|
|
|
import importlib.util
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parents[1]
|
|
SCRIPT = REPO / 'tools' / 'build_release.py'
|
|
SERVER = REPO / 'pysim_simple_server' / 'server.py'
|
|
|
|
|
|
def _load_builder():
|
|
spec = importlib.util.spec_from_file_location('build_release', SCRIPT)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
def _version():
|
|
m = re.search(r"^VERSION = '([^']+)'$", SERVER.read_text(encoding='utf-8'), re.M)
|
|
return m.group(1)
|
|
|
|
|
|
def _sw_precache():
|
|
"""The PWA files sw.js precaches (the shell a release must be able to serve)."""
|
|
text = (REPO / 'frontend' / 'sw.js').read_text(encoding='utf-8')
|
|
m = re.search(r'const URLS = \[(.*?)\];', text, re.S)
|
|
return re.findall(r"'([^']+)'", m.group(1))
|
|
|
|
|
|
class ReleasePackageTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
cls.builder = _load_builder()
|
|
cls.version = _version()
|
|
cls.tmp = tempfile.TemporaryDirectory()
|
|
cls.zip_path = cls.builder.build(cls.tmp.name)
|
|
with zipfile.ZipFile(cls.zip_path) as z:
|
|
cls.names = z.namelist()
|
|
cls.root = 'simple-%s/' % cls.version
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
cls.tmp.cleanup()
|
|
|
|
def test_the_package_is_named_after_the_server_version(self):
|
|
self.assertEqual(self.zip_path.name, 'simple-%s.zip' % self.version)
|
|
|
|
def test_a_single_top_level_directory(self):
|
|
self.assertTrue(all(n.startswith(self.root) for n in self.names), 'stray top-level entries')
|
|
self.assertIn(self.root + 'pyproject.toml', self.names)
|
|
|
|
def test_the_runtime_files_are_present(self):
|
|
for f in ('pysim_simple_server/server.py',
|
|
'pysim_simple_server/__main__.py',
|
|
'pysim_simple_server/data/mcc-mnc-list.json',
|
|
'pysim_simple_server/data/mcc-mnc-list.LICENSE',
|
|
'setup.sh', 'setup.bat', 'start.sh', 'start.bat',
|
|
'update.sh', 'update.bat', 'tools/update.py',
|
|
'tools/changelog_section.py', 'CHANGELOG.md',
|
|
'pyproject.toml', 'requirements-pysim.txt',
|
|
'README.md', 'README_RUS.md', 'LICENSE', 'THIRD-PARTY-NOTICES',
|
|
'docs/api.md'):
|
|
self.assertIn(self.root + f, self.names)
|
|
|
|
def test_every_frontend_runtime_file_is_present(self):
|
|
for f in self.builder.FRONTEND_FILES:
|
|
self.assertIn(self.root + 'frontend/' + f, self.names, f)
|
|
|
|
def test_the_service_worker_precache_is_covered(self):
|
|
for f in _sw_precache():
|
|
self.assertIn(self.root + 'frontend/' + f, self.names,
|
|
'sw.js precaches %s but the package lacks it' % f)
|
|
|
|
def test_no_development_files(self):
|
|
for n in self.names:
|
|
rel = n[len(self.root):]
|
|
self.assertFalse(rel.startswith('tests/'), n)
|
|
self.assertFalse(rel.startswith('frontend/src/'), n)
|
|
self.assertFalse(rel.startswith('frontend/tests/'), n)
|
|
for dev in ('frontend/package.json', 'frontend/package-lock.json',
|
|
'frontend/tailwind.config.js', 'frontend/postcss.config.js'):
|
|
self.assertNotEqual(rel, dev, n)
|
|
self.assertNotIn('node_modules/', rel, n)
|
|
self.assertNotIn('.git/', rel, n)
|
|
self.assertNotIn('__pycache__', rel, n)
|
|
self.assertFalse(rel.endswith('.pyc'), n)
|
|
|
|
def test_no_bytecode_or_temp_files(self):
|
|
basenames = [n.rsplit('/', 1)[-1] for n in self.names]
|
|
self.assertNotIn('.DS_Store', basenames)
|
|
self.assertFalse(any(b.endswith('~') for b in basenames))
|
|
|
|
def test_expect_version_guard(self):
|
|
# CI passes the tag without "v"; a mismatch must fail the build before
|
|
# anything is published.
|
|
r = subprocess.run([sys.executable, str(SCRIPT), '--expect-version', '0.0.0',
|
|
'--out', self.tmp.name], capture_output=True, text=True)
|
|
self.assertNotEqual(r.returncode, 0)
|
|
self.assertIn('version mismatch', r.stderr)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|