Skip to content

Commit 24a1467

Browse files
authored
Merge pull request #283 from PyAutoLabs/claude/repo-cleanup-82k6kh
feature(branch_archive): move history worth keeping into the Gut, from anywhere
2 parents 0e7c1e4 + 760616c commit 24a1467

2 files changed

Lines changed: 211 additions & 0 deletions

File tree

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
name: Branch Archive (to the Gut)
2+
3+
# Move a branch that must NOT simply be deleted into PyAutoGut, then remove it
4+
# from the repo it came from.
5+
#
6+
# WHY THIS IS NOT THE SWEEP. branch_sweep deletes branches whose content is
7+
# provably already in main — spent work, recoverable from main forever. This is
8+
# the opposite case: a branch worth keeping precisely BECAUSE main does not
9+
# contain it. The motivating example is PyAutoHands' `master`, 442 commits from
10+
# 2021-2022 orphaned by a 2023 "history reset", sharing no ancestor with main
11+
# and pinned by no tag. Deleting it would have made those commits unreachable.
12+
#
13+
# So the two must never share a code path: the sweep's question is "is this
14+
# already in main?", and this workflow's is "is this safely in the Gut yet?".
15+
#
16+
# THE ORDERING IS THE WHOLE POINT. Archive, VERIFY the ref exists on the Gut
17+
# remote, and only then delete. Not archive-and-assume: a push that reports
18+
# success but lands nothing, or lands under a different name, would otherwise
19+
# be followed by an irreversible delete. The verification is a separate
20+
# `ls-remote` against the Gut, not a reading of the push's own exit code.
21+
#
22+
# The archive itself is delegated to `pyauto-gut archive`, not reimplemented
23+
# here. PyAutoGut is the organ that holds and voids; the Brain only drives it
24+
# (the Heart ↔ vitals template). Reimplementing the push would put the Gut's
25+
# namespace convention in two places.
26+
#
27+
# Filing the condemned.md entry is deliberately NOT done here — that index is
28+
# Mind state, written by the hygiene conductor's condemn pass with a human
29+
# deciding the transit clock. Some material (original project history) should
30+
# be held indefinitely rather than swept, and that is a judgement, not a step.
31+
32+
on:
33+
workflow_dispatch:
34+
inputs:
35+
repo:
36+
description: "owner/repo the branch lives in (must be a sweepable repo)"
37+
type: string
38+
required: true
39+
branch:
40+
description: "Branch to archive, e.g. master"
41+
type: string
42+
required: true
43+
name:
44+
description: "Name under archive/condemned/ in the Gut, e.g. pyautohands-pre-2023-history"
45+
type: string
46+
required: true
47+
delete_after:
48+
description: "Delete the source branch once the archive is verified"
49+
type: boolean
50+
default: false
51+
52+
permissions:
53+
contents: read
54+
55+
concurrency:
56+
group: branch-archive
57+
cancel-in-progress: false
58+
59+
jobs:
60+
archive:
61+
runs-on: ubuntu-latest
62+
steps:
63+
- name: Check out PyAutoBrain (target policy)
64+
uses: actions/checkout@v4
65+
with:
66+
fetch-depth: 1
67+
68+
- name: Check out PyAutoMind (the body map)
69+
uses: actions/checkout@v4
70+
with:
71+
repository: ${{ github.repository_owner }}/PyAutoMind
72+
path: .mind
73+
fetch-depth: 1
74+
75+
- name: Check out PyAutoGut (the organ that holds)
76+
uses: actions/checkout@v4
77+
with:
78+
repository: ${{ github.repository_owner }}/PyAutoGut
79+
path: .gut
80+
fetch-depth: 1
81+
82+
- uses: actions/setup-python@v5
83+
with:
84+
python-version: "3.12"
85+
- run: pip install --quiet pyyaml
86+
87+
- name: Archive, verify, then delete
88+
env:
89+
PAT: ${{ secrets.PAT_PYAUTOLABS }}
90+
run: |
91+
set -euo pipefail
92+
repo='${{ inputs.repo }}'
93+
branch='${{ inputs.branch }}'
94+
name='${{ inputs.name }}'
95+
owner='${{ github.repository_owner }}'
96+
97+
[ -n "${PAT:-}" ] || { echo "::error::PAT_PYAUTOLABS is not set."; exit 1; }
98+
99+
# Same boundary as the sweep: a repo the body map does not class as a
100+
# development repo is not one this may touch.
101+
allowed=$(python3 bin/branch_sweep_targets.py .mind/repos.yaml | xargs)
102+
case " $allowed " in
103+
*" $repo "*) ;;
104+
*) echo "::error::'$repo' is not a sweepable repo — see bin/branch_sweep_targets.py."; exit 1 ;;
105+
esac
106+
107+
# The archive name lands in a shared namespace; keep it a plain slug
108+
# so it cannot climb out of archive/condemned/ or collide by accident.
109+
case "$name" in
110+
*[!a-zA-Z0-9._-]*|""|.*) echo "::error::archive name must be a plain slug (a-z 0-9 . _ -)."; exit 1 ;;
111+
esac
112+
113+
src="https://x-access-token:${PAT}@github.com/${repo}.git"
114+
gut="https://x-access-token:${PAT}@github.com/${owner}/PyAutoGut.git"
115+
ref="refs/heads/archive/condemned/${name}"
116+
117+
# Refuse to delete a repo's default branch, whatever it is called.
118+
default=$(git ls-remote --symref "$src" HEAD | sed -n 's|^ref: refs/heads/\([^\t]*\)\tHEAD|\1|p')
119+
if [ "$branch" = "$default" ]; then
120+
echo "::error::'$branch' is the default branch of $repo. Refusing."
121+
exit 1
122+
fi
123+
124+
git config --global user.email "actions@github.com"
125+
git config --global user.name "PyAuto Branch Archive"
126+
127+
echo "::group::Fetch $repo@$branch"
128+
cd .gut
129+
git remote set-url origin "$gut"
130+
git fetch --quiet "$src" "refs/heads/${branch}:refs/heads/__to_archive__"
131+
sha=$(git rev-parse __to_archive__)
132+
count=$(git rev-list --count __to_archive__)
133+
echo " $repo@$branch = $sha ($count commits)"
134+
echo "::endgroup::"
135+
136+
echo "::group::Archive into the Gut"
137+
# The Gut owns its own namespace and its own refusal-to-overwrite.
138+
PYAUTO_GUT_REMOTE=origin bin/pyauto-gut archive __to_archive__ "$name"
139+
echo "::endgroup::"
140+
141+
echo "::group::Verify the archive actually landed"
142+
# Deliberately a fresh question to the remote, not the push's exit
143+
# code: the delete below is irreversible and must not rest on an
144+
# assumption about what just happened.
145+
landed=$(git ls-remote origin "$ref" | awk '{print $1}')
146+
if [ -z "$landed" ]; then
147+
echo "::error::$ref is not on the Gut after the push. NOT deleting anything."
148+
exit 1
149+
fi
150+
if [ "$landed" != "$sha" ]; then
151+
echo "::error::$ref is $landed on the Gut but the branch was $sha. NOT deleting anything."
152+
exit 1
153+
fi
154+
echo " verified $ref = $landed"
155+
echo "::endgroup::"
156+
157+
{
158+
echo "## Archived to the Gut"
159+
echo
160+
echo "| | |"
161+
echo "|---|---|"
162+
echo "| source | \`$repo@$branch\` |"
163+
echo "| commits | $count |"
164+
echo "| sha | \`$sha\` |"
165+
echo "| archive-ref | \`$ref\` |"
166+
} >> "$GITHUB_STEP_SUMMARY"
167+
168+
if [ '${{ inputs.delete_after }}' != 'true' ]; then
169+
echo "delete_after not set — the source branch is untouched."
170+
echo >> "$GITHUB_STEP_SUMMARY"
171+
echo "_Source branch left in place (\`delete_after\` was not set)._" >> "$GITHUB_STEP_SUMMARY"
172+
exit 0
173+
fi
174+
175+
echo "::group::Delete $repo@$branch"
176+
git push "$src" --delete "$branch"
177+
echo " deleted $repo@$branch"
178+
echo "::endgroup::"
179+
180+
echo >> "$GITHUB_STEP_SUMMARY"
181+
echo "Source branch deleted. Recover with:" >> "$GITHUB_STEP_SUMMARY"
182+
echo '```' >> "$GITHUB_STEP_SUMMARY"
183+
echo "pyauto-gut recover $name" >> "$GITHUB_STEP_SUMMARY"
184+
echo '```' >> "$GITHUB_STEP_SUMMARY"
185+
echo >> "$GITHUB_STEP_SUMMARY"
186+
echo "Now file the \`condemned.md\` entry in PyAutoMind — this workflow does not." >> "$GITHUB_STEP_SUMMARY"

