diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5644f85 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ + +# live-build Arbeitsverzeichnis (falls lokal gebaut) +/live/ +*.iso diff --git a/CHANGELOG.md b/CHANGELOG.md index e96514f..9610b1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,3 +17,7 @@ Versionierung nach [SemVer](https://semver.org/lang/de/). - NTFS-Dirty-Bit-Erkennung beim Mounten mit Rückfrage und automatischem `ntfsfix` + erzwungenem Schreib-Mount; zusätzlich eigener Button „NTFS reparieren (Dirty-Bit)". +- `release.py`: Version automatisch hochzählen (Patch/Minor/Major), CHANGELOG + pflegen, committen + taggen, ISO bauen und als Gitea-Release (via `tea`) + mit ISO-Anhang veröffentlichen. +- `out/` bleibt im Repo (per `out/.gitignore`), die gebauten ISOs werden ignoriert. diff --git a/README.md b/README.md index b7dc155..8a719b1 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,23 @@ sudo dd if=out/pc-unlocker.iso of=/dev/sdX bs=4M status=progress conv=fsync > Der Container läuft mit `--privileged`, weil `live-build` Loop-Devices und > Mounts benötigt. +## Release veröffentlichen + +`release.py` zählt die Version hoch, pflegt das CHANGELOG, committet + taggt, +baut die ISO und veröffentlicht sie als **Gitea-Release** mit der ISO als Anhang. + +```bash +./release.py # Patch: 0.1.0 -> 0.1.1 +./release.py --minor # 0.1.0 -> 0.2.0 +./release.py --major # 0.1.0 -> 1.0.0 +./release.py --no-build # vorhandene out/pc-unlocker.iso anhängen +./release.py --no-publish # nur lokal (bump, commit, tag, build) +./release.py --dry-run # nur anzeigen +``` + +Veröffentlichen pusht Release-Commit und Tag nach `origin` und ruft dann +`tea release create`. Einmalig nötig: `tea login add` für die Gitea-Instanz. + ## App direkt testen (ohne ISO) Auf einem Linux-Host mit `python3-tk`, `chntpw`, `ntfs-3g`: diff --git a/app/__pycache__/pc_unlocker.cpython-311.pyc b/app/__pycache__/pc_unlocker.cpython-311.pyc deleted file mode 100644 index 05b2dba..0000000 Binary files a/app/__pycache__/pc_unlocker.cpython-311.pyc and /dev/null differ diff --git a/app/puk/__pycache__/__init__.cpython-311.pyc b/app/puk/__pycache__/__init__.cpython-311.pyc deleted file mode 100644 index 6a7ab46..0000000 Binary files a/app/puk/__pycache__/__init__.cpython-311.pyc and /dev/null differ diff --git a/app/puk/__pycache__/disks.cpython-311.pyc b/app/puk/__pycache__/disks.cpython-311.pyc deleted file mode 100644 index b487a41..0000000 Binary files a/app/puk/__pycache__/disks.cpython-311.pyc and /dev/null differ diff --git a/app/puk/__pycache__/linux.cpython-311.pyc b/app/puk/__pycache__/linux.cpython-311.pyc deleted file mode 100644 index a648e7a..0000000 Binary files a/app/puk/__pycache__/linux.cpython-311.pyc and /dev/null differ diff --git a/app/puk/__pycache__/windows.cpython-311.pyc b/app/puk/__pycache__/windows.cpython-311.pyc deleted file mode 100644 index 20da056..0000000 Binary files a/app/puk/__pycache__/windows.cpython-311.pyc and /dev/null differ diff --git a/out/.gitignore b/out/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/out/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/release.py b/release.py new file mode 100755 index 0000000..e466a79 --- /dev/null +++ b/release.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 +"""Release-Automatik fuer PC-Unlocker. + +Zaehlt die Version hoch, pflegt das CHANGELOG, committet + taggt, baut die ISO +und veroeffentlicht sie als Gitea-Release (via `tea`) mit der ISO als Anhang. + +Beispiele: + ./release.py # Patch-Release (0.1.0 -> 0.1.1), bauen + publizieren + ./release.py --minor # 0.1.0 -> 0.2.0 + ./release.py --major # 0.1.0 -> 1.0.0 + ./release.py --no-build # vorhandene out/pc-unlocker.iso verwenden + ./release.py --no-publish # nur lokal: bump, commit, tag, (build) + ./release.py --dry-run # nur anzeigen, nichts aendern + +Veroeffentlichen pusht den Release-Commit und das Tag nach origin (zwingend, +damit Gitea das Release anlegen kann) und ruft dann `tea release create`. +Einmalig noetig: `tea login add` fuer git.hacker-net.de. +""" + +from __future__ import annotations + +import argparse +import datetime as dt +import re +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +INIT = ROOT / "app" / "puk" / "__init__.py" +CHANGELOG = ROOT / "CHANGELOG.md" +ISO = ROOT / "out" / "pc-unlocker.iso" + +VERSION_RE = re.compile(r'^__version__\s*=\s*"(\d+)\.(\d+)\.(\d+)"', re.M) + + +def run(cmd: list[str], *, check: bool = True, capture: bool = False) -> subprocess.CompletedProcess: + print(f" $ {' '.join(cmd)}") + return subprocess.run(cmd, check=check, text=True, + capture_output=capture, cwd=ROOT) + + +def fail(msg: str) -> None: + print(f"\nFEHLER: {msg}", file=sys.stderr) + sys.exit(1) + + +def read_version() -> tuple[int, int, int]: + m = VERSION_RE.search(INIT.read_text(encoding="utf-8")) + if not m: + fail(f"__version__ nicht in {INIT} gefunden") + return tuple(int(x) for x in m.groups()) # type: ignore[return-value] + + +def bump(ver: tuple[int, int, int], part: str) -> tuple[int, int, int]: + major, minor, patch = ver + if part == "major": + return major + 1, 0, 0 + if part == "minor": + return major, minor + 1, 0 + return major, minor, patch + 1 + + +def write_version(ver: tuple[int, int, int]) -> None: + new = f'__version__ = "{ver[0]}.{ver[1]}.{ver[2]}"' + text = VERSION_RE.sub(new, INIT.read_text(encoding="utf-8"), count=1) + INIT.write_text(text, encoding="utf-8") + + +def update_changelog(ver_str: str) -> str: + """[Unreleased]-Inhalt in einen datierten Versionsabschnitt ueberfuehren. + + Gibt die Notizen (den verschobenen Inhalt) fuer die Release-Beschreibung zurueck. + """ + text = CHANGELOG.read_text(encoding="utf-8") + marker = "## [Unreleased]" + idx = text.find(marker) + if idx == -1: + fail("'## [Unreleased]' nicht im CHANGELOG gefunden") + after = idx + len(marker) + # Inhalt bis zur naechsten Versionsueberschrift (## [..]) oder Dateiende + nxt = text.find("\n## [", after) + end = nxt if nxt != -1 else len(text) + body = text[after:end].strip("\n") + notes = body if body else "Wartungs-Release." + + today = dt.date.today().isoformat() + new_block = ( + f"{marker}\n\n" + f"## [{ver_str}] - {today}\n\n" + f"{body}\n" + ) + new_text = text[:idx] + new_block + text[end:] + CHANGELOG.write_text(new_text, encoding="utf-8") + return notes + + +def git_clean_or_die() -> None: + res = run(["git", "status", "--porcelain"], capture=True) + dirty = [l for l in res.stdout.splitlines() + if l and not l.endswith(("CHANGELOG.md", "__init__.py"))] + if dirty: + fail("Arbeitsverzeichnis nicht sauber. Bitte erst committen/stashen:\n" + + "\n".join(dirty)) + + +def main() -> int: + ap = argparse.ArgumentParser(description="PC-Unlocker Release") + g = ap.add_mutually_exclusive_group() + g.add_argument("--major", action="store_const", const="major", dest="part") + g.add_argument("--minor", action="store_const", const="minor", dest="part") + g.add_argument("--patch", action="store_const", const="patch", dest="part") + ap.add_argument("--no-build", action="store_true", help="ISO nicht neu bauen") + ap.add_argument("--no-publish", action="store_true", + help="nur lokal: kein push, kein Gitea-Release") + ap.add_argument("--dry-run", action="store_true", help="nur anzeigen") + args = ap.parse_args() + part = args.part or "patch" + + old = read_version() + new = bump(old, part) + old_s = ".".join(map(str, old)) + new_s = ".".join(map(str, new)) + tag = f"v{new_s}" + print(f"Release: {old_s} -> {new_s} (tag {tag})") + + if args.dry_run: + print("--dry-run: es wird nichts geaendert.") + return 0 + + if not args.no_publish: + git_clean_or_die() + if shutil.which("tea") is None: + fail("`tea` (Gitea CLI) nicht gefunden. Mit --no-publish nur lokal " + "arbeiten oder tea installieren.") + + # 1) Version + CHANGELOG + print("\n==> Version und CHANGELOG aktualisieren") + write_version(new) + notes = update_changelog(new_s) + + # 2) ISO bauen + if not args.no_build: + print("\n==> ISO bauen") + run(["bash", str(ROOT / "build.sh")]) + if not ISO.exists(): + fail(f"ISO fehlt: {ISO} (ohne --no-build bauen lassen)") + + # 3) Commit + Tag + print("\n==> Commit und Tag") + run(["git", "add", str(INIT), str(CHANGELOG)]) + run(["git", "commit", "-m", f"release: {tag}"]) + run(["git", "tag", "-a", tag, "-m", f"PC-Unlocker {tag}"]) + + if args.no_publish: + print(f"\nLokal fertig. Veroeffentlichen spaeter z.B.:\n" + f" git push origin HEAD && git push origin {tag}\n" + f" tea release create --tag {tag} --title {tag} " + f"--asset {ISO}") + return 0 + + # 4) Push (noetig, damit Gitea das Release kennt) + print("\n==> Push nach origin") + run(["git", "push", "origin", "HEAD"]) + run(["git", "push", "origin", tag]) + + # 5) Gitea-Release mit ISO-Anhang + print("\n==> Gitea-Release anlegen") + run(["tea", "release", "create", + "--tag", tag, + "--title", tag, + "--note", notes, + "--asset", str(ISO)]) + + print(f"\nFertig: Release {tag} veroeffentlicht, ISO angehaengt.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())