ci(strings): gate the Compose catalog escaping check

Wires compose_escaping_check.py into the two layers that already police the
identical orphan-strings desync, mirroring pre-push-orphan-strings.sh:

  - the fast `lint` job in build.yml, next to the orphan check
  - a PreToolUse hook, so a push or PR from an agent session is gated too

CI is the layer that matters here. As amethyst/src/main/res/CLAUDE.md records for
the orphan desync, these arrive through bot-authored PRs -- the Crowdin sync
reintroduced 2,068 escaped apostrophes across 40 locales twice in two days, with
no local session anywhere in the path.

Verified end to end: a payload with no push exits 0 without spawning python; a
push against a clean tree exits 0; a push with values-tr-rTR restored to its
pre-repair state exits 1 and names the file with a per-escape breakdown.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1CzYQvWyHfipSW7x3j4Yo
This commit is contained in:
Vitor Pamplona
2026-09-03 14:19:04 -04:00
co-authored by Claude Opus 5
parent f9baab0e92
commit 9585e66e60
3 changed files with 42 additions and 0 deletions
+34
View File
@@ -0,0 +1,34 @@
#!/bin/bash
# PreToolUse gate: the Compose resource catalog must not carry Android escaping.
#
# Fires on `git push` (Bash tool) and on the create_pull_request MCP tool.
# Delegates to compose_escaping_check.py, which scans
# `*/src/*/composeResources/values*/strings.xml` for \' \" \? \@ and `tools:`
# attributes. Compose resolves only \uXXXX, \n and \t, so anything else carried
# verbatim out of an Android res tree renders literally -- the login screen once
# read `Don\'t have a Nostr account?`.
#
# Why a dedicated hook: this is reintroduced by every Crowdin sync, because
# Crowdin holds the Android-escaped source. It came back twice in two days, 2,068
# escaped apostrophes across 40 locales each time. Nothing in the Gradle build
# fails on it -- the strings simply ship wrong -- so there is no slow gate this
# stands in for; it is the only gate.
#
# Run the scan by hand any time with: .claude/hooks/compose_escaping_check.py
set -uo pipefail
hook_dir="$(cd "$(dirname "$0")" && pwd)"
cd "${CLAUDE_PROJECT_DIR:-.}" || exit 0
payload="$(cat)"
# Same cheap pre-filter as the orphan-strings gate: only a payload mentioning a
# push or the PR tool can possibly match, and this runs on every Bash call.
case "$payload" in
*push*|*pull_request*) ;;
*) exit 0 ;;
esac
printf '%s' "$payload" | python3 "$hook_dir/lib/git_push_gate.py" || exit 0
exec python3 "$hook_dir/compose_escaping_check.py"
+5
View File
@@ -13,6 +13,11 @@
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-push-orphan-strings.sh",
"timeout": 30
},
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-push-compose-escaping.sh",
"timeout": 30
}
]
}
+3
View File
@@ -24,6 +24,9 @@ jobs:
- name: Orphaned translations (no locale string may outlive its default key)
run: .claude/hooks/orphan_strings_check.py
- name: Compose catalog escaping (Android escapes render literally there)
run: .claude/hooks/compose_escaping_check.py
- name: Set up JDK 21
uses: actions/setup-java@v6.0.0
with: