sync: help_scripts v0.4

This commit is contained in:
2025-10-29 15:51:25 +01:00
parent e0916f4845
commit 4677df4b80
10 changed files with 1641 additions and 270 deletions
+229 -36
View File
@@ -57,6 +57,11 @@ DRY_RUN: bool = False # globalny tryb symulacji (nic nie zmienia)
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
@@ -123,6 +128,87 @@ 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:
@@ -306,33 +392,36 @@ 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) -> None:
def run_git_commands(repo_dir: Path, commit_msg: str, dry_run: bool) -> Optional[str]:
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
return None
def run(cmd: List[str]) -> None:
rc, out = sh(cmd, repo_dir)
if rc != 0:
raise RuntimeError(f"Błąd podczas uruchamiania: {' '.join(cmd)}\nWyjście:\n{out}")
raise RuntimeError(f"Blad podczas uruchamiania: {' '.join(cmd)}\nWyjscie:\n{out}")
if out.strip():
print(out)
run(["git", "add", "-A"])
rc, out = sh(["git", "commit", "-m", commit_msg], repo_dir)
if rc != 0:
raise RuntimeError(f"Błąd podczas git commit:\n{out}")
raise RuntimeError(f"Blad podczas git commit:\n{out}")
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]:
def preflight_module(src_dir: Path, dry_run: bool) -> Tuple[str, str, Optional[str]]:
"""
Moduł: odczyt module.json, bump, zapis (jeśli nie dry_run), commit i push.
Zwraca (module_id, new_version).
Zwraca (module_id, new_version, commit_sha lub None przy dry-run).
"""
manifest_path = src_dir / "module.json"
manifest = read_manifest(manifest_path)
@@ -346,26 +435,29 @@ def preflight_module(src_dir: Path, dry_run: bool) -> Tuple[str, str]:
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}"
run_git_commands(src_dir, commit_msg, dry_run=False)
commit_sha = run_git_commands(src_dir, commit_msg, dry_run=False)
return module_id, new_version
return module_id, new_version, commit_sha
def preflight_non_module(src_dir: Path, dry_run: bool) -> str:
def preflight_non_module(src_dir: Path, dry_run: bool) -> Tuple[str, Optional[str]]:
"""
Niemoduł: version.nfo w formacie '0.x' (x int).
- jeśli brak -> '0.1'
- jeśli jest -> inkrementuj x
Zapis (nie dry-run) oraz osobny commit/push.
Zwraca new_version (np. '0.4').
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}")
@@ -373,32 +465,36 @@ def preflight_non_module(src_dir: Path, dry_run: bool) -> str:
with nfo.open("w", encoding="utf-8") as f:
f.write(new_version + "\n")
commit_msg = f"sync: {src_dir.name} v{new_version}"
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, commit_sha
# istnieje -> wczytaj i bumpnij
content = nfo.read_text(encoding="utf-8").strip()
if not content.startswith("0."):
raise ValueError(f"{norm(nfo)}: oczekiwano formatu '0.x', otrzymano '{content}'")
if not (content.startswith("0.") or content.startswith("1.")):
raise ValueError(f"{norm(nfo)}: oczekiwano formatu '0/1.x', otrzymano '{content}'")
tail = content.split(".", 1)[1]
head = content.split(".",1)[0]
try:
_ = int(head)
x = int(tail)
except ValueError:
raise ValueError(f"{norm(nfo)}: część po '0.' nie jest liczbą: '{tail}'")
new_version = f"0.{x+1}"
raise ValueError(f"{norm(nfo)}: część po '0/1.' nie jest liczbą: '{tail}'")
new_version = f"{head}.{x+1}"
print(f"[PRE][FOLDER] {src_dir.name}: version {content} -> {new_version}")
if not dry_run:
with nfo.open("w", encoding="utf-8") as f:
f.write(new_version + "\n")
commit_msg = f"sync: {src_dir.name} v{new_version}"
run_git_commands(src_dir, commit_msg, dry_run=False)
commit_sha = run_git_commands(src_dir, commit_msg, dry_run=False)
return new_version
return new_version, commit_sha
# ====== ROOT version.nfo (globalna wersja repo) ======
def maybe_bump_root_version(repo_dir: Path, changed_any: bool, dry_run: bool) -> Optional[str]:
def maybe_bump_root_version(
repo_dir: Path, changed_any: bool, dry_run: bool
) -> Optional[Tuple[str, Optional[str]]]:
"""
Jeśli:
- istnieją zmiany do przetworzenia (changed_any == True)
@@ -434,11 +530,26 @@ def maybe_bump_root_version(repo_dir: Path, changed_any: bool, dry_run: bool) ->
if dry_run:
print("[DRY-RUN][ROOT] Zapis version.nfo i commit/push zostaną pominięte.")
return new_version
return new_version, None
nfo.write_text(new_version + "\n", encoding="utf-8")
run_git_commands(repo_dir, f"sync: root v{new_version}", dry_run=False)
return new_version
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, [])
# ====== ZIP utilities ======
@@ -520,6 +631,28 @@ def parse_args() -> argparse.Namespace:
default=ZIP_OUTPUT_DIR,
help="Katalog wyjściowy na zipy (domyślnie: SOURCE_BASE_DIR/_zips)")
# Tryb check (tylko walidacja)
p.add_argument("--check", dest="check", action="store_true",
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()
@@ -534,7 +667,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 = preflight_module(src_dir, dry_run=dry_run)
module_id, new_ver, commit_sha = 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)}")
@@ -548,10 +681,11 @@ def process_module(src_dir: Path, modules_base: Path, confirm_delete: bool, dry_
print("[INFO] (dry-run) Pytanie o potwierdzenie pominięte.")
# ZIP po commicie, przed podmianą
zip_path: Optional[Path] = None
if should_zip(is_module=True, zip_mode=ARGS.zip_mode):
out_dir = ensure_zip_output_dir(Path(ARGS.source_base_dir), ARGS.zip_output_dir)
try:
make_zip_for_folder(src_dir, new_ver, out_dir, dry_run)
zip_path = make_zip_for_folder(src_dir, new_ver, out_dir, dry_run)
except Exception as e:
raise RuntimeError(f"Błąd zipowania modułu {src_dir.name}: {e}") from e
@@ -566,24 +700,58 @@ 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 = preflight_non_module(src_dir, dry_run=dry_run)
new_ver, commit_sha = 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):
out_dir = ensure_zip_output_dir(Path(ARGS.source_base_dir), ARGS.zip_output_dir)
try:
make_zip_for_folder(src_dir, new_ver, out_dir, dry_run)
zip_path = make_zip_for_folder(src_dir, new_ver, out_dir, dry_run)
except Exception as e:
raise RuntimeError(f"Błąd zipowania folderu {src_dir.name}: {e}") from e
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()
check_mode = ARGS.check
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
@@ -595,39 +763,61 @@ def main() -> int:
dry_run = ARGS.dry_run
if not source_base.is_dir():
raise FileNotFoundError(f"Katalog źródłowy nie istnieje: {norm(source_base)}")
raise FileNotFoundError(f"Katalog zrodlowy nie istnieje: {norm(source_base)}")
ensure_git_repo(source_base)
# Ustal bazę modules (tylko raz)
modules_base = resolve_modules_base(user, relative_pattern, appdata_kind, modules_override)
print(f"[INFO] Katalog modules base: {norm(modules_base)}")
pending_actions = False
if mode == "single":
src_dir = source_base / module_subdir_single
if not src_dir.is_dir():
raise FileNotFoundError(f"[SINGLE] Brak katalogu: {norm(src_dir)}")
# Root bump (globalna wersja) tylko jeśli "są zmiany do przetworzenia" (single -> True)
maybe_bump_root_version(source_base, changed_any=True, dry_run=dry_run)
root_info = maybe_bump_root_version(source_base, changed_any=True, dry_run=dry_run)
if root_info:
pending_actions = True
root_version, root_commit = root_info
maybe_publish_root_release(source_base, root_version, root_commit, dry_run)
pending_actions = True
if (src_dir / "module.json").is_file():
print(f"[MODE=SINGLE][MODULE] {src_dir.name}")
process_module(src_dir, modules_base, confirm_delete, dry_run)
else:
print(f"[MODE=SINGLE][FOLDER] {src_dir.name}")
process_non_module(src_dir, dry_run)
print("[SUKCES] Tryb SINGLE zakończony.")
print("[SUKCES] Tryb SINGLE zakonczony.")
if check_mode:
print("[CHECK] Wykryto oczekujace akcje (tryb single).")
return 1
return 0
# AUTO: wykryj zmienione katalogi
changed = git_changed_top_level_dirs(source_base)
print(f"[AUTO] Zmienione katalogi w {norm(source_base)}: {changed if changed else '(brak)'}")
# Root bump (globalna wersja) tylko jeśli są jakiekolwiek zmiany do przetworzenia
maybe_bump_root_version(source_base, changed_any=bool(changed), dry_run=dry_run)
if changed:
pending_actions = True
root_info = maybe_bump_root_version(source_base, changed_any=bool(changed), dry_run=dry_run)
if root_info:
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.")
if check_mode:
if pending_actions:
print("[CHECK] Wykryto oczekujace akcje.")
return 1
print("[CHECK] OK - brak akcji wymaganych.")
return 0
# Przetwarzaj w stałej kolejności
# Przetwarzaj w stalej kolejnosci
for name in changed:
src_dir = source_base / name
if (src_dir / "module.json").is_file():
@@ -637,7 +827,10 @@ def main() -> int:
print(f"\n[AUTO][FOLDER] {name}")
process_non_module(src_dir, dry_run)
print("\n[SUKCES] Tryb AUTO zakończony.")
print("\n[SUKCES] Tryb AUTO zakonczony.")
if check_mode and pending_actions:
print("[CHECK] Wykryto oczekujace akcje.")
return 1
return 0
+1 -1
View File
@@ -1 +1 @@
0.3
0.4