mirror of
https://github.com/migatu/vtt_work.git
synced 2026-07-14 13:34:42 +00:00
sync: root v0.4
This commit is contained in:
@@ -0,0 +1,356 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
manage_releases.py
|
||||
|
||||
Automates synchronization of Git tags with GitHub releases. On each run it:
|
||||
* Verifies the repository is clean and the current branch is fully pushed.
|
||||
* Ensures HEAD is tagged by comparing the root version.nfo with existing
|
||||
release tags and bumping/committing when required.
|
||||
* Iterates over every tag in the repository, creating GitHub releases for any
|
||||
missing entries and attaching per-directory ZIP archives (excluding
|
||||
version.nfo files) that reflect the repository state for that tag.
|
||||
|
||||
Dependencies:
|
||||
- gh CLI (https://cli.github.com/)
|
||||
- git available in PATH
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import tempfile
|
||||
import textwrap
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List, Optional, Sequence, Tuple
|
||||
from zipfile import ZIP_DEFLATED, ZipFile
|
||||
|
||||
|
||||
def sh(args: Sequence[str], cwd: Path, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
"""Run a subprocess and return the completed process."""
|
||||
result = subprocess.run(
|
||||
list(args),
|
||||
cwd=str(cwd),
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
if check and result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"Command failed ({' '.join(args)}):\n"
|
||||
f"{result.stdout}{result.stderr}"
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Synchronize Git tags with GitHub releases.")
|
||||
parser.add_argument(
|
||||
"--repo",
|
||||
default=".",
|
||||
help="Ścieżka do repozytorium git (domyślnie: bieżący katalog).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--remote",
|
||||
default="origin",
|
||||
help="Nazwa zdalnego repozytorium do pushowania commitów oraz tagów (domyślnie: origin).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
default="Release packs",
|
||||
help="Katalog wyjściowy, w którym powstaną ZIP-y uploadowane do release (domyślnie: 'Release packs').",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--notes-template",
|
||||
default="Automated release for {tag}",
|
||||
help="Szablon notatek release (dostępne pola: {tag}).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Tylko plan – bez commitów, tagowania i publikacji release.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def read_text_if_exists(path: Path) -> Optional[str]:
|
||||
if not path.is_file():
|
||||
return None
|
||||
return path.read_text(encoding="utf-8").strip()
|
||||
|
||||
|
||||
def parse_version(version: str) -> Optional[Tuple[int, ...]]:
|
||||
if not version:
|
||||
return None
|
||||
if not re.fullmatch(r"\d+(?:\.\d+)*", version):
|
||||
return None
|
||||
return tuple(int(part) for part in version.split("."))
|
||||
|
||||
|
||||
def bump_version(version: str) -> str:
|
||||
parts = parse_version(version)
|
||||
if parts is None:
|
||||
# fallback: append .1 to non-numeric strings
|
||||
return f"{version}.1"
|
||||
bumped = list(parts)
|
||||
bumped[-1] += 1
|
||||
return ".".join(str(piece) for piece in bumped)
|
||||
|
||||
|
||||
def sanitize_component(value: str) -> str:
|
||||
value = value.strip()
|
||||
if not value:
|
||||
return "unknown"
|
||||
return re.sub(r"[\\s/\\\\]+", "-", value)
|
||||
|
||||
|
||||
class ReleaseManager:
|
||||
def __init__(self, args: argparse.Namespace):
|
||||
self.args = args
|
||||
self.repo = Path(args.repo).resolve()
|
||||
self.output_dir = (self.repo / args.output_dir).resolve()
|
||||
|
||||
# ------------------------------------------------------------------ helpers
|
||||
def git(self, *args: str, check: bool = True) -> subprocess.CompletedProcess[str]:
|
||||
return sh(args, cwd=self.repo, check=check)
|
||||
|
||||
def ensure_tooling(self) -> None:
|
||||
sh(["gh", "--version"], cwd=self.repo, check=True)
|
||||
|
||||
def ensure_repo(self) -> None:
|
||||
if not (self.repo / ".git").exists():
|
||||
raise RuntimeError(f"{self.repo} nie jest repozytorium git.")
|
||||
|
||||
def ensure_clean_state(self) -> None:
|
||||
status = self.git("git", "status", "--porcelain", check=True).stdout.strip()
|
||||
if status:
|
||||
raise RuntimeError(
|
||||
"Wykryto niezakomitowane zmiany. Ukończ commit lub stash przed publikacją release.\n"
|
||||
"Release podpisany z nieczystego drzewa może wymagać resetu HEAD do taga."
|
||||
)
|
||||
|
||||
upstream = self.git("git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}", check=False)
|
||||
if upstream.returncode != 0:
|
||||
raise RuntimeError(
|
||||
"Brak skonfigurowanego upstreamu dla bieżącej gałęzi – nie można zweryfikować, "
|
||||
"czy wszystkie commity zostały wypchnięte."
|
||||
)
|
||||
ahead = self.git("git", "rev-list", "--count", "@{u}..HEAD", check=True).stdout.strip()
|
||||
if ahead and int(ahead) > 0:
|
||||
raise RuntimeError(
|
||||
"Bieżąca gałąź zawiera niewypchnięte commity. Wypchnij zmiany zanim utworzysz release."
|
||||
)
|
||||
|
||||
def get_head_tag(self) -> Optional[str]:
|
||||
result = self.git("git", "describe", "--tags", "--exact-match", "HEAD", check=False)
|
||||
tag = result.stdout.strip()
|
||||
if result.returncode == 0 and tag:
|
||||
return tag
|
||||
return None
|
||||
|
||||
def list_tags(self) -> List[str]:
|
||||
tags = self.git("git", "tag", "--sort=creatordate", check=True).stdout.splitlines()
|
||||
return [tag.strip() for tag in tags if tag.strip()]
|
||||
|
||||
def list_version_tags(self) -> List[str]:
|
||||
tags = []
|
||||
for tag in self.list_tags():
|
||||
if parse_version(tag):
|
||||
tags.append(tag)
|
||||
return tags
|
||||
|
||||
def read_root_version(self) -> Optional[str]:
|
||||
return read_text_if_exists(self.repo / "version.nfo")
|
||||
|
||||
def write_root_version(self, version: str) -> None:
|
||||
target = self.repo / "version.nfo"
|
||||
target.write_text(f"{version}\n", encoding="utf-8")
|
||||
|
||||
def commit_root_version(self, version: str) -> None:
|
||||
if self.args.dry_run:
|
||||
print(f"[DRY-RUN] git add version.nfo && git commit -m 'release: bump root to v{version}'")
|
||||
return
|
||||
self.write_root_version(version)
|
||||
self.git("git", "add", "version.nfo", check=True)
|
||||
self.git("git", "commit", "-m", f"release: bump root to v{version}", check=True)
|
||||
self.git("git", "push", self.args.remote, check=True)
|
||||
|
||||
def tag_exists(self, tag: str) -> bool:
|
||||
result = self.git("git", "rev-parse", "-q", "--verify", f"refs/tags/{tag}", check=False)
|
||||
return result.returncode == 0
|
||||
|
||||
def create_tag(self, tag: str, message: str) -> None:
|
||||
if self.tag_exists(tag):
|
||||
print(f"[INFO] Tag {tag} już istnieje – pomijam tworzenie.")
|
||||
return
|
||||
if self.args.dry_run:
|
||||
print(f"[DRY-RUN] git tag -a {tag} -m \"{message}\"")
|
||||
return
|
||||
self.git("git", "tag", "-a", tag, "-m", message, check=True)
|
||||
self.git("git", "push", self.args.remote, tag, check=True)
|
||||
|
||||
def ensure_head_tagged(self) -> str:
|
||||
head_tag = self.get_head_tag()
|
||||
if head_tag:
|
||||
return head_tag
|
||||
|
||||
root_version = self.read_root_version()
|
||||
root_tuple = parse_version(root_version) if root_version else None
|
||||
|
||||
version_tags = self.list_version_tags()
|
||||
latest_version = version_tags[-1] if version_tags else None
|
||||
latest_tuple = parse_version(latest_version) if latest_version else None
|
||||
|
||||
if root_version and (root_tuple and (latest_tuple is None or root_tuple > latest_tuple)):
|
||||
new_version = root_version
|
||||
else:
|
||||
base_version = latest_version or root_version or "0.0"
|
||||
new_version = bump_version(base_version)
|
||||
if not self.args.dry_run:
|
||||
print(f"[INFO] Bump root version.nfo -> {new_version}")
|
||||
self.commit_root_version(new_version)
|
||||
|
||||
tag_message = f"release {new_version}"
|
||||
self.create_tag(new_version, tag_message)
|
||||
return new_version
|
||||
|
||||
def gather_directories_for_tag(self, tag: str) -> List[str]:
|
||||
result = self.git("git", "ls-tree", "-d", "--name-only", tag, check=True)
|
||||
candidates = []
|
||||
for line in result.stdout.splitlines():
|
||||
name = line.strip()
|
||||
if not name:
|
||||
continue
|
||||
candidates.append(name)
|
||||
return candidates
|
||||
|
||||
def version_for_directory(self, tag: str, directory: str) -> str:
|
||||
show = self.git("git", "show", f"{tag}:{directory}/version.nfo", check=False)
|
||||
if show.returncode == 0:
|
||||
version = show.stdout.strip()
|
||||
if version:
|
||||
return version
|
||||
return tag
|
||||
|
||||
def build_assets_for_tag(self, tag: str) -> List[Path]:
|
||||
assets: List[Path] = []
|
||||
directories = self.gather_directories_for_tag(tag)
|
||||
if not directories:
|
||||
print(f"[WARN] Tag {tag} nie zawiera katalogów do zzipowania.")
|
||||
return assets
|
||||
|
||||
tag_output_dir = self.output_dir / sanitize_component(tag)
|
||||
if not self.args.dry_run:
|
||||
if tag_output_dir.exists():
|
||||
shutil.rmtree(tag_output_dir)
|
||||
tag_output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for directory in directories:
|
||||
version = self.version_for_directory(tag, directory)
|
||||
safe_version = sanitize_component(version)
|
||||
safe_dir = sanitize_component(directory)
|
||||
zip_name = f"{safe_dir}-v{safe_version}.zip"
|
||||
zip_path = tag_output_dir / zip_name
|
||||
|
||||
if self.args.dry_run:
|
||||
print(f"[DRY-RUN] Przygotuj ZIP {zip_path} z taga {tag}:{directory} (pomijając version.nfo)")
|
||||
assets.append(zip_path)
|
||||
continue
|
||||
|
||||
# Re-run capturing binary properly.
|
||||
archive_proc = subprocess.run(
|
||||
["git", "archive", "--format=tar", tag, directory],
|
||||
cwd=str(self.repo),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
if archive_proc.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"git archive nie powiódł się dla {tag}:{directory}:\n"
|
||||
f"{archive_proc.stderr.decode('utf-8', errors='ignore')}"
|
||||
)
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
tmp_path = Path(tmp_dir)
|
||||
with tarfile.open(fileobj=io.BytesIO(archive_proc.stdout)) as tar:
|
||||
tar.extractall(tmp_path)
|
||||
|
||||
extracted_root = tmp_path / directory
|
||||
for extra in extracted_root.rglob("version.nfo"):
|
||||
if extra.is_file():
|
||||
extra.unlink()
|
||||
|
||||
with ZipFile(zip_path, "w", ZIP_DEFLATED) as zipf:
|
||||
for root, _, files in os.walk(extracted_root):
|
||||
for filename in files:
|
||||
file_path = Path(root) / filename
|
||||
rel_path = file_path.relative_to(tmp_path)
|
||||
zipf.write(file_path, rel_path.as_posix())
|
||||
|
||||
assets.append(zip_path)
|
||||
|
||||
return assets
|
||||
|
||||
def release_exists(self, tag: str) -> bool:
|
||||
result = self.git("gh", "release", "view", tag, check=False)
|
||||
return result.returncode == 0
|
||||
|
||||
def create_release(self, tag: str, assets: List[Path]) -> None:
|
||||
notes = self.args.notes_template.format(tag=tag)
|
||||
wrapped = "\n".join(textwrap.wrap(notes, width=100))
|
||||
|
||||
if self.args.dry_run:
|
||||
print(f"[DRY-RUN] gh release create {tag} {' '.join(str(a) for a in assets)}")
|
||||
print(f"[DRY-RUN] Notatki:\n{wrapped}")
|
||||
return
|
||||
|
||||
cmd = ["gh", "release", "create", tag, "--title", tag, "--notes", wrapped]
|
||||
cmd.extend(str(asset) for asset in assets)
|
||||
result = self.git(*cmd, check=False)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"Nie udało się utworzyć release {tag}:\n{result.stdout}{result.stderr}"
|
||||
)
|
||||
|
||||
def sync_releases(self) -> None:
|
||||
tags = self.list_tags()
|
||||
if not tags:
|
||||
print("[INFO] Brak tagów do synchronizacji.")
|
||||
return
|
||||
|
||||
self.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for tag in tags:
|
||||
if self.release_exists(tag):
|
||||
print(f"[INFO] Release {tag} już istnieje – pomijam.")
|
||||
continue
|
||||
print(f"[INFO] Przygotowuję release dla taga {tag}...")
|
||||
assets = self.build_assets_for_tag(tag)
|
||||
self.create_release(tag, assets)
|
||||
|
||||
def run(self) -> None:
|
||||
self.ensure_repo()
|
||||
self.ensure_tooling()
|
||||
self.ensure_clean_state()
|
||||
self.ensure_head_tagged()
|
||||
self.sync_releases()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
manager = ReleaseManager(args)
|
||||
try:
|
||||
manager.run()
|
||||
except RuntimeError as exc:
|
||||
print(f"[BŁĄD] {exc}")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -58,10 +58,6 @@ ZIP_MODE: str = "ALL" # "ALL" | "MODULE_ONLY" | "OFF"
|
||||
# Katalog wyjściowy na archiwa .zip; jeśli None, użyje SOURCE_BASE_DIR/_zips
|
||||
ZIP_OUTPUT_DIR: Optional[str] = r"C:\Release packs"
|
||||
CHECK_MODE: bool = False # tylko walidacja – brak zmian, wyjście !=0 jeśli są akcje
|
||||
ENABLE_GH_RELEASE: bool = False # publikowanie release przez gh CLI
|
||||
RELEASE_TAG_PREFIX: Optional[str] = None # prefiks taga release (np. "module/")
|
||||
RELEASE_NOTES_TEMPLATE: str = "Automated release for {name} v{version}"
|
||||
GIT_REMOTE_NAME: str = "origin"
|
||||
|
||||
# ==========================
|
||||
# KONIEC KONFIGURACJI
|
||||
@@ -128,87 +124,6 @@ def git_changed_top_level_dirs(repo_dir: Path) -> List[str]:
|
||||
return sorted(changed)
|
||||
|
||||
|
||||
def git_current_head(repo_dir: Path) -> str:
|
||||
rc, out = sh(["git", "rev-parse", "HEAD"], repo_dir)
|
||||
if rc != 0:
|
||||
raise RuntimeError(f"Nie mogę ustalić HEAD w {norm(repo_dir)}:\n{out}")
|
||||
return out.strip()
|
||||
|
||||
|
||||
def git_tag_exists(repo_dir: Path, tag: str) -> bool:
|
||||
rc, _ = sh(["git", "rev-parse", "-q", "--verify", f"refs/tags/{tag}"], repo_dir)
|
||||
return rc == 0
|
||||
|
||||
|
||||
def create_git_tag(repo_dir: Path, tag: str, message: str, commit: Optional[str]) -> None:
|
||||
if git_tag_exists(repo_dir, tag):
|
||||
print(f"[INFO] Tag {tag} już istnieje – pomijam tworzenie.")
|
||||
return
|
||||
|
||||
cmd = ["git", "tag", "-a", tag]
|
||||
if commit:
|
||||
cmd.append(commit)
|
||||
cmd += ["-m", message]
|
||||
rc, out = sh(cmd, repo_dir)
|
||||
if rc != 0:
|
||||
raise RuntimeError(f"Nie udało się utworzyć taga {tag}:\n{out}")
|
||||
if out.strip():
|
||||
print(out)
|
||||
|
||||
|
||||
def push_git_tag(repo_dir: Path, tag: str) -> None:
|
||||
remote = getattr(ARGS, "git_remote", GIT_REMOTE_NAME)
|
||||
rc, out = sh(["git", "push", remote, tag], repo_dir)
|
||||
if rc != 0:
|
||||
raise RuntimeError(f"Nie udało się wypchnąć taga {tag}:\n{out}")
|
||||
if out.strip():
|
||||
print(out)
|
||||
|
||||
|
||||
def ensure_gh_available(repo_dir: Path) -> None:
|
||||
rc, out = sh(["gh", "--version"], repo_dir)
|
||||
if rc != 0:
|
||||
raise RuntimeError("Polecenie 'gh' nie jest dostępne w PATH:\n" + out)
|
||||
|
||||
|
||||
def github_release_exists(repo_dir: Path, tag: str) -> bool:
|
||||
rc, _ = sh(["gh", "release", "view", tag], repo_dir)
|
||||
return rc == 0
|
||||
|
||||
|
||||
def publish_github_release(repo_dir: Path, tag: str, title: str, notes: str, assets: List[Path]) -> None:
|
||||
ensure_gh_available(repo_dir)
|
||||
assets_str = [str(p) for p in assets]
|
||||
|
||||
if github_release_exists(repo_dir, tag):
|
||||
if not assets_str:
|
||||
print(f"[INFO] Release {tag} już istnieje – brak nowych assetów do przesłania.")
|
||||
return
|
||||
cmd = ["gh", "release", "upload", tag, *assets_str, "--clobber"]
|
||||
rc, out = sh(cmd, repo_dir)
|
||||
if rc != 0:
|
||||
raise RuntimeError(f"Nie udało się zaktualizować release {tag}:\n{out}")
|
||||
if out.strip():
|
||||
print(out)
|
||||
return
|
||||
|
||||
cmd = ["gh", "release", "create", tag, *assets_str, "--title", title, "--notes", notes]
|
||||
rc, out = sh(cmd, repo_dir)
|
||||
if rc != 0:
|
||||
raise RuntimeError(f"Nie udało się utworzyć release {tag}:\n{out}")
|
||||
if out.strip():
|
||||
print(out)
|
||||
|
||||
|
||||
def build_release_tag(name: str, version: str) -> str:
|
||||
safe_name = name.replace(" ", "-").replace("/", "-")
|
||||
base = f"{safe_name}-v{version}"
|
||||
prefix = getattr(ARGS, "release_tag_prefix", RELEASE_TAG_PREFIX)
|
||||
if prefix:
|
||||
return f"{prefix}{base}"
|
||||
return base
|
||||
|
||||
|
||||
def find_modules_dir_for_user(user: str,
|
||||
relative_pattern: str,
|
||||
appdata_kind: Optional[str]) -> Path:
|
||||
@@ -392,12 +307,12 @@ def write_json_atomic(path: Path, data: dict) -> None:
|
||||
tmp.replace(path)
|
||||
|
||||
|
||||
def run_git_commands(repo_dir: Path, commit_msg: str, dry_run: bool) -> Optional[str]:
|
||||
def run_git_commands(repo_dir: Path, commit_msg: str, dry_run: bool) -> None:
|
||||
if dry_run:
|
||||
print("[DRY-RUN] git add -A")
|
||||
print(f"[DRY-RUN] git commit -m {commit_msg!r}")
|
||||
print("[DRY-RUN] git push")
|
||||
return None
|
||||
return
|
||||
|
||||
def run(cmd: List[str]) -> None:
|
||||
rc, out = sh(cmd, repo_dir)
|
||||
@@ -413,12 +328,10 @@ def run_git_commands(repo_dir: Path, commit_msg: str, dry_run: bool) -> Optional
|
||||
if out.strip():
|
||||
print(out)
|
||||
run(["git", "push"])
|
||||
head = git_current_head(repo_dir)
|
||||
return head
|
||||
|
||||
|
||||
|
||||
def preflight_module(src_dir: Path, dry_run: bool) -> Tuple[str, str, Optional[str]]:
|
||||
def preflight_module(src_dir: Path, dry_run: bool) -> Tuple[str, str]:
|
||||
"""
|
||||
Moduł: odczyt module.json, bump, zapis (jeśli nie dry_run), commit i push.
|
||||
Zwraca (module_id, new_version, commit_sha lub None przy dry-run).
|
||||
@@ -435,20 +348,18 @@ def preflight_module(src_dir: Path, dry_run: bool) -> Tuple[str, str, Optional[s
|
||||
|
||||
print(f"[PRE][MODULE] {src_dir.name}: id={module_id}, version={old_version} -> {new_version}")
|
||||
|
||||
commit_sha: Optional[str] = None
|
||||
|
||||
if dry_run:
|
||||
print("[DRY-RUN] Nie zapisuję module.json i nie wykonuję git.")
|
||||
else:
|
||||
manifest["version"] = new_version
|
||||
write_json_atomic(manifest_path, manifest)
|
||||
commit_msg = f"sync: {module_id} v{new_version}"
|
||||
commit_sha = run_git_commands(src_dir, commit_msg, dry_run=False)
|
||||
run_git_commands(src_dir, commit_msg, dry_run=False)
|
||||
|
||||
return module_id, new_version, commit_sha
|
||||
return module_id, new_version
|
||||
|
||||
|
||||
def preflight_non_module(src_dir: Path, dry_run: bool) -> Tuple[str, Optional[str]]:
|
||||
def preflight_non_module(src_dir: Path, dry_run: bool) -> str:
|
||||
"""
|
||||
Niemoduł: version.nfo w formacie '0.x' (x int).
|
||||
- jeśli brak -> '0.1'
|
||||
@@ -457,7 +368,6 @@ def preflight_non_module(src_dir: Path, dry_run: bool) -> Tuple[str, Optional[st
|
||||
Zwraca (new_version, commit_sha lub None przy dry-run).
|
||||
"""
|
||||
nfo = src_dir / "version.nfo"
|
||||
commit_sha: Optional[str] = None
|
||||
if not nfo.exists():
|
||||
new_version = "0.1"
|
||||
print(f"[PRE][FOLDER] {src_dir.name}: version.nfo brak -> {new_version}")
|
||||
@@ -465,8 +375,8 @@ def preflight_non_module(src_dir: Path, dry_run: bool) -> Tuple[str, Optional[st
|
||||
with nfo.open("w", encoding="utf-8") as f:
|
||||
f.write(new_version + "\n")
|
||||
commit_msg = f"sync: {src_dir.name} v{new_version}"
|
||||
commit_sha = run_git_commands(src_dir, commit_msg, dry_run=False)
|
||||
return new_version, commit_sha
|
||||
run_git_commands(src_dir, commit_msg, dry_run=False)
|
||||
return new_version
|
||||
|
||||
# istnieje -> wczytaj i bumpnij
|
||||
content = nfo.read_text(encoding="utf-8").strip()
|
||||
@@ -486,15 +396,13 @@ def preflight_non_module(src_dir: Path, dry_run: bool) -> Tuple[str, Optional[st
|
||||
with nfo.open("w", encoding="utf-8") as f:
|
||||
f.write(new_version + "\n")
|
||||
commit_msg = f"sync: {src_dir.name} v{new_version}"
|
||||
commit_sha = run_git_commands(src_dir, commit_msg, dry_run=False)
|
||||
run_git_commands(src_dir, commit_msg, dry_run=False)
|
||||
|
||||
return new_version, commit_sha
|
||||
return new_version
|
||||
|
||||
|
||||
# ====== ROOT version.nfo (globalna wersja repo) ======
|
||||
def maybe_bump_root_version(
|
||||
repo_dir: Path, changed_any: bool, dry_run: bool
|
||||
) -> Optional[Tuple[str, Optional[str]]]:
|
||||
def maybe_bump_root_version(repo_dir: Path, changed_any: bool, dry_run: bool) -> Optional[str]:
|
||||
"""
|
||||
Jeśli:
|
||||
- istnieją zmiany do przetworzenia (changed_any == True)
|
||||
@@ -530,26 +438,11 @@ def maybe_bump_root_version(
|
||||
|
||||
if dry_run:
|
||||
print("[DRY-RUN][ROOT] Zapis version.nfo i commit/push zostaną pominięte.")
|
||||
return new_version, None
|
||||
return new_version
|
||||
|
||||
nfo.write_text(new_version + "\n", encoding="utf-8")
|
||||
commit_sha = run_git_commands(repo_dir, f"sync: root v{new_version}", dry_run=False)
|
||||
return new_version, commit_sha
|
||||
|
||||
|
||||
def maybe_publish_root_release(repo_dir: Path, version: str, commit_sha: Optional[str], dry_run: bool) -> None:
|
||||
if not ARGS.gh_release or dry_run:
|
||||
return
|
||||
name = repo_dir.name or "root"
|
||||
tag = build_release_tag(name, version)
|
||||
title = f"{name} v{version}"
|
||||
template = getattr(ARGS, "release_notes_template", RELEASE_NOTES_TEMPLATE)
|
||||
notes = template.format(name=name, version=version, tag=tag)
|
||||
if commit_sha is None:
|
||||
commit_sha = git_current_head(repo_dir)
|
||||
create_git_tag(repo_dir, tag, title, commit_sha)
|
||||
push_git_tag(repo_dir, tag)
|
||||
publish_github_release(repo_dir, tag, title, notes, [])
|
||||
run_git_commands(repo_dir, f"sync: root v{new_version}", dry_run=False)
|
||||
return new_version
|
||||
|
||||
|
||||
# ====== ZIP utilities ======
|
||||
@@ -636,22 +529,6 @@ def parse_args() -> argparse.Namespace:
|
||||
help="Tylko sprawdzenie zmian (wymusza dry-run, kod wyjścia 1 przy wymaganych akcjach).")
|
||||
p.set_defaults(check=CHECK_MODE)
|
||||
|
||||
# GitHub Release options
|
||||
release_group = p.add_mutually_exclusive_group()
|
||||
release_group.add_argument("--gh-release", dest="gh_release", action="store_true",
|
||||
help="Po zakończeniu commitów utwórz release na GitHubie (wymaga gh CLI).")
|
||||
release_group.add_argument("--no-gh-release", dest="gh_release", action="store_false",
|
||||
help="Wyłącz publikowanie release (domyślne).")
|
||||
p.set_defaults(gh_release=ENABLE_GH_RELEASE)
|
||||
p.add_argument("--release-tag-prefix", dest="release_tag_prefix",
|
||||
default=RELEASE_TAG_PREFIX,
|
||||
help="Opcjonalny prefiks dodawany przed tagiem release (np. 'module/').")
|
||||
p.add_argument("--release-notes-template", dest="release_notes_template",
|
||||
default=RELEASE_NOTES_TEMPLATE,
|
||||
help="Szablon notatek release (format: name, version, tag).")
|
||||
p.add_argument("--git-remote", dest="git_remote",
|
||||
default=GIT_REMOTE_NAME,
|
||||
help="Nazwa zdalnego repozytorium do wypychania tagów (domyślnie 'origin').")
|
||||
|
||||
return p.parse_args()
|
||||
|
||||
@@ -667,7 +544,7 @@ def resolve_modules_base(user: str, relative_pattern: str, appdata_kind: Optiona
|
||||
|
||||
|
||||
def process_module(src_dir: Path, modules_base: Path, confirm_delete: bool, dry_run: bool) -> None:
|
||||
module_id, new_ver, commit_sha = preflight_module(src_dir, dry_run=dry_run)
|
||||
module_id, new_ver = preflight_module(src_dir, dry_run=dry_run)
|
||||
|
||||
target_dir = modules_base / src_dir.name
|
||||
print(f"[INFO][MODULE] Cel modułu: {norm(target_dir)}")
|
||||
@@ -700,21 +577,8 @@ def process_module(src_dir: Path, modules_base: Path, confirm_delete: bool, dry_
|
||||
print(f"[DONE][MODULE] {src_dir.name} -> commit 'sync: {module_id} v{new_ver}' wykonany." if not dry_run
|
||||
else f"[DRY-RUN][MODULE] {src_dir.name} -> commit planowany.")
|
||||
|
||||
if ARGS.gh_release and not dry_run:
|
||||
tag = build_release_tag(module_id, new_ver)
|
||||
title = f"{module_id} v{new_ver}"
|
||||
template = getattr(ARGS, "release_notes_template", RELEASE_NOTES_TEMPLATE)
|
||||
notes = template.format(name=module_id, version=new_ver, tag=tag)
|
||||
assets = [zip_path] if zip_path else []
|
||||
if commit_sha is None:
|
||||
commit_sha = git_current_head(src_dir)
|
||||
create_git_tag(src_dir, tag, title, commit_sha)
|
||||
push_git_tag(src_dir, tag)
|
||||
publish_github_release(src_dir, tag, title, notes, assets)
|
||||
|
||||
|
||||
def process_non_module(src_dir: Path, dry_run: bool) -> None:
|
||||
new_ver, commit_sha = preflight_non_module(src_dir, dry_run=dry_run)
|
||||
new_ver = preflight_non_module(src_dir, dry_run=dry_run)
|
||||
# ZIP po commicie (jeśli ustawienie pozwala)
|
||||
zip_path: Optional[Path] = None
|
||||
if should_zip(is_module=False, zip_mode=ARGS.zip_mode):
|
||||
@@ -726,20 +590,6 @@ def process_non_module(src_dir: Path, dry_run: bool) -> None:
|
||||
print(f"[DONE][FOLDER] {src_dir.name} -> version.nfo = {new_ver}; commit wykonany." if not dry_run
|
||||
else f"[DRY-RUN][FOLDER] {src_dir.name} -> version.nfo będzie {new_ver}; commit planowany.")
|
||||
|
||||
if ARGS.gh_release and not dry_run:
|
||||
name = src_dir.name
|
||||
tag = build_release_tag(name, new_ver)
|
||||
title = f"{name} v{new_ver}"
|
||||
template = getattr(ARGS, "release_notes_template", RELEASE_NOTES_TEMPLATE)
|
||||
notes = template.format(name=name, version=new_ver, tag=tag)
|
||||
assets = [zip_path] if zip_path else []
|
||||
if commit_sha is None:
|
||||
commit_sha = git_current_head(src_dir)
|
||||
create_git_tag(src_dir, tag, title, commit_sha)
|
||||
push_git_tag(src_dir, tag)
|
||||
publish_github_release(src_dir, tag, title, notes, assets)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
global ARGS
|
||||
ARGS = parse_args()
|
||||
@@ -748,10 +598,6 @@ def main() -> int:
|
||||
if check_mode:
|
||||
ARGS.dry_run = True
|
||||
|
||||
ARGS.release_tag_prefix = (ARGS.release_tag_prefix or "").strip() or None
|
||||
ARGS.release_notes_template = ARGS.release_notes_template or RELEASE_NOTES_TEMPLATE
|
||||
ARGS.git_remote = ARGS.git_remote or GIT_REMOTE_NAME
|
||||
|
||||
mode = ARGS.mode
|
||||
user = ARGS.user
|
||||
appdata_kind = ARGS.appdata_kind if ARGS.appdata_kind != "None" else None
|
||||
@@ -777,11 +623,9 @@ def main() -> int:
|
||||
if not src_dir.is_dir():
|
||||
raise FileNotFoundError(f"[SINGLE] Brak katalogu: {norm(src_dir)}")
|
||||
|
||||
root_info = maybe_bump_root_version(source_base, changed_any=True, dry_run=dry_run)
|
||||
if root_info:
|
||||
root_bumped = maybe_bump_root_version(source_base, changed_any=True, dry_run=dry_run)
|
||||
if root_bumped:
|
||||
pending_actions = True
|
||||
root_version, root_commit = root_info
|
||||
maybe_publish_root_release(source_base, root_version, root_commit, dry_run)
|
||||
|
||||
pending_actions = True
|
||||
|
||||
@@ -802,11 +646,9 @@ def main() -> int:
|
||||
if changed:
|
||||
pending_actions = True
|
||||
|
||||
root_info = maybe_bump_root_version(source_base, changed_any=bool(changed), dry_run=dry_run)
|
||||
if root_info:
|
||||
root_bumped = maybe_bump_root_version(source_base, changed_any=bool(changed), dry_run=dry_run)
|
||||
if root_bumped:
|
||||
pending_actions = True
|
||||
root_version, root_commit = root_info
|
||||
maybe_publish_root_release(source_base, root_version, root_commit, dry_run)
|
||||
|
||||
if not changed:
|
||||
print("[INFO] Brak zmian do przetworzenia.")
|
||||
|
||||
Reference in New Issue
Block a user