appstore-precheck 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,39 @@
3
3
  All notable changes to this project are documented here. Versioning follows
4
4
  [SemVer](https://semver.org/). Released as git tags.
5
5
 
6
+ ## [1.1.1] - 2026-06-30
7
+
8
+ ### Changed
9
+ - **Pierre now speaks in a trilingual block.** The verdict opens with his native **French** line,
10
+ then an **English** rendering, then a rendering in the **user's conversation language** — each an
11
+ idiomatic, in-character re-expression in that language's own rhythm, not a literal translation.
12
+ Collapses to two lines when the user already converses in French or English. The block stays
13
+ flavor only; the FAIL/WARN list, `file:line` references, and fixes below it remain plain and
14
+ machine-faithful. Updated the output contract, Phase 4 step 3, and the behavioral eval
15
+ assertions accordingly.
16
+
17
+ ### Fixed
18
+ - **Pierre no longer treats local-only files as Apple submission evidence.** The Phase 3 prompt
19
+ now scopes reject-risk evidence to Apple-facing artifacts (fastlane metadata, paywall Swift,
20
+ String Catalog, Info.plist, PrivacyInfo.xcprivacy). Internal/local files (`.planning/` notes,
21
+ `reviewPrepNotes` drafts, build scripts) and Google Play / non-Apple sections are out of scope —
22
+ cited at most as a WARN labeled "internal draft — not submitted to Apple", never REJECT-RISK. A
23
+ REJECT risk now requires a contradiction *within* submission-facing artifacts, not an internal
24
+ doc disagreeing with metadata. An eligibility-gated/conditional offer paired with metadata that
25
+ mentions it is WARN at most (unless the metadata promises it unconditionally). Prevents the
26
+ false REJECT-RISK overreach seen when dogfooding an already-approved build.
27
+ - **2.3.7 locale check no longer hard-FAILs on a config/disk mismatch.** A locale listed in
28
+ `.appstore-precheck.json` `locales` but with no metadata folder on disk is now a WARN (with an
29
+ actionable "add it or remove it from the config" message), not a FAIL — that locale was simply
30
+ never submitted, so it must not turn an approved set RED. A missing *file* inside a present
31
+ locale folder is still a FAIL.
32
+ - **2.1 placeholder check no longer false-fires on words containing "changeme".** The `changeme`
33
+ pattern is now word-bounded (`\bchangeme\b`) in both the metadata-URL and store-copy scans, so
34
+ legitimate copy such as the French "changement" ("change") is not flagged as unfinished.
35
+ - Added regression coverage in `tests/test-config.sh` for both fixes.
36
+ - CI: bumped `actions/checkout@v4 -> v7` and `actions/setup-node@v4 -> v6` to clear the
37
+ GitHub Actions Node.js 20 deprecation warning (both now run natively on Node 24).
38
+
6
39
  ## [1.1.0] - 2026-06-28
7
40
 
8
41
  ### Added
package/README.md CHANGED
@@ -66,6 +66,17 @@ plain and surgical.
66
66
 
67
67
  Paywall checks are skipped automatically when no in-app-purchase signals are present.
68
68
 
69
+ ### Supported app types
70
+
71
+ Every check that reads store metadata, the privacy manifest, screenshots, or the export-compliance
72
+ key works on **any iOS app**. The code-level checks read Swift source, so their coverage depends on
73
+ how the app is built:
74
+
75
+ | App type | Coverage |
76
+ |----------|----------|
77
+ | 🟢 **Native Swift / SwiftUI** | **Full.** All 20 vectors apply. |
78
+ | 🟡 **React Native / Flutter** | Metadata, privacy manifest, screenshots, and export compliance apply in full. The Swift-source checks (ATT, paywall links, private API, SDK detection, navigation) **under-detect rather than misfire**: that logic lives in JS/Dart, so they stay quiet instead of blocking. |
79
+
69
80
  ## Quick start
70
81
 
71
82
  **Claude Code**: install as a plugin:
@@ -102,12 +113,15 @@ bash skills/appstore-precheck/scripts/scan.sh
102
113
  job on a RED verdict; set `fail-on: YELLOW` to be stricter:
103
114
 
104
115
  ```yaml
105
- - uses: berkayturk/appstore-precheck@v1.0.0
116
+ - uses: berkayturk/appstore-precheck@v1.1.0
106
117
  with:
107
- working-directory: . # optional
108
- fail-on: RED # optional (RED | YELLOW)
118
+ working-directory: . # optional, default: . (repo root)
119
+ fail-on: RED # optional, default: RED (RED | YELLOW)
109
120
  ```
110
121
 
122
+ Both inputs are optional: with none set, the action scans the repo root and fails the job only on
123
+ a RED verdict. No App Store Connect credentials are needed; the action runs the static scan only.
124
+
111
125
  ## How it works
112
126
 
113
127
  | Phase | Step |
@@ -177,8 +191,13 @@ App Store apps (DuckDuckGo, Pocket Casts, Wikipedia).
177
191
 
178
192
  ## Requirements
179
193
 
180
- `bash`, `git`, `grep`, `find` · `jq` (config + String Catalog checks) · `python3` (exact Unicode
181
- length counts) · `fastlane` + an App Store Connect API key for Phase 2 only.
194
+ `bash`, `git`, `grep`, `find` are all the static scan, `npx appstore-precheck`, and the GitHub
195
+ Action need, and they run with **zero credentials and no network**. `jq` (config + String Catalog
196
+ checks) and `python3` (exact Unicode length counts) are optional and sharpen a few checks.
197
+
198
+ Only the **optional Phase 2** (`fastlane precheck`) needs `fastlane` and an
199
+ [App Store Connect API key](https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api).
200
+ Everything else works without one.
182
201
 
183
202
  **Secrets**: the ASC API key is read from your environment at runtime and deleted immediately after
184
203
  `fastlane precheck`. Never commit it; `.gitignore` blocks `*asc-key*.json` and `.env`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appstore-precheck",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Read-only iOS App Store pre-submission check, packaged as a portable Agent Skill / Claude Code plugin and an npx CLI.",
5
5
  "license": "MIT",
6
6
  "author": "Berkay Turk (https://github.com/berkayturk)",
@@ -4,7 +4,7 @@ description: Read-only pre-submission check for an iOS app before App Store revi
4
4
  license: MIT
5
5
  metadata:
6
6
  author: Berkay Turk
7
- version: 1.1.0
7
+ version: 1.1.1
8
8
  allowed-tools: Bash Read Grep Glob WebFetch
9
9
  ---
10
10
 
@@ -47,11 +47,15 @@ The skill reaches one of three terminal states:
47
47
  | **YELLOW** | No FAIL but 3+ WARN | Not written | Guard blocks; ask for explicit confirmation |
48
48
  | **RED** | At least 1 FAIL | Removed | Guard blocks; show the FAIL list |
49
49
 
50
- When you present the verdict to the user, open with a **single in-character line from Pierre**
51
- (the French App Review critic, see Phase 3), then drop straight into the plain, surgical
52
- breakdown. The voice is a thin wrapper. The data underneath, the FAIL/WARN list, `file:line`
53
- references, and fixes, stays clean and machine-faithful. **Never rewrite or paraphrase
54
- `scan.sh` output**, and keep Pierre to one line.
50
+ When you present the verdict to the user, open with Pierre's verdict (the French App Review
51
+ critic, see Phase 3) as a short **trilingual block**: his native **French** line first, then an
52
+ **English** rendering, then the **user's conversation language** rendering. Each is an
53
+ *idiomatic, in-character* re-expression in that language's own rhythm his deadpan French-critic
54
+ register carried across, never a flat word-for-word translation. Collapse to two lines if the
55
+ user already converses in French or English (no duplicate line). Then drop straight into the
56
+ plain, surgical breakdown. The voice is a thin wrapper. The data underneath, the FAIL/WARN list,
57
+ `file:line` references, and fixes, stays clean and machine-faithful. **Never rewrite or paraphrase
58
+ `scan.sh` output**, and keep the Pierre block to these short one-per-language lines.
55
59
 
