mirror of
https://github.com/Donchitos/Claude-Code-Game-Studios.git
synced 2026-10-16 01:37:05 +00:00
Release 1.1.2 — the default path works end to end
Fix release for 1.1.1: the minimal path end to end, engine commands that run on Godot, Unity and Unreal, hook warnings you can see, and verdicts that say when they could not check. Details in CHANGELOG.md; upgrade steps in UPGRADING.md (v1.1.1 → v1.1.2). Fixes #67 Fixes #131 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WuJn2FdynEybfL5KC697nJ
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7ed2c3e9c4
commit
b21fa0f7f2
+117
-15
@@ -28,47 +28,149 @@ fi
|
||||
[ -n "$CCGS_ROOT" ] && cd "$CCGS_ROOT" 2>/dev/null || true
|
||||
|
||||
# Claude Code PreToolUse hook: Validates git push commands
|
||||
# Warns on pushes to protected branches
|
||||
# Exit 0 = allow, Exit 2 = block
|
||||
# Warns on pushes to protected branches -- as hook JSON on stdout, the only
|
||||
# channel the user or Claude sees on exit 0.
|
||||
# Exit 0 = allow (the normal permission flow still applies), Exit 2 = block
|
||||
#
|
||||
# Input schema (PreToolUse for Bash):
|
||||
# { "tool_name": "Bash", "tool_input": { "command": "git push origin main" } }
|
||||
|
||||
INPUT=$(cat)
|
||||
|
||||
# Parse command -- use jq if available, fall back to grep
|
||||
# Most Bash and PowerShell calls do not mention git at all. Leave before any
|
||||
# parsing: this hook runs on every one of them, and the parse is most of its
|
||||
# cost (~350 ms a call measured; the early exit is the shell start-up alone).
|
||||
# Tested only after the "tool_input" key: matching the whole payload also
|
||||
# tests cwd and transcript_path, so a project path containing "GitHub" or
|
||||
# "digital" never took this exit and paid the full parse on every call.
|
||||
case "$INPUT" in *'"tool_input"'*[Gg][Ii][Tt]*) ;; *) exit 0 ;; esac
|
||||
|
||||
# Parse command -- use jq if available, fall back to grep. The fallback's string
|
||||
# body is (\\.|[^"\\])* so an escaped quote does not end it early.
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
|
||||
else
|
||||
COMMAND=$(echo "$INPUT" | grep -oE '"command"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/"command"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
||||
# LC_ALL=C: in a UTF-8 locale an emoji in the command makes this grep match
|
||||
# nothing, and an empty COMMAND skips the check below.
|
||||
COMMAND=$(echo "$INPUT" | LC_ALL=C grep -oE '"command"[[:space:]]*:[[:space:]]*"(\\.|[^"\\])*"' | head -1 | LC_ALL=C sed 's/"command"[[:space:]]*:[[:space:]]*"//;s/"$//')
|
||||
# Decode the JSON escapes once, here, and never jq's output -- \\ first,
|
||||
# through a placeholder. validate-commit.sh explains each escape.
|
||||
COMMAND=$(printf '%s\n' "$COMMAND" \
|
||||
| LC_ALL=C awk '{ gsub(/\\\\/, "\002"); gsub(/\\r/, ""); gsub(/\\t/, "\t"); gsub(/\\n/, "\n"); gsub(/\\"/, "\""); print }' \
|
||||
| tr '\002' '\\')
|
||||
fi
|
||||
|
||||
# Print the first segment of $2 that runs `git $1`, anywhere in the command.
|
||||
# An ^git anchor missed every chained push (`git add . && git push`).
|
||||
# Kept identical in validate-commit.sh, which explains the splitting.
|
||||
git_segment() {
|
||||
printf '%s\n' "$2" \
|
||||
| awk '{ while ($0 ~ /\\$/) { line = $0; sub(/\\$/, "", line); if ((getline nxt) > 0) { $0 = line nxt } else { $0 = line; break } } print }' \
|
||||
| awk '
|
||||
pd != "" { t = $0; sub(/\r$/, "", t); if (t ~ /^'"'"'@/) pd = ""; next }
|
||||
d != "" { t = $0; sub(/\r$/, "", t); if (tabs) sub(/^\t+/, "", t); if (t == d) d = ""; next }
|
||||
{ print }
|
||||
match($0, /<<-?[ \t]*("[^"]*"|'"'"'[^'"'"']*'"'"'|\\?[A-Za-z_][^ \t\r;&|()<>"'"'"'\\]*)/) && substr($0, RSTART - 1, 1) != "<" \
|
||||
&& substr($0, 1, RSTART - 1) !~ /\$\(\([^)]*$/ \
|
||||
&& $0 !~ /(^|[^A-Za-z0-9_.-])(bash|sh|zsh|dash|ksh|fish|pwsh|powershell|python[0-9.]*|py|node|ruby|perl|php|eval|source)([ \t]|$)/ {
|
||||
t = substr($0, RSTART, RLENGTH); q = ""
|
||||
for (i = 1; i < RSTART; i++) {
|
||||
c = substr($0, i, 1)
|
||||
if (q == "") { if (c == "\"" || c == "'"'"'") q = c }
|
||||
else if (c == q || (q == "\"" && c == "$" && substr($0, i + 1, 1) == "(")) q = ""
|
||||
}
|
||||
if (q == "") { d = t; tabs = (d ~ /^<<-/); sub(/^<<-?[ \t]*/, "", d); gsub(/[\\"'"'"']/, "", d) }
|
||||
}
|
||||
# A PowerShell single-quote here-string body (opened by @ then a
|
||||
# single quote, closed by a single quote then @) is literal text,
|
||||
# the same as a bash here-doc -- UNLESS it is the double-quote form,
|
||||
# which PowerShell itself expands $(...) inside, so that form is
|
||||
# left unmarked and flows through unchanged.
|
||||
$0 ~ /@'"'"'[ \t]*\r?$/ { pd = "x" }' \
|
||||
| awk '{
|
||||
s = $0; out = ""
|
||||
gsub(/"([^"]*[\/\\])?[Gg][Ii][Tt](\.exe)?"/, "git", s); gsub(/'"'"'([^'"'"']*[\/\\])?[Gg][Ii][Tt](\.exe)?'"'"'/, "git", s)
|
||||
gsub(/\$\([^ \t;&|()`"'"'"']*\)|`[^ \t;&|()`"'"'"']*`/, "$_", s)
|
||||
while (match(s, /(-[Cc]|--(git-dir|work-tree|namespace|super-prefix|config-env|attr-source))[ \t]+("[^"]*"|'"'"'[^'"'"']*'"'"')|=("[^"]*"|'"'"'[^'"'"']*'"'"')/)) {
|
||||
p = out substr(s, 1, RSTART - 1); a = substr(s, RSTART, RLENGTH); s = substr(s, RSTART + RLENGTH)
|
||||
if (a ~ /^=/) {
|
||||
f = "="; ok = p ~ /(^|[ \t;&|(){`"'"'"'])([^ \t;&|(){`"'"'"']*[\/\\])?[Gg][Ii][Tt](\.exe)?([ \t]+[^ \t;&|(){`"'"'"']+)+$/
|
||||
} else {
|
||||
f = a; sub(/[ \t]+["'"'"'].*$/, " ", f)
|
||||
ok = p ~ /(^|[ \t;&|(){`"'"'"'])([^ \t;&|(){`"'"'"']*[\/\\])?[Gg][Ii][Tt](\.exe)?([ \t]+[^ \t;&|(){`"'"'"']+)*[ \t]+$/
|
||||
}
|
||||
if (!ok) { out = p substr(a, 1, 1); s = substr(a, 2) s; continue }
|
||||
v = a; sub(/^[^"'"'"']*["'"'"']/, "", v); v = substr(v, 1, length(v) - 1); gsub(/[ \t;&|()`]/, "\001", v)
|
||||
out = p f v
|
||||
}
|
||||
s = out s
|
||||
# A quoted WORD with nothing inside it a shell would split on (a
|
||||
# quoted branch name, a quoted refspec) is unquoted the same way
|
||||
# rather than read as a delimiter -- a bare `"` or `'"'"'` used to
|
||||
# split the line here, dropping the quoted text (and everything
|
||||
# after it on that line) from what the classifier below ever saw.
|
||||
# A quoted string that DOES contain such characters is left alone:
|
||||
# it may be a whole command (`bash -c "git commit"`) meant to fall
|
||||
# on its own line for re-inspection, or a commit message, and either
|
||||
# reading is unchanged from before.
|
||||
out = ""
|
||||
while (match(s, /"[^" \t;&|()`]*"|'"'"'[^'"'"' \t;&|()`]*'"'"'/)) {
|
||||
p = out substr(s, 1, RSTART - 1); a = substr(s, RSTART, RLENGTH); s = substr(s, RSTART + RLENGTH)
|
||||
out = p substr(a, 2, length(a) - 2)
|
||||
}
|
||||
s = out s
|
||||
gsub(/&&|\|\|/, "\n", s); gsub(/[;&|()`"'"'"']/, "\n", s); print s
|
||||
}' \
|
||||
| grep -E "^[[:space:]]*((if|then|else|elif|do|while|until|time|exec|command|nohup|!|[{])[[:space:]]+|(env|sudo|doas|xargs|nice|timeout|stdbuf|winpty|command)([[:space:]]+-[A-Za-z0-9-]+(=[^[:space:]]*)?([[:space:]]+[^-[:space:]][^[:space:]]*)?)*[[:space:]]+([0-9][0-9.]*[smhd]?[[:space:]]+)?|[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*([^[:space:]]*[/\\])?[Gg][Ii][Tt](\.exe)?([[:space:]]+(-[Cc][[:space:]]+[^[:space:]]+|--(git-dir|work-tree|namespace|super-prefix|config-env|attr-source)[[:space:]]+[^[:space:]]+|-[A-Za-z]|--[A-Za-z-]+(=[^[:space:]]+)?))*[[:space:]]+$1([[:space:]]|\$)"
|
||||
}
|
||||
|
||||
# Only process git push commands
|
||||
if ! echo "$COMMAND" | grep -qE '^git[[:space:]]+push'; then
|
||||
exit 0
|
||||
fi
|
||||
PUSH_SEG=$(git_segment push "$COMMAND") || exit 0
|
||||
|
||||
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
|
||||
MATCHED_BRANCH=""
|
||||
|
||||
# Check if pushing to a protected branch
|
||||
for branch in develop main master; do
|
||||
# Check if pushing to a protected branch: main, master, or any release/*
|
||||
# branch (the trunk model's only other protected line -- no GitFlow develop).
|
||||
for branch in main master; do
|
||||
if [ "$CURRENT_BRANCH" = "$branch" ]; then
|
||||
MATCHED_BRANCH="$branch"
|
||||
break
|
||||
fi
|
||||
# Also check if pushing to a protected branch explicitly (quote branch name for safety)
|
||||
if echo "$COMMAND" | grep -qE "[[:space:]]${branch}([[:space:]]|$)"; then
|
||||
# Also check if pushing to a protected branch explicitly: as its own word,
|
||||
# or as a refspec's destination (HEAD:main, feature:main, +main,
|
||||
# refs/heads/main). `main:feature` pushes TO feature and stays quiet.
|
||||
if echo "$PUSH_SEG" | grep -qE "([[:space:]+:]|refs/heads/)${branch}([[:space:]]|$)"; then
|
||||
MATCHED_BRANCH="$branch"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
# release/* is a name pattern, not a fixed branch, so it needs its own checks:
|
||||
# pushing FROM a release/* branch (a bare `git push`), or explicitly TO one.
|
||||
if [ -z "$MATCHED_BRANCH" ]; then
|
||||
case "$CURRENT_BRANCH" in
|
||||
release/*) MATCHED_BRANCH="$CURRENT_BRANCH" ;;
|
||||
esac
|
||||
fi
|
||||
if [ -z "$MATCHED_BRANCH" ] && echo "$PUSH_SEG" | grep -qE "([[:space:]+:]|refs/heads/)release/[^[:space:]:]+([[:space:]]|$)"; then
|
||||
MATCHED_BRANCH=$(echo "$PUSH_SEG" | grep -oE "release/[^[:space:]:]+" | head -1)
|
||||
fi
|
||||
|
||||
if [ -n "$MATCHED_BRANCH" ]; then
|
||||
echo "Push to protected branch '$MATCHED_BRANCH' detected." >&2
|
||||
echo "Reminder: Ensure build passes, unit tests pass, and no S1/S2 bugs exist." >&2
|
||||
# Allow the push but warn -- uncomment below to block instead:
|
||||
# echo "BLOCKED: Run tests before pushing to $CURRENT_BRANCH" >&2
|
||||
# Allow the push, but warn where it is seen. stderr on exit 0 goes to the
|
||||
# debug log only, so this warning reached neither the user nor Claude.
|
||||
# systemMessage is shown to the user; additionalContext reaches Claude next
|
||||
# to the tool result. No permissionDecision: "allow" would skip the user's
|
||||
# permission prompt. MATCHED_BRANCH is no longer a fixed name -- for
|
||||
# release/* it is the current branch or text taken from the command, and
|
||||
# git allows a literal `"` or `\` in a branch name, so it must be escaped
|
||||
# before it goes into the JSON string below.
|
||||
SAFE_BRANCH=$(printf '%s' "$MATCHED_BRANCH" | sed 's/\\/\\\\/g; s/"/\\"/g')
|
||||
MSG="Push to protected branch '$SAFE_BRANCH' detected. Reminder: Ensure build passes, unit tests pass, and no open bugs the release gate blocks (S1; S1-S3 at workflow: full)."
|
||||
printf '{"systemMessage":"%s","hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":"%s"}}\n' "$MSG" "$MSG"
|
||||
# To block instead, replace the printf above with:
|
||||
# echo "BLOCKED: Run tests before pushing to $MATCHED_BRANCH" >&2
|
||||
# exit 2
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user