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 +33 -0
- package/README.md +24 -5
- package/package.json +1 -1
- package/skills/appstore-precheck/SKILL.md +39 -16
- package/skills/appstore-precheck/evals/evals.json +2 -2
- package/skills/appstore-precheck/references/methodology.md +6 -0
- package/skills/appstore-precheck/scripts/scan.sh +16 -3
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.
|
|
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`
|
|
181
|
-
|
|
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.
|
|
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.
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
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
|
|
153
|
-
|
|
154
|
-
-
|
|
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:**
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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):"
|