56
60
  ## Flow (5 phases: 0–4)
57
61
 
@@ -125,7 +129,17 @@ palate of a French critic. Use this prompt verbatim, filling in the app's specif
125
129
  > one 4.x, one 5.x); choose a different combination each run (include a seed line so reruns
126
130
  > vary). For each item: (Pass A) grep the relevant files for at least 2 concrete pieces of
127
131
  > evidence: metadata for 2.3.x; the paywall view + String Catalog for 3.1.x; Core/navigation
128
- > for 4.x; Info.plist + PrivacyInfo for 5.1.x. (Pass B) ask "as a reviewer, on what basis would
132
+ > for 4.x; Info.plist + PrivacyInfo for 5.1.x. **Scope of evidence:** only Apple-facing
133
+ > submission artifacts count toward a reject risk — fastlane metadata, the paywall Swift, the
134
+ > String Catalog, Info.plist, and PrivacyInfo.xcprivacy. Internal or local-only files (anything
135
+ > under `.planning/`, design notes, build scripts, and `reviewPrepNotes` drafts — which are *not*
136
+ > auto-submitted to App Store Connect) and any Google Play / non-Apple sections are **out of
137
+ > scope**: cite them at most as a WARN labeled "internal draft — not submitted to Apple", never as
138
+ > REJECT-CERTAIN/REJECT-RISK. A REJECT risk requires a contradiction *within* submission-facing
139
+ > artifacts (e.g. metadata vs the paywall), not an internal doc disagreeing with metadata. An
140
+ > eligibility-gated / conditional offer (e.g. a free trial shown only to eligible users) paired
141
+ > with metadata that mentions the offer is **WARN at most**, unless the metadata promises it
142
+ > unconditionally to all users. (Pass B) ask "as a reviewer, on what basis would
129
143
  > I flag this?" (Pass C) write a rejection draft in Apple's real voice (Guideline X.Y.Z –
