Bauen und Releases ohne lokale Abhaengigkeiten ueber ./build.sh

- build.sh (release, build, test) startet einen temporaeren Runner-Container
  (Python, Git, Docker-CLI), der die Build-Container ueber den Docker-Socket startet
- release.py: Quellexport unter releases/ statt /tmp (Host-Pfade fuer Bind-Mounts),
  Test-Befehl in Docker, Git-Identitaet vorab pruefen, bei Commit-Fehler Versionen zuruecknehmen
- Android-Build-Image enthaelt Platform-Tools fest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
duffyduck
2026-10-17 15:41:45 +02:00
co-authored by Claude Opus 5.5
parent ecefd347b2
commit 298c142c14
9 changed files with 117 additions and 19 deletions
+49 -4
View File
@@ -1,6 +1,10 @@
#!/usr/bin/env python3
"""Release erstellen: Version hochzaehlen, CHANGELOG abschliessen, committen, taggen, optional bauen.
Normalerweise ueber ./build.sh aufrufen (laeuft komplett in Docker, nichts lokal zu installieren):
./build.sh release [patch|minor|major] [--build] ./build.sh build ./build.sh test
Direkt (benoetigt python3, git, docker auf dem Rechner):
python3 scripts/release.py # patch: 1.0.0 -> 1.0.1
python3 scripts/release.py minor # 1.0.1 -> 1.1.0
python3 scripts/release.py major # 1.1.0 -> 2.0.0
@@ -131,7 +135,10 @@ def ensure_android_keystore():
def build(version: str, ref: str, targets: list[str]):
out = ROOT / "releases" / f"v{version}"
out.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="netclip-release-") as tmp:
# Unterhalb des Repos statt /tmp: laeuft das Skript selbst im Runner-Container (build.sh),
# muessen die Pfade fuer die Bind-Mounts der Build-Container auch auf dem Host gelten.
(ROOT / "releases").mkdir(exist_ok=True)
with tempfile.TemporaryDirectory(prefix=".src-", dir=ROOT / "releases") as tmp:
src = Path(tmp) / "src"
src.mkdir()
archive = subprocess.run(["git", "archive", "--format=tar", ref], cwd=ROOT, check=True, capture_output=True).stdout
@@ -169,6 +176,30 @@ def build(version: str, ref: str, targets: list[str]):
print(f" {f.name:45} {f.stat().st_size / 1024 / 1024:8.1f} MB")
def test():
"""Krypto-Kompatibilitaet Python <-> Kotlin, komplett in den Build-Containern."""
(ROOT / "releases").mkdir(exist_ok=True)
with tempfile.TemporaryDirectory(prefix=".test-", dir=ROOT / "releases") as tmp:
src = Path(tmp) / "src"
data = Path(tmp) / "interop"
src.mkdir()
data.mkdir()
archive = subprocess.run(["git", "archive", "--format=tar", "HEAD"], cwd=ROOT, check=True, capture_output=True).stdout
subprocess.run(["tar", "-x", "-C", str(src)], input=archive, check=True)
for name in ("linux", "android"):
docker_image(name, src)
user = ["--user", f"{os.getuid()}:{os.getgid()}", "-e", "HOME=/tmp"]
py = ["docker", "run", "--rm", *user, "-v", f"{src}:/src:ro", "-v", f"{data}:/data", "netclip-build-linux",
"python", "/src/tools/interop.py"]
run(*py, "gen", "/data")
run("docker", "run", "--rm", "-v", f"{src}:/src:ro", "-v", f"{data}:/data",
"-v", "netclip-gradle-cache:/root/.gradle", "netclip-build-android", "sh", "-c",
"cp -r /src/client-android /work && cd /work && rm -f local.properties && "
"./gradlew --no-daemon -q testDebugUnitTest -Pinterop=/data; r=$?; "
f"chown -R {os.getuid()}:{os.getgid()} /data; exit $r")
run(*py, "check", "/data")
# ---------------------------------------------------------------------------- Ablauf
def main():
p = argparse.ArgumentParser(description="NetClip-Release erstellen")
@@ -177,12 +208,17 @@ def main():
p.add_argument("--build-only", action="store_true", help="nur die aktuelle Version (HEAD) bauen")
p.add_argument("--targets", default=",".join(TARGETS), help=f"Auswahl aus {','.join(TARGETS)}")
p.add_argument("--allow-empty", action="store_true", help="auch ohne Eintraege unter [Unreleased]")
p.add_argument("--test", action="store_true", help="nur Kompatibilitaetstest Python <-> Kotlin")
args = p.parse_args()
if args.test:
test()
return
targets = [t.strip() for t in args.targets.split(",") if t.strip()]
if unknown := set(targets) - set(TARGETS):
die(f"Unbekannte Ziele: {', '.join(unknown)}")
if (args.build or args.build_only) and not shutil.which("docker"):
if not shutil.which("docker"):
die("docker nicht gefunden")
current = (ROOT / "VERSION").read_text().strip()
@@ -198,16 +234,25 @@ def main():
if not notes and not args.allow_empty:
die("Keine Eintraege unter '## [Unreleased]' im CHANGELOG (oder --allow-empty).")
if subprocess.run(["git", "var", "GIT_COMMITTER_IDENT"], cwd=ROOT, capture_output=True).returncode:
die("Git kennt keinen Namen/E-Mail fuer Commits: git config --global user.name/user.email setzen")
new = bump(current, args.part)
tag = f"v{new}"
if run("git", "tag", "--list", tag, capture=True).strip():
die(f"Tag {tag} existiert bereits")
print(f"Release {current} -> {new}")
files = ["CHANGELOG.md", *VERSION_SPOTS.keys()]
write_versions(new)
close_changelog(new)
run("git", "add", "VERSION", "CHANGELOG.md", "client-android/app/build.gradle", *VERSION_SPOTS.keys())
run("git", "commit", "-q", "-m", f"Release {tag}\n\n{notes}".strip())
try:
run("git", "add", *files)
run("git", "commit", "-q", "-m", f"Release {tag}\n\n{notes}".strip())
except subprocess.CalledProcessError:
run("git", "reset", "-q", "--", *files)
run("git", "checkout", "--", *files)
die("Commit fehlgeschlagen – Versionsaenderungen wurden zurueckgenommen.")
run("git", "tag", "-a", tag, "-m", f"NetClip {new}\n\n{notes}".strip())
if args.build: