Skip to content

Commit 6952692

Browse files
committed
fix(strip): make strip_for_main.sh work mid-merge + align safety lists
Two issues surfaced when attempting Task 28 (develop → main strip merge) of the M-sec plan: 1. strip_for_main.sh could never actually run the documented flow. The header comment said: git merge develop --no-commit ./scripts/strip_for_main.sh git add -A && git commit But the script's dirty-tree guard (git diff --quiet HEAD) refused to run on the dirty post-merge tree, so step 2 always aborted. Fix: detect mid-merge state via $GIT_DIR/MERGE_HEAD presence. When IN_MERGE=1, skip the dirty-tree guard (the dirty tree is expected) and have strip_path use 'git rm -rf --ignore-unmatch' so modify/delete conflicts are resolved (deleted AND staged) in a single step. Outside a merge, the guard still fires — stripping on top of unrelated edits would mix commits. Critical invariant preserved: the 'refuses to run on develop' guard still fires no matter what mode or merge state. develop is the source of dev artifacts; the script must never touch it. 2. Two defense layers had drifted: - strip_for_main.sh had 'adr', .githooks/pre-commit didn't - neither had '.vscode' (editor settings, Snyk IDE prefs etc.) Both lists now agree: 12 strip paths in each. Added an explicit comment in pre-commit telling future editors to keep both in sync. Verified locally: - go build ./cmd/broker ./cmd/aactl: OK - go test -short ./...: 15/15 packages PASS - golangci-lint run ./...: clean - ./scripts/strip_for_main.sh --dry-run (from develop): 12 paths listed including new .vscode entry
1 parent e560301 commit 6952692

3 files changed

Lines changed: 115 additions & 18 deletions

File tree

.githooks/pre-commit

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ if [[ "$BRANCH" != "main" ]]; then
2020
exit 0
2121
fi
2222