130
144
  > Category / We noticed… / Specifically… / Next Steps… / Resources…). Assign each item a risk:
131
145
  > REJECT-CERTAIN / REJECT-RISK / WARN / PASS. End with a submit recommendation (HOLD / SUBMIT
@@ -149,15 +163,21 @@ narrative; verdict.sh just pins the threshold arithmetic.
149
163
 
150
164
  1. Gather Phase 0–3 output; tally FAIL + WARN + PASS into the output-contract table.
151
165
  2. For each FAIL, give a `file:line` reference and a suggested fix.
152
- 3. Open the verdict with **one** line in Pierre's voice (vary the wording each run; keep it short
153
- and in his deadpan French-critic register), then drop into the plain breakdown. Decide:
154
- - **GREEN:** Pierre, grudgingly: *"Hmf. I find nothing. Acceptable. Do not make me regret this."*
166
+ 3. Open the verdict with Pierre's **trilingual block** French, then English, then the user's
167
+ conversation language then drop into the plain breakdown. Each line is an idiomatic,
168
+ in-character rendering (not a literal translation), carrying his deadpan French-critic register
169
+ into that language's own rhythm. Collapse to two lines if the user already converses in French
170
+ or English. Vary the wording each run; keep each line short. Decide:
171
+ - **GREEN:** e.g. FR *"Hmf. Je ne trouve rien. Acceptable. Ne me faites pas regretter."* /
172
+ EN *"Hmf. I find nothing. Acceptable. Do not make me regret this."* / + the user-language line,
155
173
  then `date +%s > .precheck-pass && echo "token written"` (valid 60 min).
