diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 000000000..989649664 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,56 @@ +# Scripts + +## Bulk delete old Vercel deployments + +`list-old-deployments.py` fetches all deployments for a given app via the Vercel API and writes UIDs of non-current-production deployments to `old-deployment-uids.txt`. + +### Prerequisites + +- Vercel CLI installed and authenticated (`vercel login`) +- Access to the `saleorcommerce` scope + +### Usage + +1. Edit `APP_NAME` in the script to the target app. + +2. Run the script to generate the list: + + ```bash + python3 scripts/list-old-deployments.py + ``` + + This prints a table of all old deployments and writes their UIDs to `old-deployment-uids.txt` (one per line). + +3. Review the file and remove any deployments you want to keep (e.g. staging aliases): + + ```bash + vim old-deployment-uids.txt + ``` + +4. Delete all remaining deployments (Vercel will show the list and ask for confirmation): + + ```bash + vercel remove $(cat old-deployment-uids.txt) --scope saleorcommerce + ``` + + If there are too many for a single command, delete in batches of 50: + + ```bash + vercel remove $(head -50 old-deployment-uids.txt) --scope saleorcommerce + # After confirming, remove processed lines: + sed -i '' '1,50d' old-deployment-uids.txt + # To skip ahead (e.g. next 50 starting at line 51): + vercel remove $(sed -n '51,100p' old-deployment-uids.txt) --scope saleorcommerce + ``` + +5. Re-run the script to verify everything was cleaned up: + + ```bash + python3 scripts/list-old-deployments.py + ``` + +### Notes + +- Vercel performs safe deletion — it will skip the current production deployment automatically. +- Vercel warns about alias removals before confirming — review these warnings before accepting. +- The `-s` flag skips the confirmation prompt (`vercel remove ... -s`). Use `-y` to auto-confirm. diff --git a/scripts/list-old-deployments.py b/scripts/list-old-deployments.py new file mode 100644 index 000000000..9486c754e --- /dev/null +++ b/scripts/list-old-deployments.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""List all Vercel deployments for saleor-app-smtp that are older than the current production deployment.""" + +import json +import subprocess +import sys +from datetime import datetime, timezone + +APP_NAME = "saleor-app-products-feed" +SCOPE = "saleorcommerce" + + +def vercel_api(endpoint: str) -> dict: + result = subprocess.run( + ["vercel", "api", endpoint, "--scope", SCOPE], + capture_output=True, + text=True, + ) + if result.returncode != 0: + print(f"Error calling vercel api: {result.stderr}", file=sys.stderr) + sys.exit(1) + return json.loads(result.stdout) + + +def fetch_all_deployments() -> list[dict]: + """Fetch all deployments, paginating through all pages.""" + all_deps = [] + next_ts = None + + while True: + url = f"/v6/deployments?app={APP_NAME}&limit=100" + if next_ts: + url += f"&until={next_ts}" + + data = vercel_api(url) + deps = data.get("deployments", []) + pagination = data.get("pagination", {}) + + all_deps.extend(deps) + print( + f" Fetched {len(deps)} deployments (total: {len(all_deps)})", + file=sys.stderr, + ) + + next_ts = pagination.get("next") + if not next_ts or len(deps) == 0: + break + + return all_deps + + +def format_ts(ts: int | None) -> str: + if not ts: + return "N/A" + return datetime.fromtimestamp(ts / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M") + + +def main(): + print("Fetching all deployments...", file=sys.stderr) + all_deps = fetch_all_deployments() + + # Find current production: newest deployment with target=production and readySubstate=PROMOTED + prod_deps = [ + d + for d in all_deps + if d.get("target") == "production" and d.get("readySubstate") == "PROMOTED" + ] + if not prod_deps: + print("ERROR: No current production deployment found!", file=sys.stderr) + sys.exit(1) + + current_prod = max(prod_deps, key=lambda d: d["created"]) + print("\nCurrent production deployment:", file=sys.stderr) + print(f" URL: {current_prod['url']}", file=sys.stderr) + print(f" UID: {current_prod['uid']}", file=sys.stderr) + print(f" Created: {format_ts(current_prod['created'])}", file=sys.stderr) + sha = current_prod.get("meta", {}).get("githubCommitSha", "N/A") + print(f" Commit: {sha[:8]}", file=sys.stderr) + + # Filter: everything except current production, not already soft-deleted + old_deps = [ + d + for d in all_deps + if d["uid"] != current_prod["uid"] and not d.get("softDeletedByRetention") + ] + + print( + f"\nOld deployments to delete: {len(old_deps)} (out of {len(all_deps)} total)\n", + file=sys.stderr, + ) + + # Print table to stdout + header = f"{'URL':<60} {'Target':<12} {'State':<10} {'Commit':<10} {'Branch':<20} {'Creator':<20} {'Created':<18} {'Message'}" + print(header) + print("-" * len(header)) + + for d in sorted(old_deps, key=lambda x: x["created"], reverse=True): + meta = d.get("meta", {}) + sha = meta.get("githubCommitSha", "")[:8] + ref = meta.get("githubCommitRef", "") + msg = meta.get("githubCommitMessage", "")[:60].replace("\n", " ") + target = d.get("target") or "preview" + user = d.get("creator", {}).get("username", "?") + created = format_ts(d.get("created")) + + print( + f"{d['url']:<60} {target:<12} {d['state']:<10} {sha:<10} {ref:<20} {user:<20} {created:<18} {msg}" + ) + + # Output deployment UIDs for bulk deletion (one per line) + uids = [d["uid"] for d in old_deps] + uid_file = "old-deployment-uids.txt" + with open(uid_file, "w") as f: + for uid in uids: + f.write(uid + "\n") + print(f"\nWrote {len(uids)} deployment UIDs to {uid_file}", file=sys.stderr) + print(f"To delete 50 at a time:", file=sys.stderr) + print(f" vercel remove $(head -50 {uid_file}) --scope {SCOPE}", file=sys.stderr) + print(f" sed -i '' '1,50d' {uid_file}", file=sys.stderr) + + +if __name__ == "__main__": + main()