diff options
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/release/README.md | 61 | ||||
| -rw-r--r-- | scripts/release/repo_init.py | 57 | ||||
| -rw-r--r-- | scripts/release/repo_release.py | 64 |
3 files changed, 182 insertions, 0 deletions
diff --git a/scripts/release/README.md b/scripts/release/README.md new file mode 100644 index 0000000..54c9086 --- /dev/null +++ b/scripts/release/README.md @@ -0,0 +1,61 @@ +# Release process (manual, local signing) + +All TUF signing keys stay on your machine, in `./keystore` (gitignored — never +commit it). Nothing here touches CI or GitHub Secrets. + +## One-time setup + +``` +pip install -r requirements-dev.txt +python scripts/release/repo_init.py +``` + +Generates `scripts/release/keystore/` (private + public keys for the +root/targets/snapshot/timestamp TUF roles) and `scripts/release/repository/` +(initial signed metadata, including `root.json`). + +**Back up `keystore/` immediately, somewhere private and durable outside this +repo.** Losing the root key means you can never publish a trusted update to +existing installs again — they'd all need a fresh, non-updating reinstall. + +Rebuild once more (`pyinstaller labui.spec`) after this step, so the +freshly-generated `root.json` gets bundled into the app (see the `.spec` +file's `_root_json` check — it only bundles the file if it already exists). + +## Every release + +1. Bump `__version__` in `core/version.py`. +2. `pyinstaller labui.spec` → produces `dist/LabUI/`. +3. `python scripts/release/repo_release.py` → creates+signs + `scripts/release/repository/targets/labui-<version>.tar.gz` and updates + the metadata files in `scripts/release/repository/metadata/`. +4. Create (first time) or reuse the GitHub Release tagged **`updates`** on + this repo, and upload every file from `scripts/release/repository/metadata/` + and `scripts/release/repository/targets/` as release assets, **replacing** + any same-named files already there. + - This tag is fixed on purpose — the app's `metadata_base_url` always + points at `https://github.com/<owner>/<repo>/releases/download/updates/`. + A normal per-version-tagged release would give every version a + different URL, which breaks TUF's requirement that top-level metadata + (`timestamp.json`, `snapshot.json`, ...) live at a stable location. + - Cut a normal, separately-tagged human-facing release too if you want a + changelog/download page — this doesn't have to be the same release. +5. Test: launch a previous installed version, Settings → Check for Updates, + confirm it finds and applies the new version. + +## Re-signing without a new release + +Metadata expires even if nothing changed (`expiration_days` in +`repo_init.py`: root 365 days, targets/snapshot/timestamp 90 days). If you +haven't shipped a release before targets/snapshot/timestamp expire, clients +will start rejecting the metadata as stale. Re-sign with: + +``` +python -m tufup sign snapshot scripts/release/keystore +python -m tufup sign timestamp scripts/release/keystore +``` + +(run from the repo root; adjust the key directory path if you invoke this +from elsewhere) and re-upload the resulting files to the `updates` release. +Set a calendar reminder — there is no automation for this on purpose (manual +signing was a deliberate choice, not an oversight). diff --git a/scripts/release/repo_init.py b/scripts/release/repo_init.py new file mode 100644 index 0000000..ccbfe17 --- /dev/null +++ b/scripts/release/repo_init.py @@ -0,0 +1,57 @@ +""" +scripts/release/repo_init.py + +Run ONCE, ever, by the maintainer, locally. Generates the TUF signing keys +and the initial repository metadata (including root.json, which later gets +bundled into the app via labui.spec). + +Running this again after the repo already exists is safe (tufup skips +re-creating keys/roles that already exist) but there is normally no reason +to run it more than once per project. + +Keys land in ./keystore — never commit that directory (already gitignored). +Back it up somewhere safe and private; losing the root key means you can +never publish another trusted update to existing installs. +""" + +import os +from pathlib import Path + +from tufup.repo import Repository, RolesDict + +# Run from this script's own directory — tufup's config file (.tufup-repo-config) +# and all repo_dir/keys_dir paths are resolved relative to the current working +# directory, so invocation location must be consistent every time. +os.chdir(Path(__file__).resolve().parent) + +APP_NAME = "labui" + +# tufup-example's 1-day timestamp / 7-day snapshot defaults assume an +# automated CI worker re-signs on a schedule. We're signing manually +# (see README.md in this directory), so use longer, human-manageable +# expirations instead. Whichever of these lapses first is the cadence +# you need to re-sign at, even between actual app releases. +EXPIRATION_DAYS = RolesDict(root=365, targets=90, snapshot=90, timestamp=90) + + +def main(): + # Not using app_version_attr here: tufup's setuptools-based attribute + # reader resolves dotted paths relative to the current working directory, + # which we deliberately pin to this script's own directory below (tufup's + # own config-file/relative-path handling expects that) — the two + # assumptions conflict. repo_release.py passes the version explicitly + # instead, which sidesteps this entirely. + repo = Repository( + app_name=APP_NAME, + repo_dir=Path("repository"), + keys_dir=Path("keystore"), + expiration_days=EXPIRATION_DAYS, + ) + repo.initialize() + repo.save_config() + print(f"Initialized tufup repo for '{APP_NAME}' in ./repository, keys in ./keystore") + print("Back up ./keystore now, somewhere private and durable. It is gitignored on purpose.") + + +if __name__ == "__main__": + main() diff --git a/scripts/release/repo_release.py b/scripts/release/repo_release.py new file mode 100644 index 0000000..d50e458 --- /dev/null +++ b/scripts/release/repo_release.py @@ -0,0 +1,64 @@ +""" +scripts/release/repo_release.py + +Run after every `pyinstaller labui.spec` build, from anywhere (paths are +made absolute below before the working directory changes). Packages the +freshly built dist/LabUI bundle as a new signed tufup target. + +Prerequisites: + - repo_init.py has been run once already (./repository and ./keystore exist) + - core/version.py has been bumped to the new version + - dist/LabUI/ exists (pyinstaller labui.spec has just been run) + +After this completes, see README.md in this directory for how to publish +the updated ./repository/metadata and ./repository/targets files to the +"updates" GitHub Release. +""" + +import os +import sys +from pathlib import Path + +_THIS_DIR = Path(__file__).resolve().parent +_REPO_ROOT = _THIS_DIR.parent.parent +_DIST_DIR = _REPO_ROOT / "dist" / "LabUI" + +# Import core.version before changing cwd — needs the repo root on sys.path, +# which won't be true anymore once we chdir into scripts/release below. +sys.path.insert(0, str(_REPO_ROOT)) +from core.version import __version__ as APP_VERSION + +from tufup.repo import Repository + +# tufup's config file / relative repo_dir / keys_dir all resolve against cwd +# (same reasoning as repo_init.py) — pin it here too, for the same reason. +os.chdir(_THIS_DIR) + + +def main(): + if not _DIST_DIR.is_dir(): + raise SystemExit( + f"{_DIST_DIR} not found — run `pyinstaller labui.spec` from the " + f"repo root first." + ) + + repo = Repository.from_config() + + changelog = input(f"One-line changelog for v{APP_VERSION} (optional): ").strip() + custom_metadata = {"changelog": changelog} if changelog else None + + repo.add_bundle( + new_bundle_dir=_DIST_DIR, + new_version=APP_VERSION, + skip_patch=True, # no binary delta patching for now — full archive only + custom_metadata=custom_metadata, + ) + repo.publish_changes(private_key_dirs=[Path("keystore")]) + + print(f"Signed and published v{APP_VERSION} to ./repository") + print("Next: upload ./repository/metadata/* and ./repository/targets/* " + "to the 'updates' GitHub Release — see README.md in this directory.") + + +if __name__ == "__main__": + main() |