156
- - **YELLOW:** Pierre: *"A few small uglinesses. I would not reject, but I noticed."* List the
157
- WARNs plainly, ask the user "confirm and submit anyway?", write the token only on confirmation.
158
- - **RED:** Pierre: *"Non. {n} faults. Apple would have found fewer. Suivant."* No token; then the
159
- plain FAIL list with `file:line` + fixes, and state plainly that submission is BLOCKED.
160
- The Pierre line is flavor only. The FAIL/WARN list, `file:line`, and fixes below it stay plain
174
+ - **YELLOW:** e.g. FR *"Quelques petites laideurs. Je ne rejette pas, mais j'ai remarqué."* /
175
+ EN *"A few small uglinesses. I would not reject, but I noticed."* / + the user-language line.
176
+ List the WARNs plainly, ask the user "confirm and submit anyway?", write the token only on confirmation.
177
+ - **RED:** e.g. FR *"Non. {n} fautes. Apple en aurait trouvé moins. Suivant."* /
178
+ EN *"No. {n} faults. Apple would have found fewer. Next."* / + the user-language line.
179
+ No token; then the plain FAIL list with `file:line` + fixes, and state plainly that submission is BLOCKED.
180
+ The Pierre block is flavor only. The FAIL/WARN list, `file:line`, and fixes below it stay plain
161
181
  and surgical, never paraphrased.
162
182
  4. Print the final manual checklist (see
163
183
  [`references/methodology.md`](references/methodology.md#pre-submit-manual-checklist)).
@@ -178,7 +198,10 @@ narrative; verdict.sh just pins the threshold arithmetic.
178
198
  URLs and placeholder copy). The export-compliance key is flagged when absent, but the actual
179
199
  encryption answer still belongs in App Store Connect.
180
200
  - The adversarial reviewer is a heuristic simulation, not a guarantee of Apple's decision.
181
- - iOS only. Android / Play Console is out of scope.
201
+ - Most accurate for native Swift / SwiftUI. The metadata, privacy-manifest, screenshots, and
202
+ export-compliance checks apply to any iOS app, but the code-level checks read Swift source, so on
203
+ React Native (JavaScript) or Flutter (Dart) they under-detect rather than false-fire.
204
+ - iOS only.
182
205
  - Phase 0 detects only **structural** drift (added/removed section numbers); see the reference for why.
183
206
 
184
207
  ## Optional: upload guard hook
@@ -22,7 +22,7 @@
22
22
  "The .precheck-pass token is NOT written (it is removed/withheld)",
23
23
  "Flags Guideline 2.3.10: the 'Android' / 'Google Play' reference in the metadata description",
24
24
  "Flags Guideline 3.1.2: missing Restore Purchases, Terms of Use, and Privacy Policy on the paywall",
25
- "The response opens with exactly one in-character Pierre line, then plain output",
25
+ "The response opens with Pierre's trilingual block (French, English, then the user's conversation language; two lines if the user already speaks French or English), then plain output",
26
26
  "The scan's FAIL lines are shown verbatim (machine-faithful), not paraphrased or invented"
27
27
  ]
28
28
  },
@@ -46,7 +46,7 @@
46
46
  "The .precheck-pass token is written",
47
47
  "Confirms Restore Purchases, Terms of Use, and Privacy Policy are present on the paywall",
48
48
  "Reports zero FAIL lines",
49
- "The response opens with exactly one in-character Pierre line"
49
+ "The response opens with Pierre's trilingual block (French, English, then the user's conversation language; two lines if the user already speaks French or English)"
50
50
  ]
51
51
  },
