Commit 410b2b68 authored by Vacaliuc, Bogdan's avatar Vacaliuc, Bogdan
Browse files

plan: scripts/ — add cleanup-dry-run-refs.sh



Per reviewer request: a standalone script the user can run after a
dry-run effort to identify (default) and optionally remove
(--apply) any tags or branches left on the remote with the
{dry-run-prefix}.

Usage:
  ./cleanup-dry-run-refs.sh                      # list-only, today's prefix
  PREFIX=dry-run-2026-04-28 ./cleanup-dry-run-refs.sh           # list specific
  PREFIX=dry-run-2026-04-28 ./cleanup-dry-run-refs.sh --apply   # delete

Behaviors:
  - Cheap discovery via `git ls-remote {remote}`; no fetch.
  - Strips ^{} peel suffix and dedups by ref-name (annotated tags
    otherwise list twice with different SHAs — tag object + peeled
    commit). Tested against the 2026-04-28 dry-run residue: shows
    14 distinct refs as expected (was 15 before the dedup fix).
  - In --apply mode: deletes remote refs first
    (git push --delete), then local tracking refs (refs/heads/,
    refs/tags/, refs/remotes/{remote}/). Continues on individual
    delete failures and tallies summary at the end.
  - Defaults: REMOTE=agentic, PREFIX=dry-run-<today-UTC>.
  - Mode 700, set -uo pipefail, --help, exit 1 on bad arg, exit 2
    on git ls-remote failure.

Complements the workers' protocol --delete pushes during normal
operation. Use this script after abnormal exits, killed sessions,
network blips during cleanup, or to sweep an old dry-run prefix
that no agent is currently watching.

Co-Authored-By: default avatarClaude Opus 4.7 (1M context) <noreply@anthropic.com>
parent fdf3749e
Loading
Loading
Loading
Loading
+170 −0
Original line number Diff line number Diff line
#!/usr/bin/env bash
# cleanup-dry-run-refs.sh — list (and optionally delete) every ref on
# {remote} matching {dry-run-prefix}-*
#
# Per orchestration.md §8 v2.1 end-state contract and dry-run.md §9.
# Use after a dry run to clean up residual refs that the workers'
# protocol --delete pushes might have missed (abnormal exit, killed
# session mid-cycle, network blip during cleanup, etc.).
#
# Usage:
#   ./cleanup-dry-run-refs.sh                                  # dry-run; list only
#   ./cleanup-dry-run-refs.sh --apply                          # actually delete
#   PREFIX=dry-run-2026-04-28 REMOTE=agentic ./cleanup-dry-run-refs.sh --apply
#
# Environment:
#   REMOTE   git remote name (default: agentic)
#   PREFIX   dry-run prefix without trailing dash (default: dry-run-<today>)
#
# Exit code:
#   0 — completed (with or without deletes; with or without --apply)
#   1 — bad args
#   2 — git ls-remote failed (network / auth)
#
# Examples:
#   # 1. Inspect what's left from today's dry run on agentic:
#   PREFIX=dry-run-2026-05-02 ./cleanup-dry-run-refs.sh
#
#   # 2. Delete what's left from a specific dry run:
#   PREFIX=dry-run-2026-04-28 ./cleanup-dry-run-refs.sh --apply
#
#   # 3. Inspect and delete from a non-default remote:
#   REMOTE=upstream PREFIX=dry-run-2026-04-28 \
#       ./cleanup-dry-run-refs.sh --apply
#
# Cleanup also removes local tracking refs and tags matching the
# prefix (refs/heads/{prefix}-*, refs/tags/{prefix}-*,
# refs/remotes/{remote}/{prefix}-*) so a fresh dry run with the same
# prefix starts from a clean local state.

set -uo pipefail

REMOTE="${REMOTE:-agentic}"
PREFIX="${PREFIX:-dry-run-$(date -u +%Y-%m-%d)}"

APPLY=0
case "${1:-}" in
  ""|"--list")    APPLY=0 ;;
  "--apply")      APPLY=1 ;;
  "-h"|"--help")
    sed -n '/^# /,/^$/p' "$0" | sed 's/^# //; s/^#$//'
    exit 0
    ;;
  *)
    echo "error: unknown argument '$1' (use --apply or --list, or --help)" >&2
    exit 1
    ;;
