diff --git a/scripts/archive_eol_stable_branches.py b/scripts/archive_eol_stable_branches.py new file mode 100644 index 0000000000..c2cafc9c5c --- /dev/null +++ b/scripts/archive_eol_stable_branches.py @@ -0,0 +1,151 @@ +#! /usr/bin/env python3 +import argparse +import os +import subprocess +import sys + + +def run(cmd, *, cwd=None, env=None, dry_run=True): + """Run a command with optional dry-run behavior.""" + environ = os.environ.copy() + if env: + environ.update(env) + if dry_run: + print("[DRY RUN]", " ".join(cmd)) + else: + print("[EXECUTE]", " ".join(cmd)) + try: + result = subprocess.check_output( + cmd, cwd=cwd, env=environ, stderr=subprocess.STDOUT + ) + except subprocess.CalledProcessError as e: + result = e.output + print(" [ERROR]", result) + raise + else: + print(" [RESULT]", result) + return result.decode().strip() + + +def validate_env(checkout_dir): + if not checkout_dir: + sys.exit("Error: checkout directory not provided (--checkout-dir).") + if not os.path.exists(checkout_dir): + sys.exit(f"Error: checkout directory '{checkout_dir}' does not exist.") + if not os.path.isdir(checkout_dir): + sys.exit(f"Error: '{checkout_dir}' is not a directory.") + + +def get_remote_branches(checkout_dir, include_fn): + """Return list of remote branches filtered by include_fn.""" + result = run( + ["git", "branch", "--list", "-r"], + cwd=checkout_dir, + dry_run=False, + ) + branches = [b.strip() for b in result.split("\n") if b.strip()] + return [b for b in branches if include_fn(b)] + + +def get_branch_info(checkout_dir, branch): + """Return (commit_hash, last_update_date) for a given branch.""" + commit_hash = run(["git", "rev-parse", branch], cwd=checkout_dir, dry_run=False) + last_update = run( + ["git", "show", branch, "--format=format:%ai", "-s"], + cwd=checkout_dir, + dry_run=False, + ) + return commit_hash, last_update + + +def create_tag(checkout_dir, branch, commit_hash, last_update, *, dry_run=True): + """Create a tag locally for a given branch at its last update.""" + tag_name = branch.replace("origin/", "", 1) + msg = f'"Tagged {tag_name} for EOL stable branch removal."' + run( + ["git", "tag", "--sign", "--message", msg, tag_name, commit_hash], + cwd=checkout_dir, + env={"GIT_COMMITTER_DATE": last_update}, + dry_run=dry_run, + ) + return tag_name + + +def delete_remote_and_local_branch(checkout_dir, branch, *, dry_run=True): + """Delete a remote branch from origin and the maching local branch.""" + try: + run( + ["git", "branch", "-D", branch], + cwd=checkout_dir, + dry_run=dry_run, + ) + except subprocess.CalledProcessError: + print(f"[ERROR] Local branch {branch} can not be deleted.") + + run( + ["git", "push", "origin", "--delete", branch.replace("origin/", "", 1)], + cwd=checkout_dir, + dry_run=dry_run, + ) + + +def main(): + parser = argparse.ArgumentParser( + description="Archive Django branches into tags and optionally delete them." + ) + parser.add_argument( + "--checkout-dir", required=True, help="Path to Django git checkout" + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print commands instead of executing them", + ) + parser.add_argument( + "--branches", nargs="*", help="Specific remote branches to include (optional)" + ) + args = parser.parse_args() + + validate_env(args.checkout_dir) + dry_run = args.dry_run + checkout_dir = args.checkout_dir + + if args.branches: + wanted = set(f"origin/{b}" for b in args.branches) + else: + wanted = set() + + branches = get_remote_branches(checkout_dir, include_fn=lambda b: b in wanted) + if not branches: + print("No branches matched inclusion criteria.") + return + + print("\nMatched branches:") + print("\n".join(branches)) + print() + + branch_updates = {b: get_branch_info(checkout_dir, b) for b in branches} + print("\nLast updates:") + for b, (h, d) in branch_updates.items(): + print(f"{b}\t{h}\t{d}") + + if ( + input("\nDelete remote branches and create tags? [y/N]: ").strip().lower() + == "y" + ): + for b, (commit_hash, last_update_date) in branch_updates.items(): + print(f"Creating tag for {b} at {commit_hash=} with {last_update_date=}") + create_tag(checkout_dir, b, commit_hash, last_update_date, dry_run=dry_run) + print(f"Deleting remote branch {b}") + delete_remote_and_local_branch(checkout_dir, b, dry_run=dry_run) + run( + ["git", "push", "--tags"], + cwd=checkout_dir, + dry_run=dry_run, + ) + + print("Done.") + + +if __name__ == "__main__": + main() diff --git a/scripts/confirm_release.sh b/scripts/confirm_release.sh index 920f2061af..c3b4d12c5a 100755 --- a/scripts/confirm_release.sh +++ b/scripts/confirm_release.sh @@ -25,7 +25,7 @@ echo "Download checksum file ..." curl --fail --output "$CHECKSUM_FILE" "${MEDIA_URL_PREFIX}/pgp/${CHECKSUM_FILE}" echo "Verify checksum file ..." -if [ -n "${GPG_KEY}" ] ; then +if [ -n "${GPG_KEY:-}" ] ; then gpg --recv-keys "${GPG_KEY}" fi gpg --verify "${CHECKSUM_FILE}"