1919# scripts/pre-pr-check.sh --text "any arbitrary text blob" # review body, comment, commit msg
2020# scripts/pre-pr-check.sh --stdin < body.md
2121# scripts/pre-pr-check.sh --scan-sources # same scan CI does for firstdata/sources
22+ # scripts/pre-pr-check.sh --tags-lint # scan firstdata/sources tags for ASCII
23+ # # uppercase or duplicate (case-insensitive)
24+ # # violations — complements CI / style guide
2225#
23- # Exit code: 0 = clean, 1 = confidential term found, 2 = usage error.
26+ # Exit code: 0 = clean, 1 = confidential term or tags violation found, 2 = usage error.
2427#
2528# Keep the BANNED_TERMS list in sync with .github/workflows/secrecy-check.yml.
2629set -euo pipefail
@@ -43,9 +46,10 @@ BRANCH=""
4346TEXT=" "
4447SCAN_SOURCES=0
4548READ_STDIN=0
49+ TAGS_LINT=0
4650
4751usage () {
48- sed -n ' 2,25p ' " $0 "
52+ sed -n ' 2,28p ' " $0 "
4953 exit 2
5054}
5155
@@ -56,8 +60,10 @@ while [[ $# -gt 0 ]]; do
5660 --title) TITLE=" $2 " ; shift 2 ;;
5761 --branch) BRANCH=" $2 " ; shift 2 ;;
5862 --text) TEXT=" $2 " ; shift 2 ;;
63+ --review-body) TEXT=" $2 " ; shift 2 ;; # alias of --text (reviewer-side)
5964 --stdin) READ_STDIN=1; shift ;;
6065 --scan-sources) SCAN_SOURCES=1; shift ;;
66+ --tags-lint) TAGS_LINT=1; shift ;;
6167 -h|--help) usage ;;
6268 * ) echo " Unknown arg: $1 " >&2 ; usage ;;
6369 esac
@@ -122,4 +128,63 @@ if [[ "$found" -eq 1 ]]; then
122128 exit 1
123129fi
124130
131+ if [[ " $TAGS_LINT " -eq 1 ]]; then
132+ repo_root=" $( git rev-parse --show-toplevel 2> /dev/null || pwd) "
133+ src_dir=" $repo_root /firstdata/sources"
134+ if [[ ! -d " $src_dir " ]]; then
135+ echo " ::error::--tags-lint: sources dir not found at $src_dir " >&2
136+ exit 1
137+ fi
138+ python3 - " $src_dir " << 'PY '
139+ import json, pathlib, re, sys
140+ src = pathlib.Path(sys.argv[1])
141+ ascii_re = re.compile(r'^[\x20-\x7E]+$')
142+ violations_upper = [] # (file, tag)
143+ violations_dup = [] # (file, tag_lower, originals)
144+ parse_errors = []
145+ for f in sorted(src.rglob('*.json')):
146+ try:
147+ d = json.load(open(f, encoding='utf-8'))
148+ except Exception as e:
149+ parse_errors.append((str(f), str(e)))
150+ continue
151+ tags = d.get('tags') or []
152+ if not isinstance(tags, list):
153+ continue
154+ # ASCII uppercase check
155+ for t in tags:
156+ if isinstance(t, str) and ascii_re.match(t) and re.search(r'[A-Z]', t):
157+ violations_upper.append((str(f), t))
158+ # Duplicate (case-insensitive) check
159+ by_lower = {}
160+ for t in tags:
161+ if not isinstance(t, str): continue
162+ k = t.lower()
163+ by_lower.setdefault(k, []).append(t)
164+ for k, originals in by_lower.items():
165+ if len(originals) > 1:
166+ violations_dup.append((str(f), k, originals))
167+
168+ exit_code = 0
169+ if parse_errors:
170+ print('🔴 JSON parse errors:', file=sys.stderr)
171+ for f, e in parse_errors:
172+ print(f' {f}: {e}', file=sys.stderr)
173+ exit_code = 1
174+ if violations_upper:
175+ print(f'🔴 tags-lint: {len(violations_upper)} ASCII-uppercase tag(s) found:', file=sys.stderr)
176+ for f, t in violations_upper:
177+ print(f' {f}: {t!r}', file=sys.stderr)
178+ exit_code = 1
179+ if violations_dup:
180+ print(f'🔴 tags-lint: {len(violations_dup)} duplicate tag group(s) (case-insensitive):', file=sys.stderr)
181+ for f, k, originals in violations_dup:
182+ print(f' {f}: {originals} → {k!r}', file=sys.stderr)
183+ exit_code = 1
184+ if exit_code == 0:
185+ print('✅ tags-lint: all tags compliant (ASCII lowercase + no case-insensitive duplicates).')
186+ sys.exit(exit_code)
187+ PY
188+ fi
189+
125190echo " ✅ Pre-PR secrecy check passed."
0 commit comments