esac

echo "remote = $REMOTE"
echo "prefix = $PREFIX"
echo "mode   = $([ $APPLY -eq 1 ] && echo APPLY || echo LIST-ONLY)"
echo

# 1) Discover remote refs under the prefix. Strip ^{} peels.
echo "=== Remote refs under $PREFIX-*/* on $REMOTE ==="
if ! refs_now=$(git ls-remote "$REMOTE" 2>&1); then
  echo "git ls-remote failed:" >&2
  echo "$refs_now" >&2
  exit 2
fi

# Strip ^{} peel and dedup by ref-name (annotated tags otherwise
# show twice — once as the tag object, once as the peeled commit
# both having the same name after the strip but different SHAs).
remote_refs=$(echo "$refs_now" \
  | awk -v prefix="$PREFIX" '
      {
        is_peel = ($2 ~ /\^\{\}$/)
        sub(/\^\{\}$/, "", $2)
        if ($2 ~ "refs/(heads|tags)/" prefix "-") {
          # Prefer the non-peel line (tag-object SHA) when both exist;
          # only print the first time we see this ref-name.
          if (!(seen[$2]++)) {
            print $0
          }
        }
      }' \
  | sort -u)

if [[ -z "$remote_refs" ]]; then
  echo "  (none)"
else
  echo "$remote_refs" | awk '{print "  " $2 " @ " substr($1,1,12)}'
fi
echo

# 2) Discover local refs (branches, tags, remote-tracking).
echo "=== Local refs matching $PREFIX-* ==="
local_branches=$(git for-each-ref --format='%(refname)' \
  "refs/heads/${PREFIX}-*" 2>/dev/null || true)
local_tags=$(git for-each-ref --format='%(refname)' \
  "refs/tags/${PREFIX}-*" 2>/dev/null || true)
local_remote_tracking=$(git for-each-ref --format='%(refname)' \
  "refs/remotes/${REMOTE}/${PREFIX}-*" 2>/dev/null || true)

local_all="$local_branches"$'\n'"$local_tags"$'\n'"$local_remote_tracking"
local_all=$(echo "$local_all" | sed '/^$/d')

if [[ -z "$local_all" ]]; then
  echo "  (none)"
else
  echo "$local_all" | sed 's/^/  /'
fi
echo

# 3) If listing only, exit cleanly.
if [[ $APPLY -eq 0 ]]; then
  remote_count=$(echo "$remote_refs" | sed '/^$/d' | wc -l)
  local_count=$(echo "$local_all" | sed '/^$/d' | wc -l)
  echo "LIST-ONLY mode (default). To delete, re-run with --apply."
  echo "Would delete: ${remote_count} remote ref(s), ${local_count} local ref(s)."
  exit 0
fi

# 4) APPLY mode: delete remote refs first (one push --delete per ref).
echo "=== Deleting remote refs ==="
deleted_remote=0
failed_remote=0
if [[ -n "$remote_refs" ]]; then
  while IFS= read -r line; do
    [[ -z "$line" ]] && continue
    refname=$(echo "$line" | awk '{print $2}')
    if git push "$REMOTE" --delete "$refname" 2>&1 | sed 's/^/  /'; then
      deleted_remote=$((deleted_remote + 1))
    else
      failed_remote=$((failed_remote + 1))
    fi
  done <<<"$remote_refs"
else
  echo "  (none)"
fi
echo

# 5) Delete local refs.
echo "=== Deleting local refs ==="
deleted_local=0
failed_local=0
if [[ -n "$local_all" ]]; then
  while IFS= read -r refname; do
    [[ -z "$refname" ]] && continue
    if git update-ref -d "$refname" 2>&1 | sed 's/^/  /'; then
      echo "  deleted $refname"
      deleted_local=$((deleted_local + 1))
    else
      failed_local=$((failed_local + 1))
    fi
  done <<<"$local_all"
else
  echo "  (none)"
fi
echo

echo "=== Summary ==="
echo "  remote: deleted=${deleted_remote}, failed=${failed_remote}"
echo "  local : deleted=${deleted_local}, failed=${failed_local}"

if (( failed_remote > 0 || failed_local > 0 )); then
  exit 2
fi
exit 0