feat(gitflow): auto-purge transient superpowers artifacts at finish (BDR-065)
_gitflow_purge_transient removes docs/superpowers/{specs,plans} on the
feature/bugfix branch just before the directed merge, so develop's tip
lands clean while the feature commits stay reachable as the archive
(git show <sha>:...). Best-effort: never aborts a finish (no-op when
absent, skip on dirty paths, restore index+tree on commit failure).
Opt-out GITFLOW_PURGE_TRANSIENT=0; purge-transient CLI verb. Automates
the manual post-merge cleanup BDR-065 left as doctrine (slipped once,
655e364). Universal via the ~/.claude/lib symlink. gitflow-test T17 a-d;
shellcheck clean; make test exit 0.
This commit is contained in:
@@ -32,8 +32,13 @@ or re-run `make plugin`.
|
|||||||
|
|
||||||
`docs/superpowers/specs/**` and `docs/superpowers/plans/**` are run-time
|
`docs/superpowers/specs/**` and `docs/superpowers/plans/**` are run-time
|
||||||
artifacts of a feature pipeline (subagent briefs, reviewer references).
|
artifacts of a feature pipeline (subagent briefs, reviewer references).
|
||||||
They are committed DURING the run and DELETED in the post-merge cleanup
|
They are committed DURING the run (the SDD worktree + reviewers read them
|
||||||
(BDR-065) — git history at the feature commits is their archive. Durable
|
from disk — NOT gitignored), then AUTO-PURGED by `gitflow finish` on a
|
||||||
knowledge goes to `.claude/memory/` registries, never to these files.
|
`feature`/`bugfix` branch, before the merge, so develop's tip stays clean
|
||||||
Derived scan/audit outputs (`.audit/**`) are gitignored and never
|
(BDR-065, `lib/gitflow.sh` `_gitflow_purge_transient`). The feature commits
|
||||||
committed, even redacted (LRN-124).
|
stay reachable from develop, so `git show <sha>:docs/…` is still the archive.
|
||||||
|
Opt out with `GITFLOW_PURGE_TRANSIENT=0`. NOT in scope: `.claude/tasks/{contracts,plans}`
|
||||||
|
(durable, versioned, referenced by decisions.md). Durable knowledge goes to
|
||||||
|
`.claude/memory/` registries, never to these files. Derived scan/audit
|
||||||
|
outputs (`.audit/**`) are gitignored and never committed, even redacted
|
||||||
|
(LRN-124).
|
||||||
|
|||||||
@@ -239,6 +239,7 @@ gitflow_start feature glwork >/dev/null 2>&1
|
|||||||
# proving this backstop is NOT gated by the branch-protection check above it)
|
# proving this backstop is NOT gated by the branch-protection check above it)
|
||||||
printf 'aws_access_key_id = AKIA%s\n' "GDR5XRBXYARW2I5N" > secret.txt
|
printf 'aws_access_key_id = AKIA%s\n' "GDR5XRBXYARW2I5N" > secret.txt
|
||||||
git add secret.txt
|
git add secret.txt
|
||||||
|
# shellcheck disable=SC2034 # gl_out is used in the deferred chk eval strings
|
||||||
gl_out="$(git commit -q -m "add secret" 2>&1)"; gl_rc=$?
|
gl_out="$(git commit -q -m "add secret" 2>&1)"; gl_rc=$?
|
||||||
chk "T16a fake secret on feature branch → blocked" "[ $gl_rc -ne 0 ]"
|
chk "T16a fake secret on feature branch → blocked" "[ $gl_rc -ne 0 ]"
|
||||||
chk "T16a message mentions gitleaks" 'printf "%s" "$gl_out" | grep -qi gitleaks'
|
chk "T16a message mentions gitleaks" 'printf "%s" "$gl_out" | grep -qi gitleaks'
|
||||||
@@ -252,10 +253,57 @@ chk "T16b clean commit still succeeds" 'git commit -q -m "clean work" 2>/dev/nul
|
|||||||
# T16c — gitleaks missing from PATH → warn, never block (defense in depth
|
# T16c — gitleaks missing from PATH → warn, never block (defense in depth
|
||||||
# must not become a new single point of failure)
|
# must not become a new single point of failure)
|
||||||
echo clean2 > clean2.txt; git add clean2.txt
|
echo clean2 > clean2.txt; git add clean2.txt
|
||||||
|
# shellcheck disable=SC2034 # noleaks_out is used in the deferred chk eval strings
|
||||||
noleaks_out="$(PATH=/usr/bin:/bin git commit -q -m "clean work 2" 2>&1)"; noleaks_rc=$?
|
noleaks_out="$(PATH=/usr/bin:/bin git commit -q -m "clean work 2" 2>&1)"; noleaks_rc=$?
|
||||||
chk "T16c missing-gitleaks → still commits (rc0)" "[ $noleaks_rc -eq 0 ]"
|
chk "T16c missing-gitleaks → still commits (rc0)" "[ $noleaks_rc -eq 0 ]"
|
||||||
chk "T16c missing-gitleaks → warns" 'printf "%s" "$noleaks_out" | grep -qi "not installed"'
|
chk "T16c missing-gitleaks → warns" 'printf "%s" "$noleaks_out" | grep -qi "not installed"'
|
||||||
|
|
||||||
|
echo "T17 — finish auto-purges transient superpowers artifacts (BDR-065)"
|
||||||
|
# T17a — feature carrying docs/superpowers spec+plan: purged before merge,
|
||||||
|
# develop TIP clean, artifacts still recoverable from history (archive property)
|
||||||
|
newrepo purgefeat; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||||
|
gitflow_start feature pf >/dev/null 2>&1
|
||||||
|
mkdir -p docs/superpowers/specs docs/superpowers/plans
|
||||||
|
echo spec > docs/superpowers/specs/s.md
|
||||||
|
echo plan > docs/superpowers/plans/p.md
|
||||||
|
echo code > feat.txt
|
||||||
|
git add -A; git commit -q -m "feat + transient spec/plan"
|
||||||
|
gitflow_finish >/dev/null 2>&1
|
||||||
|
# the add-commit stays reachable from develop via the --no-ff merge's 2nd parent;
|
||||||
|
# --full-history defeats the path simplification that hides it, and `git show
|
||||||
|
# <sha>:path` proves BDR-065's "git history = the archive" recovery.
|
||||||
|
# shellcheck disable=SC2034 # pf_add_sha is used in the deferred chk eval string
|
||||||
|
pf_add_sha="$(git log develop --full-history --format=%H -- docs/superpowers/specs/s.md | tail -1)"
|
||||||
|
chk "T17a merged into develop" 'git log develop --oneline | grep -q "Merge feature/pf into develop"'
|
||||||
|
chk "T17a develop TIP has no transient" '[ -z "$(git ls-tree -r develop --name-only -- docs/superpowers)" ]'
|
||||||
|
chk "T17a purge commit on record" 'git log develop --oneline | grep -q "purge transient planning artifacts"'
|
||||||
|
chk "T17a artifact recoverable from history" '[ "$(git show "$pf_add_sha":docs/superpowers/specs/s.md 2>/dev/null)" = spec ]'
|
||||||
|
chk "T17a non-transient code survives" 'git ls-tree -r develop --name-only | grep -qx feat.txt'
|
||||||
|
chk "T17a feature branch deleted" '! git rev-parse --verify -q refs/heads/feature/pf >/dev/null'
|
||||||
|
|
||||||
|
# T17b — no artifacts → purge is a silent no-op, no spurious commit
|
||||||
|
newrepo purgenone; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||||
|
gitflow_start feature pn >/dev/null 2>&1; echo w>w.txt; git add w.txt; git commit -q -m w
|
||||||
|
gitflow_finish >/dev/null 2>&1
|
||||||
|
chk "T17b merged into develop" 'git log develop --oneline | grep -q "Merge feature/pn into develop"'
|
||||||
|
chk "T17b no purge commit created" '! git log develop --oneline | grep -q "purge transient"'
|
||||||
|
|
||||||
|
# T17c — opt-out (GITFLOW_PURGE_TRANSIENT=0) keeps the artifacts on develop
|
||||||
|
newrepo purgeoff; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||||
|
gitflow_start feature po >/dev/null 2>&1
|
||||||
|
mkdir -p docs/superpowers/specs; echo spec > docs/superpowers/specs/s.md
|
||||||
|
git add -A; git commit -q -m "feat + spec"
|
||||||
|
GITFLOW_PURGE_TRANSIENT=0 gitflow_finish >/dev/null 2>&1
|
||||||
|
chk "T17c opt-out keeps transient on develop TIP" '[ -n "$(git ls-tree -r develop --name-only -- docs/superpowers)" ]'
|
||||||
|
|
||||||
|
# T17d — chore is OUT of purge scope (only feature/bugfix originate artifacts)
|
||||||
|
newrepo purgechore; echo a>a; hookon; gitflow_init >/dev/null 2>&1
|
||||||
|
gitflow_start chore pc >/dev/null 2>&1
|
||||||
|
mkdir -p docs/superpowers/specs; echo spec > docs/superpowers/specs/s.md
|
||||||
|
git add -A; git commit -q -m "chore + spec"
|
||||||
|
gitflow_finish >/dev/null 2>&1
|
||||||
|
chk "T17d chore leaves transient (not in scope)" '[ -n "$(git ls-tree -r develop --name-only -- docs/superpowers)" ]'
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "==== RESULT: $PASS passed, $FAIL failed ===="
|
echo "==== RESULT: $PASS passed, $FAIL failed ===="
|
||||||
[ "$FAIL" -eq 0 ]
|
[ "$FAIL" -eq 0 ]
|
||||||
|
|||||||
+48
-2
@@ -18,6 +18,12 @@ GITFLOW_MAIN="main"
|
|||||||
GITFLOW_DEVELOP="develop"
|
GITFLOW_DEVELOP="develop"
|
||||||
# template resolved relative to the lib; overridable for tests.
|
# template resolved relative to the lib; overridable for tests.
|
||||||
GITFLOW_GITIGNORE_TEMPLATE="${GITFLOW_GITIGNORE_TEMPLATE:-$_GITFLOW_LIB_DIR/../templates/gitignore/standard.gitignore}"
|
GITFLOW_GITIGNORE_TEMPLATE="${GITFLOW_GITIGNORE_TEMPLATE:-$_GITFLOW_LIB_DIR/../templates/gitignore/standard.gitignore}"
|
||||||
|
# Transient planning artifacts (superpowers spec/plan). A feature/bugfix run
|
||||||
|
# COMMITS them (SDD worktree + reviewers read them from disk); finish PURGES
|
||||||
|
# them before the merge reaches develop's tip (BDR-065). Fixed path list;
|
||||||
|
# read GITFLOW_PURGE_TRANSIENT=0 at finish time to opt out (read in the helper,
|
||||||
|
# never cached here, so an inline `VAR=0 gitflow_finish` override works).
|
||||||
|
GITFLOW_TRANSIENT_PATHS=("docs/superpowers/specs" "docs/superpowers/plans")
|
||||||
|
|
||||||
# ── predicates / pure helpers ────────────────────────────────────────────────
|
# ── predicates / pure helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -97,6 +103,42 @@ _gitflow_delete() { # <branch>
|
|||||||
git branch -q -d "$br" || { echo "gitflow: '$br' not fully merged — branch kept" >&2; return 5; }
|
git branch -q -d "$br" || { echo "gitflow: '$br' not fully merged — branch kept" >&2; return 5; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# _gitflow_purge_transient → remove the committed transient planning artifacts
|
||||||
|
# (BDR-065) from the CURRENT branch just before the directed merge. Result: the
|
||||||
|
# removal rides the feature/bugfix branch, whose earlier commits stay reachable
|
||||||
|
# from develop through the --no-ff merge (`git show <sha>:…` = the archive),
|
||||||
|
# while develop's TIP lands clean. Automates the manual post-merge chore that
|
||||||
|
# BDR-065 left as doctrine (and that slipped once — commit 655e364).
|
||||||
|
#
|
||||||
|
# BEST-EFFORT BY CONTRACT: this NEVER aborts a finish. Nothing tracked → no-op;
|
||||||
|
# uncommitted changes under those paths, or a failed commit → warn + degrade to
|
||||||
|
# the old manual-cleanup behaviour, index/tree restored, merge still proceeds.
|
||||||
|
# The scoped commit (`-- <paths>`) records only the deletions, so a dirty index
|
||||||
|
# is never swept in. Opt out with GITFLOW_PURGE_TRANSIENT=0.
|
||||||
|
_gitflow_purge_transient() {
|
||||||
|
[ "${GITFLOW_PURGE_TRANSIENT:-1}" = 1 ] || return 0
|
||||||
|
local p; local -a tracked=()
|
||||||
|
for p in "${GITFLOW_TRANSIENT_PATHS[@]}"; do
|
||||||
|
[ -n "$(git ls-files -- "$p")" ] && tracked+=("$p")
|
||||||
|
done
|
||||||
|
[ "${#tracked[@]}" -gt 0 ] || return 0 # nothing tracked → no-op
|
||||||
|
# only purge paths with no pending changes → git rm is all-or-nothing safe and
|
||||||
|
# never discards uncommitted work under docs/superpowers.
|
||||||
|
if ! git diff --quiet HEAD -- "${tracked[@]}" 2>/dev/null; then
|
||||||
|
echo "gitflow: transient artifacts have uncommitted changes — purge skipped, finishing without it (clean up by hand)" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if git rm -r -q -- "${tracked[@]}" >/dev/null 2>&1 \
|
||||||
|
&& git commit -q -m "chore: purge transient planning artifacts (BDR-065)" -- "${tracked[@]}"; then
|
||||||
|
echo "gitflow: purged transient planning artifacts before merge (${tracked[*]})" >&2
|
||||||
|
else
|
||||||
|
echo "gitflow: transient-artifact purge failed — finishing without it (clean up by hand)" >&2
|
||||||
|
git reset -q HEAD -- "${tracked[@]}" 2>/dev/null || true # unstage any partial rm
|
||||||
|
git checkout -q -- "${tracked[@]}" 2>/dev/null || true # restore working tree
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
# gitflow_finish [<type> <name>] → directed merge of the CURRENT branch per its
|
# gitflow_finish [<type> <name>] → directed merge of the CURRENT branch per its
|
||||||
# type, then delete. WHEN to call this is the human gate (SKILL.md).
|
# type, then delete. WHEN to call this is the human gate (SKILL.md).
|
||||||
#
|
#
|
||||||
@@ -117,7 +159,10 @@ gitflow_finish() {
|
|||||||
fi
|
fi
|
||||||
type="$(gitflow_branch_type "$br")"
|
type="$(gitflow_branch_type "$br")"
|
||||||
case "$type" in
|
case "$type" in
|
||||||
feature|bugfix|chore)
|
feature|bugfix)
|
||||||
|
_gitflow_purge_transient # BDR-065 auto-cleanup, on HEAD, pre-merge; never blocks
|
||||||
|
_gitflow_merge_into "$GITFLOW_DEVELOP" "$br" && _gitflow_delete "$br" ;;
|
||||||
|
chore)
|
||||||
_gitflow_merge_into "$GITFLOW_DEVELOP" "$br" && _gitflow_delete "$br" ;;
|
_gitflow_merge_into "$GITFLOW_DEVELOP" "$br" && _gitflow_delete "$br" ;;
|
||||||
release)
|
release)
|
||||||
_gitflow_merge_into "$GITFLOW_MAIN" "$br" \
|
_gitflow_merge_into "$GITFLOW_MAIN" "$br" \
|
||||||
@@ -283,8 +328,9 @@ if [ "${BASH_SOURCE[0]}" = "${0}" ]; then
|
|||||||
finish) gitflow_finish "$@" ;;
|
finish) gitflow_finish "$@" ;;
|
||||||
init) gitflow_init "$@" ;;
|
init) gitflow_init "$@" ;;
|
||||||
reconcile) gitflow_reconcile_gitignore "$@" ;;
|
reconcile) gitflow_reconcile_gitignore "$@" ;;
|
||||||
|
purge-transient) _gitflow_purge_transient ;;
|
||||||
install-hook) gitflow_install_hook "$@" ;;
|
install-hook) gitflow_install_hook "$@" ;;
|
||||||
emit-hook) _gitflow_emit_pre_commit ;;
|
emit-hook) _gitflow_emit_pre_commit ;;
|
||||||
*) echo "usage: gitflow.sh {type|protected-base|base-for|release-open|start|finish|init|reconcile|install-hook|emit-hook}" >&2; exit 2 ;;
|
*) echo "usage: gitflow.sh {type|protected-base|base-for|release-open|start|finish|init|reconcile|purge-transient|install-hook|emit-hook}" >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user