skills/repo_cleanup/reference.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,31 @@ Never-touched repos fall out with their categories — `autolens_assistant` is a
158158
names them twice. Brain code may not name satellite repos at all (the tenant
159159
firewall enforces this; a hardcoded list was written first and rejected).
160160

161+
## Branches that must not simply be deleted
162+
163+
A branch the sweep classes as *unmerged* is not automatically live work. It may
164+
be history that `main` no longer contains and nothing else pins — the 2026-08-25
165+
org-wide audit found one: a `master` carrying 442 commits from 2021-2022,
166+
orphaned by a later "history reset", sharing **no common ancestor** with `main`
167+
and referenced by no tag. Deleting that is not cleanup, it is loss.
168+
169+
Tell the two apart before proposing anything:
170+
171+
```bash
172+
git merge-base origin/main origin/<branch> # exit 1 + no output = disjoint histories
173+
git tag --contains $(git rev-parse origin/<branch>) # empty = nothing else pins it
174+
```
175+
176+
Disjoint or unpinned → route it to the Gut instead of the delete bucket:
177+
dispatch `branch_archive.yml` (PyAutoBrain) with `repo`, `branch`, `name`, and
178+
`delete_after`. It archives via `pyauto-gut archive`, **verifies the ref landed
179+
on the Gut with a fresh `ls-remote`**, and only then deletes the source branch —
180+
never on the strength of the push's own exit code.
181+
182+
It does not file the `condemned.md` entry. That is Mind state and a judgement:
183+
material like original project history should be held **undated** (no
184+
`sweep-after`), so no later sweep ever voids it.
185+
161186
That per-repo human read replaces the local sweep's *"never enumerate
162187
origin-only collaborator branches"* rule, which a workflow cannot honour —
163188
every branch it sees is origin-only. The libraries and workspaces take pull

0 commit comments

Comments
 (0)