23-
# Files/dirs that must NEVER land on main
23+
# Files/dirs that must NEVER land on main.
24+
# Keep this list in sync with the strip_path calls in scripts/strip_for_main.sh.
25+
# The two lists exist for defense in depth:
26+
# - strip_for_main.sh is the automated pre-merge step (runs once per merge)
27+
# - this hook is the last-chance safety net (runs on every commit to main)
28+
# If either catches a stray dev file, the other should too.
2429
FORBIDDEN=(
2530
"MEMORY.md"
2631
"MEMORY_ARCHIVE.md"
@@ -35,6 +40,8 @@ FORBIDDEN=(
3540
".claude"
3641
".agents"
3742
"audit"
43+
"adr"
44+
".vscode"
3845
"generate_pdf.py"
3946
)
4047

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed — strip_for_main.sh mid-merge support + two drift fixes
11+
12+
- **`scripts/strip_for_main.sh`** — the documented `git merge develop
13+
--no-commit` → strip flow could never actually work because the
14+
script's dirty-tree guard refused to run mid-merge. Added merge-state
15+
detection (`$GIT_DIR/MERGE_HEAD` presence); when mid-merge the strip
16+
uses `git rm -rf --ignore-unmatch` so modify/delete conflicts get
17+
deleted AND staged as resolved in one step. The "absolute refusal to
18+
run on develop" guard is preserved regardless of merge state.
19+
- **`scripts/strip_for_main.sh` + `.githooks/pre-commit`** — added
20+
`.vscode/` (editor settings, often carry per-user Snyk / IDE prefs)
21+
to both strip lists, and `adr/` to the pre-commit FORBIDDEN list
22+
(it was already in the strip script). The two defense layers now
23+
agree. A note in pre-commit tells future editors to keep both lists
24+
in sync.
25+
1026
### Added — CI/build/gates (M-sec v1)
1127

1228
- **`.gosec.yml`** — explicit gosec configuration with documented rule

scripts/strip_for_main.sh

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,41 @@ set -euo pipefail
1414
#
1515
# --dry-run Print what would be removed without actually removing anything
1616
#
17+
# What this script protects:
18+
# develop is private and messy; main is the public branch that ships
19+
# outside the door. This script is the automated safety net that keeps
20+
# private material off main. It never modifies develop — it only runs
21+
# on main (or a detached HEAD for testing) to ensure the main tree is
22+
# stripped before a merge commit lands.
23+
#
1724
# Safe merge flow (develop → main):
1825
# 1. Commit/push all work on develop
1926
# 2. git checkout main
20-
# 3. git merge develop --no-commit
21-
# 4. ./scripts/strip_for_main.sh
22-
# 5. git add -A
27+
# 3. git merge develop --no-commit (may conflict on modify/delete)
28+
# 4. ./scripts/strip_for_main.sh (resolves strip-target conflicts
29+
# by deleting + staging together;
30+
# see "Mid-merge mode" below)
31+
# 5. git add -A (stage any remaining clean merges)
2332
# 6. git commit -m "merge: develop → main (stripped dev files)"
2433
#
2534
# Safety checks:
26-
# - Refuses to run on the develop branch (must be on main or detached)
27-
# - Refuses to run with uncommitted changes unless --dry-run
35+
# - ABSOLUTE: refuses to run on the develop branch. No exceptions.
36+
# develop is the source of truth for dev artifacts; stripping there
37+
# would destroy work. See guard below.
38+
# - Outside a merge: refuses to run with uncommitted changes. Mixing
39+
# strips with unrelated edits in one commit would destroy the audit
40+
# trail of what-got-stripped-when.
41+
# - Mid-merge (.git/MERGE_HEAD exists): the dirty-tree check is skipped
42+
# because a --no-commit merge always leaves a dirty tree. The
43+
# "refuses on develop" check is still enforced — we can never be
44+
# mid-merge ON develop (develop is the source, not the target).
45+
#
46+
# Mid-merge mode:
47+
# When .git/MERGE_HEAD exists, strip_path uses `git rm -rf` so modify/
48+
# delete conflicts (a file deleted on main but modified on develop) are
49+
# resolved by keeping the deletion AND staging the resolution in one
50+
# step. Without this, the script would delete the file but git would
51+
# still see it as conflicted, blocking the commit.
2852
#
2953
# What it removes:
3054
# - Internal tracking: MEMORY.md, MEMORY_ARCHIVE.md, FLOW.md, TECH-DEBT.md,
@@ -55,40 +79,89 @@ if [[ "${1:-}" == "--dry-run" ]]; then
5579
else
5680
echo "=== strip_for_main.sh ==="
5781
fi
82+
83+
# Detect mid-merge state. MERGE_HEAD exists only between `git merge --no-commit`
84+
# (or a conflict pause) and the eventual merge commit. When this is set we
85+
# know the dirty working tree is expected from the merge, not from random edits.
86+
GIT_DIR=$(git rev-parse --git-dir 2>/dev/null || echo ".git")
87+
IN_MERGE=0
88+
if [[ -f "$GIT_DIR/MERGE_HEAD" ]]; then
89+
IN_MERGE=1
90+
echo "(mid-merge mode: MERGE_HEAD present)"
91+
fi
5892
echo ""
5993

60-
# Safety: refuse real runs on develop (dry-run is fine — it doesn't touch files)
94+
# Safety: ABSOLUTE refusal to run on develop. This guard exists no matter
95+
# what mode or flag is passed, and no matter whether a merge is in progress.
96+
# develop is the source of dev artifacts; stripping there would destroy
97+
# work. A mid-merge on develop is also impossible in the documented flow
98+
# (merges go FROM develop INTO main, not the other way), so reaching this
99+
# condition means something has gone badly wrong and we must abort.
61100
BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
62101
if [[ $DRY_RUN -eq 0 && "$BRANCH" == "develop" ]]; then
63102
echo "ERROR: refusing to run on 'develop' branch."
64-
echo "This script strips dev files — run it only on 'main' (or a detached HEAD for a"
65-
echo "temporary test). On develop you'd lose your tracked dev files."
103+
echo "This script strips dev files — run it only on 'main' (or a detached HEAD"
104+
echo "for a temporary test). On develop you'd lose your tracked dev files."
66105
echo ""
67106
echo "Safe flow: checkout main, merge develop --no-commit, then run this script."
68107
echo "To preview from develop: ./scripts/strip_for_main.sh --dry-run"
69108
exit 1
70109
fi
71110

72-
# Safety: refuse real runs with uncommitted changes
73-
if [[ $DRY_RUN -eq 0 ]] && ! git diff --quiet HEAD -- 2>/dev/null; then
74-
echo "ERROR: uncommitted changes detected. Commit or stash first."
75-
echo "(Running the strip with dirty state would mix strips with other edits.)"
111+
# Safety: refuse dirty tree UNLESS we're in the middle of a merge. A merge
112+
# always leaves a dirty tree (staged adds, conflicted modifies), and the
113+
# documented flow runs the strip right after `git merge --no-commit`. Outside
114+
# a merge, a dirty tree means unrelated edits — stripping would mix them in.
115+
if [[ $DRY_RUN -eq 0 && $IN_MERGE -eq 0 ]] && ! git diff --quiet HEAD -- 2>/dev/null; then
116+
echo "ERROR: uncommitted changes detected (and we are NOT mid-merge)."
117+
echo "Commit or stash first. Running the strip with a dirty state would"
118+
echo "mix strips with unrelated edits in one commit, destroying the audit"
119+
echo "trail of what-got-stripped-when."
76120
echo ""
77121
echo "Override: pass --dry-run to preview without stripping."
78122
exit 1
79123
fi
80124

81125
STRIPPED=0
82126

127+
# strip_path removes $1 from the working tree. Behavior:
128+
# - dry run : only prints what would be removed
129+
# - mid-merge : uses `git rm -rf` so modify/delete conflicts are
130+
# resolved (deleted AND staged as resolved) in one
131+
# step. This is what makes the merge flow work.
132+
# - normal run : uses `rm -rf` (classic behavior — works on tracked
133+
# and untracked paths alike; any resulting deletions
134+
# get staged by the caller's `git add -A`).
83135
strip_path() {
84136
local path="$1"
85-
if [[ -e "$path" ]]; then
86-
if [[ $DRY_RUN -eq 1 ]]; then
137+
if [[ ! -e "$path" ]] && [[ $IN_MERGE -eq 0 ]]; then
138+
return 0
139+
fi
140+
if [[ $DRY_RUN -eq 1 ]]; then
141+
if [[ -e "$path" ]]; then
87142
echo " would remove: $path"
88-
else
89-
rm -rf "$path"
90-
echo " removed: $path"
143+
STRIPPED=$((STRIPPED + 1))
144+
fi
145+
return 0
146+
fi
147+
if [[ $IN_MERGE -eq 1 ]]; then
148+
# --ignore-unmatch: quiet no-op if path doesn't exist (covers the case
149+
# where the path was already deleted on main and develop also deleted
150+
# it, leaving nothing to strip).
151+
if git rm -rf --ignore-unmatch "$path" >/dev/null 2>&1; then
152+
if [[ -e "$path" ]]; then
153+
# git rm failed to touch it (e.g. untracked in merge state) — fall
154+
# back to plain rm and stage later via git add -A.
155+
rm -rf "$path"
156+
echo " removed (untracked): $path"
157+
else
158+
echo " removed (git rm): $path"
159+
fi
160+
STRIPPED=$((STRIPPED + 1))
91161
fi
162+
else
163+
rm -rf "$path"
164+
echo " removed: $path"
92165
STRIPPED=$((STRIPPED + 1))
93166
fi
94167
}
@@ -110,6 +183,7 @@ strip_path ".claude"
110183
strip_path ".agents"
111184
strip_path "audit"
112185
strip_path "adr"
186+
strip_path ".vscode" # editor-specific settings (Snyk IDE prefs, etc.)
113187

114188
# Internal reports and utilities
115189
strip_path "generate_pdf.py"

0 commit comments

Comments
 (0)