forked from bchanot/claude
job4: SPEC-12 deploy-commit exit taxonomy
lib/deploy-commit.sh: a rejected `git commit` (pre-commit hook, protected branch, signing failure) now exits 6 (loud stderr, distinct from rc 1's "nothing to do") instead of sharing rc 1 with the no-op cases. Header comment documents the full 0/1/2/3/4/5/6 taxonomy. Closes J4-22 (UNTESTABLE): at client repos, a failed deploy-state commit was indistinguishable BY EXIT CODE from "nothing to do" (rc 1 was shared 3 ways); exit-code-only callers couldn't disambiguate (stderr-parsing callers already could). Caller census (per report's explicit gate): skills/deploy/SKILL.md documents and parses this exit-code contract in TWO places (bootstrap commit + incident-recovery commit). Flagged to the user before committing; confirmed GO to add rc 6 there too (additive — no existing code's meaning changes) so the documented contract stays accurate for live deploy runs. New T10 in lib/tests/deploy-commit.test.sh (+3 assertions, 13→16): rejecting pre-commit hook sandbox — asserts rc 6, empty stdout (no stale hash), HEAD unmoved. GREEN: full `make test` exit 0 (deploy-commit 16/16 incl. T10). shellcheck clean, bash -n clean.
This commit is contained in:
+14
-1
@@ -1,6 +1,18 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# deploy-commit.sh — surgical commit for the .claude/deploy/ runbook family.
|
# deploy-commit.sh — surgical commit for the .claude/deploy/ runbook family.
|
||||||
# Allowlist scope = .claude/deploy/ ONLY (inverse of doc-commit's .claude exclusion).
|
# Allowlist scope = .claude/deploy/ ONLY (inverse of doc-commit's .claude exclusion).
|
||||||
|
#
|
||||||
|
# Exit code taxonomy:
|
||||||
|
# 0 committed (short-hash on stdout), or `pending`: something changed
|
||||||
|
# 1 no-op — nothing staged/changed (`pending`: clean) — NOT a failure
|
||||||
|
# 2 usage error, or not a git repo
|
||||||
|
# 3 unsafe git state (detached HEAD / merge / rebase in progress)
|
||||||
|
# 4 a passed path is outside the .claude/deploy/ allowlist
|
||||||
|
# 5 a passed path is git-ignored and would not persist
|
||||||
|
# 6 `git commit` itself was REJECTED (pre-commit hook, protected branch,
|
||||||
|
# signing failure, …) — distinct from rc 1 (no-op): here something WAS
|
||||||
|
# staged and git refused it. Client repos may parse this by exit code,
|
||||||
|
# not just stderr, so it can't share rc 1's "nothing to do" (J4-22).
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
_in_git_repo() { git rev-parse --git-dir >/dev/null 2>&1; }
|
_in_git_repo() { git rev-parse --git-dir >/dev/null 2>&1; }
|
||||||
@@ -67,7 +79,8 @@ case "$cmd" in
|
|||||||
if git diff --cached --quiet -- "${changed[@]}"; then
|
if git diff --cached --quiet -- "${changed[@]}"; then
|
||||||
echo "deploy-commit: nothing staged — no-op" >&2; exit 1
|
echo "deploy-commit: nothing staged — no-op" >&2; exit 1
|
||||||
fi
|
fi
|
||||||
git commit -q -m "$msg" -- "${changed[@]}" || { echo "deploy-commit: git commit failed" >&2; exit 1; }
|
git commit -q -m "$msg" -- "${changed[@]}" \
|
||||||
|
|| { echo "deploy-commit: COMMIT REJECTED — git commit exited non-zero (pre-commit hook? protected branch? signing?)." >&2; exit 6; }
|
||||||
git rev-parse --short HEAD ;;
|
git rev-parse --short HEAD ;;
|
||||||
*) echo "usage: deploy-commit.sh pending <file>... | commit \"<msg>\" <file>..." >&2; exit 2 ;;
|
*) echo "usage: deploy-commit.sh pending <file>... | commit \"<msg>\" <file>..." >&2; exit 2 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -50,4 +50,13 @@ printf 'run\n' >"$d/.claude/deploy/PROCEDURE.md"
|
|||||||
( cd "$d" && bash "$H" commit "docs(deploy): t" .claude/deploy/PROCEDURE.md ) >/dev/null 2>&1
|
( cd "$d" && bash "$H" commit "docs(deploy): t" .claude/deploy/PROCEDURE.md ) >/dev/null 2>&1
|
||||||
check T9-ignored-rc "$?" 5
|
check T9-ignored-rc "$?" 5
|
||||||
|
|
||||||
|
d=$(mkrepo); printf '#!/bin/sh\nexit 1\n' >"$d/.git/hooks/pre-commit"; chmod +x "$d/.git/hooks/pre-commit"
|
||||||
|
BEFORE=$(git -C "$d" rev-parse --short HEAD)
|
||||||
|
printf 'run\n' >"$d/.claude/deploy/PROCEDURE.md"
|
||||||
|
OUT=$( ( cd "$d" && bash "$H" commit "docs(deploy): t" .claude/deploy/PROCEDURE.md ) 2>/dev/null ); RC=$?
|
||||||
|
AFTER=$(git -C "$d" rev-parse --short HEAD)
|
||||||
|
check T10-rejected-rc "$RC" 6
|
||||||
|
check T10-rejected-no-hash "$([ -z "$OUT" ] && echo empty || echo "$OUT")" empty
|
||||||
|
check T10-rejected-head-unmoved "$BEFORE" "$AFTER"
|
||||||
|
|
||||||
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]
|
||||||
|
|||||||
@@ -250,6 +250,7 @@ Present the full draft `PROCEDURE.md`.
|
|||||||
Return codes: **0** committed · **1** no-op (investigate — both files should be new) ·
|
Return codes: **0** committed · **1** no-op (investigate — both files should be new) ·
|
||||||
**3** unsafe git state (STOP, tell user) · **4** out-of-scope path ·
|
**3** unsafe git state (STOP, tell user) · **4** out-of-scope path ·
|
||||||
**5** a passed path is git-ignored (won't persist) — STOP, fix the target's `.gitignore` ·
|
**5** a passed path is git-ignored (won't persist) — STOP, fix the target's `.gitignore` ·
|
||||||
|
**6** commit rejected — pre-commit hook/protected branch/signing (STOP, investigate) ·
|
||||||
**2** usage error OR not a git repo.
|
**2** usage error OR not a git repo.
|
||||||
|
|
||||||
**On rc=0: continue to STEP 1.** `STATE.json` absent → first deploy →
|
**On rc=0: continue to STEP 1.** `STATE.json` absent → first deploy →
|
||||||
@@ -358,8 +359,9 @@ Return codes: **0** committed (short-hash on stdout) · **1** nothing staged —
|
|||||||
wrote neither file · **3** unsafe git state (detached/merge/rebase — STOP, tell
|
wrote neither file · **3** unsafe git state (detached/merge/rebase — STOP, tell
|
||||||
the user) · **4** out-of-scope path (you passed a non-`.claude/deploy/` path — fix
|
the user) · **4** out-of-scope path (you passed a non-`.claude/deploy/` path — fix
|
||||||
the call) · **5** a passed path is git-ignored (won't persist) — STOP, fix the
|
the call) · **5** a passed path is git-ignored (won't persist) — STOP, fix the
|
||||||
target's `.gitignore` · **2** usage error OR not a git repo. The helper commits
|
target's `.gitignore` · **6** commit rejected — pre-commit hook/protected branch/
|
||||||
whatever subset actually changed;
|
signing (STOP, investigate) · **2** usage error OR not a git repo. The helper
|
||||||
|
commits whatever subset actually changed;
|
||||||
patch+incident coupling is **Claude-discipline, not helper-enforced**.
|
patch+incident coupling is **Claude-discipline, not helper-enforced**.
|
||||||
|
|
||||||
**This commit IS the resolution** — the commit that introduces `DEP-NNN` is its
|
**This commit IS the resolution** — the commit that introduces `DEP-NNN` is its
|
||||||
|
|||||||
Reference in New Issue
Block a user