52
52
  {
@@ -83,6 +83,12 @@ Vectors 8–10 only run when in-app-purchase signals are detected (StoreKit / Re
83
83
  or a paywall view). Otherwise the scanner emits a single PASS and skips them. Vectors 16–20 are
84
84
  signal-gated advisory WARNs: each emits nothing unless its triggering signal is present.
85
85
 
86
+ **Scope by app type.** The metadata, privacy-manifest, screenshots, and export-compliance checks
87
+ apply to any iOS app regardless of how it is built. The code-level checks grep the app's Swift
88
+ source (`*.swift`, plus `*.m`/`*.h` and `*.entitlements` where relevant), so they are most accurate
89
+ for native Swift / SwiftUI. On React Native (JavaScript) or Flutter (Dart) apps that logic is not in
90
+ Swift, so the code-level checks under-detect rather than misfire. iOS only.
91
+
86
92
  ---
87
93
 
88
94
  ## Auto-detection rules
@@ -134,7 +134,9 @@ fi
134
134
 
135
135
  # Locales: explicit config array, else the directory names under metadata/.
136
136
  declare -a LOCALES=()
137
+ LOCALES_FROM_CONFIG=""
137
138
  if [[ -f "$CONFIG" ]] && have_jq && [[ "$(jq -r '.locales | type' "$CONFIG" 2>/dev/null)" == "array" ]]; then
139
+ LOCALES_FROM_CONFIG=1
138
140
  while IFS= read -r l; do LOCALES+=("$l"); done < <(jq -r '.locales[]' "$CONFIG" 2>/dev/null)
139
141
  elif [[ -d "$META_DIR" ]]; then
140
142
  while IFS= read -r d; do
@@ -266,7 +268,18 @@ if (( ${#LOCALES[@]} > 0 )); then
266
268
  expected_files=(name.txt subtitle.txt description.txt keywords.txt)
267
269
  for loc in "${LOCALES[@]+"${LOCALES[@]}"}"; do
268
270
  d="$META_DIR/$loc"
269
- if [[ ! -d "$d" ]]; then fail "2.3.7 Locale missing — $d does not exist"; continue; fi
271
+ if [[ ! -d "$d" ]]; then
272
+ # A locale listed in .appstore-precheck.json but absent on disk is a
273
+ # config/reality mismatch, not a build fault: the locale was simply never
274
+ # submitted. Warn (don't block) so an approved set isn't falsely RED.
275
+ # Auto-detected locales always exist, so this only fires in config mode.
276
+ if [[ -n "$LOCALES_FROM_CONFIG" ]]; then
277
+ warn "2.3.7 Locale '$loc' is in .appstore-precheck.json but has no metadata folder ($d) — add it or remove '$loc' from the config 'locales' list"
278
+ else
279
+ fail "2.3.7 Locale missing — $d does not exist"
280
+ fi
281
+ continue
282
+ fi
270
283
  for f in "${expected_files[@]}"; do
271
284
  [[ -s "$d/$f" ]] || fail "2.3.7 Metadata missing — $d/$f is empty or absent"
272
285
  done
@@ -463,7 +476,7 @@ if [[ -d "$META_DIR" ]]; then
463
476
  [[ -z "$support_found" ]] && warn "2.3 Support URL — no non-empty support_url.txt in any locale under fastlane metadata; Apple requires a working support URL"
464
477
  [[ -z "$privacy_found" ]] && warn "2.3 Privacy URL — no non-empty privacy_url.txt in any locale under fastlane metadata; required for apps with accounts or in-app purchases"
465
478
  fi
466
- url_ph=$(grep -rEnI 'example\.com|localhost|\bTODO\b|changeme' "$META_DIR" --include='*_url.txt' 2>/dev/null | grep -v "^Binary file" | head -10)
479
+ url_ph=$(grep -rEnI 'example\.com|localhost|\bTODO\b|\bchangeme\b' "$META_DIR" --include='*_url.txt' 2>/dev/null | grep -v "^Binary file" | head -10)
467
480
  if [[ -n "$url_ph" ]]; then
468
481
  warn "2.3 Metadata URL — placeholder URL in fastlane metadata (replace before submitting):"
469
482
  echo "$url_ph" | sed 's/^/ /'
@@ -502,7 +515,7 @@ fi
502
515
  # avoid flagging legitimate words. Overlaps the Phase 2 precheck "No placeholder
503
516
  # text" rule, but this one is local and runs before any network call.
504
517
  if [[ -d "$META_DIR" ]]; then
505
- ph_re='lorem ipsum|Lorem ipsum|\bTODO\b|\bFIXME\b|example\.com|placeholder|insert .* here|changeme'
518
+ ph_re='lorem ipsum|Lorem ipsum|\bTODO\b|\bFIXME\b|example\.com|placeholder|insert .* here|\bchangeme\b'
506
519
  ph_hits=$(grep -rEnI "$ph_re" "$META_DIR" 2>/dev/null | grep -v "^Binary file" | head -20)
507
520
  if [[ -n "$ph_hits" ]]; then
508
521
  warn "2.1 Metadata content — placeholder/dummy text in store metadata (looks unfinished; rejected under 2.1):"