appstore-precheck 1.1.0
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 +60 -0
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/bin/cli.js +119 -0
- package/package.json +31 -0
- package/skills/appstore-precheck/SKILL.md +188 -0
- package/skills/appstore-precheck/config.example.json +25 -0
- package/skills/appstore-precheck/evals/README.md +40 -0
- package/skills/appstore-precheck/evals/evals.json +73 -0
- package/skills/appstore-precheck/evals/files/green-app/App/ContentView.swift +2 -0
- package/skills/appstore-precheck/evals/files/green-app/App/Info.plist +3 -0
- package/skills/appstore-precheck/evals/files/green-app/App/PrivacyInfo.xcprivacy +3 -0
- package/skills/appstore-precheck/evals/files/green-app/App/SubscriptionView.swift +13 -0
- package/skills/appstore-precheck/evals/files/green-app/fastlane/metadata/en-US/description.txt +1 -0
- package/skills/appstore-precheck/evals/files/green-app/fastlane/metadata/en-US/keywords.txt +1 -0
- package/skills/appstore-precheck/evals/files/green-app/fastlane/metadata/en-US/name.txt +1 -0
- package/skills/appstore-precheck/evals/files/green-app/fastlane/metadata/en-US/privacy_url.txt +1 -0
- package/skills/appstore-precheck/evals/files/green-app/fastlane/metadata/en-US/subtitle.txt +1 -0
- package/skills/appstore-precheck/evals/files/green-app/fastlane/metadata/en-US/support_url.txt +1 -0
- package/skills/appstore-precheck/evals/files/no-iap-app/App/ContentView.swift +2 -0
- package/skills/appstore-precheck/evals/files/no-iap-app/App/Info.plist +3 -0
- package/skills/appstore-precheck/evals/files/no-iap-app/App/PrivacyInfo.xcprivacy +3 -0
- package/skills/appstore-precheck/evals/files/no-iap-app/fastlane/metadata/en-US/description.txt +1 -0
- package/skills/appstore-precheck/evals/files/no-iap-app/fastlane/metadata/en-US/keywords.txt +1 -0
- package/skills/appstore-precheck/evals/files/no-iap-app/fastlane/metadata/en-US/name.txt +1 -0
- package/skills/appstore-precheck/evals/files/no-iap-app/fastlane/metadata/en-US/subtitle.txt +1 -0
- package/skills/appstore-precheck/evals/files/red-app/App/ContentView.swift +2 -0
- package/skills/appstore-precheck/evals/files/red-app/App/Info.plist +3 -0
- package/skills/appstore-precheck/evals/files/red-app/App/PrivacyInfo.xcprivacy +3 -0
- package/skills/appstore-precheck/evals/files/red-app/App/SubscriptionView.swift +3 -0
- package/skills/appstore-precheck/evals/files/red-app/fastlane/metadata/en-US/description.txt +1 -0
- package/skills/appstore-precheck/evals/files/red-app/fastlane/metadata/en-US/keywords.txt +1 -0
- package/skills/appstore-precheck/evals/files/red-app/fastlane/metadata/en-US/name.txt +1 -0
- package/skills/appstore-precheck/evals/files/red-app/fastlane/metadata/en-US/subtitle.txt +1 -0
- package/skills/appstore-precheck/guidelines-baseline.json +35 -0
- package/skills/appstore-precheck/references/methodology.md +131 -0
- package/skills/appstore-precheck/scripts/phase2-precheck.sh +68 -0
- package/skills/appstore-precheck/scripts/scan.sh +513 -0
- package/skills/appstore-precheck/scripts/verdict.sh +75 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. Versioning follows
|
|
4
|
+
[SemVer](https://semver.org/). Released as git tags.
|
|
5
|
+
|
|
6
|
+
## [1.1.0] - 2026-06-28
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
- `npx appstore-precheck` CLI (`bin/cli.js`): run the static scan with no clone and no install.
|
|
10
|
+
It scans the current directory (or `--dir <path>`), prints the scan output and the verdict
|
|
11
|
+
verbatim, and exits non-zero on RED (or on YELLOW with `--fail-on YELLOW`), mirroring the
|
|
12
|
+
GitHub Action. A thin wrapper over the bundled `scan.sh` / `verdict.sh`; it adds no new checks.
|
|
13
|
+
- Published to npm under the package name `appstore-precheck`.
|
|
14
|
+
- `tests/test-cli.sh`: covers the CLI's verdict mapping and exit codes (GREEN/RED/YELLOW,
|
|
15
|
+
`--fail-on`, `--version`, `--help`, bad-usage), wired into the suite and `npm run lint`.
|
|
16
|
+
|
|
17
|
+
## [1.0.0] - 2026-06-28
|
|
18
|
+
|
|
19
|
+
Initial release.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `appstore-precheck` Agent Skill: read-only iOS App Store pre-submission gate.
|
|
23
|
+
- `scripts/scan.sh`: portable Bash scanner for 20 rejection vectors (including advisory Sign in
|
|
24
|
+
with Apple parity and external-purchase-link checks) with zero-config
|
|
25
|
+
auto-detection of a standard fastlane + Xcode layout, plus `.appstore-precheck.json` overrides.
|
|
26
|
+
- Five more signal-gated advisory checks (15 → 20): tracking/IDFA SDK shipped without an ATT
|
|
27
|
+
prompt (5.1.2, the reverse of the existing ATT check), the export-compliance key
|
|
28
|
+
(`ITSAppUsesNonExemptEncryption`), support/privacy URL presence in fastlane metadata (2.3),
|
|
29
|
+
analytics SDK vs `PrivacyInfo.xcprivacy` data-type declarations (5.1.1), and placeholder/dummy
|
|
30
|
+
copy in store metadata (2.1). All WARN-only, with a `tracking-app` fixture that exercises them.
|
|
31
|
+
- `scripts/verdict.sh`: deterministic GREEN/YELLOW/RED verdict and `.precheck-pass` token
|
|
32
|
+
action from the scan output, so the verdict is machine-testable, not just an agent judgement.
|
|
33
|
+
- 5-phase flow: guideline-drift check, static scan, Apple `fastlane precheck` wrapper,
|
|
34
|
+
adversarial reviewer pass, consolidation + `.precheck-pass` token.
|
|
35
|
+
- Optional Claude Code upload-guard hook (`hooks/fastlane-guard.sh`) that blocks
|
|
36
|
+
`fastlane deliver/pilot/release` without a fresh token.
|
|
37
|
+
- Cross-tool support: native `SKILL.md` for Claude Code / Codex / Cursor / Gemini CLI,
|
|
38
|
+
root `AGENTS.md`, and an `install.sh` multi-host installer. Claude Code and Codex CLI runs
|
|
39
|
+
verified end-to-end (`docs/cross-tool-verification.md`).
|
|
40
|
+
- Claude Code plugin + single-plugin marketplace manifests.
|
|
41
|
+
- Test suite (`tests/`): fixture scans plus unit tests for verdict thresholds, the upload-guard
|
|
42
|
+
hook, config overrides, and the installer. CI runs ShellCheck, JSON validation, a
|
|
43
|
+
version-consistency guard, and the full suite on every push and PR.
|
|
44
|
+
- Documentation: methodology reference, a how-to-add-a-check guide, an agent-portability note,
|
|
45
|
+
real Phase 0 drift-check and Phase 2 `fastlane precheck` examples, and a field-test report
|
|
46
|
+
from dogfooding real App Store apps (`docs/`).
|
|
47
|
+
- Community health files: contributing guide, security policy, code of conduct, and issue/PR
|
|
48
|
+
templates.
|
|
49
|
+
- Behavioral eval suite (`skills/appstore-precheck/evals/`) in the Agent Skills format: RED /
|
|
50
|
+
GREEN / no-IAP cases with self-contained inputs and assertions.
|
|
51
|
+
- Branding: logo, social preview, and **Pierre**, the French App Review critic mascot whose
|
|
52
|
+
voice drives the Phase 3 adversarial review.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
- More reliable app-target auto-detection: resolve the iOS source dir via the app entry point
|
|
56
|
+
(`@main` / `AppDelegate`), not Info.plist position alone, so a Watch app, extension, or
|
|
57
|
+
framework is no longer mistaken for the main target. Required-link checks now scan the whole
|
|
58
|
+
paywall cluster instead of a single auto-picked file. (Found by dogfooding Pocket Casts,
|
|
59
|
+
Wikipedia, and DuckDuckGo.)
|
|
60
|
+
- Portable empty-array handling under `set -u` on stock macOS bash 3.2 and modern bash.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Berkay Turk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/social-preview.png" width="840" alt="appstore-precheck, a French critic reviews your build before Apple does">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://github.com/berkayturk/appstore-precheck/actions/workflows/ci.yml"><img src="https://github.com/berkayturk/appstore-precheck/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
|
7
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="License: MIT"></a>
|
|
8
|
+
<img src="https://img.shields.io/badge/platform-iOS-lightgrey.svg" alt="Platform: iOS">
|
|
9
|
+
<img src="https://img.shields.io/badge/Agent%20Skill-open%20standard-1F6FEB.svg" alt="Agent Skill">
|
|
10
|
+
<img src="https://img.shields.io/badge/works%20with-Claude%20Code%20·%20Codex%20·%20Cursor%20·%20Gemini-2563EB.svg" alt="Works with Claude Code, Codex, Cursor, Gemini">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center"><strong>Catch App Store rejections before a reviewer does.</strong></p>
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
`appstore-precheck` is a read-only, pre-submission gate for iOS apps. It statically scans the most
|
|
18
|
+
common rejection vectors, runs Apple's own metadata linter, watches the App Store Review Guidelines
|
|
19
|
+
for drift, and finishes with an adversarial review pass, then hands you a single **GREEN / YELLOW /
|
|
20
|
+
RED** verdict. It never edits your code.
|
|
21
|
+
|
|
22
|
+
It ships as a portable [Agent Skill](https://agentskills.io): the same `SKILL.md` runs natively in
|
|
23
|
+
Claude Code, OpenAI Codex, Cursor, and Gemini CLI. The scanner is plain Bash, so you can also run it
|
|
24
|
+
by hand or wire it into CI.
|
|
25
|
+
|
|
26
|
+
## Meet Pierre
|
|
27
|
+
|
|
28
|
+
<img src="assets/mascot.png" align="right" width="124" alt="Pierre, the French app reviewer">
|
|
29
|
+
|
|
30
|
+
Your verdict is delivered by **Pierre**, a French critic who has seen ten thousand rejections and is
|
|
31
|
+
impressed by none of them. He reviews your build harder than Apple would, in private. A GREEN from
|
|
32
|
+
Pierre means Apple will wave you through.
|
|
33
|
+
|
|
34
|
+
- 🔴 **RED**: *"Non. Restore Purchases, absent. Guideline 3.1.2. Suivant."*
|
|
35
|
+
- 🟡 **YELLOW**: *"A few small uglinesses. I would not reject. But I noticed."*
|
|
36
|
+
- 🟢 **GREEN**: *"Hmf. I find nothing. Acceptable. Do not make me regret this."*
|
|
37
|
+
|
|
38
|
+
The verdict line is in Pierre's voice. The breakdown beneath it, every `file:line` and fix, stays
|
|
39
|
+
plain and surgical.
|
|
40
|
+
|
|
41
|
+
## What it checks
|
|
42
|
+
|
|
43
|
+
20 rejection vectors across code, fastlane metadata, screenshots, `PrivacyInfo.xcprivacy`, and the paywall:
|
|
44
|
+
|
|
45
|
+
| Guideline | Check |
|
|
46
|
+
|-----------|-------|
|
|
47
|
+
| **5.1.1(v)** | Privacy Manifest ↔ Required Reason API parity |
|
|
48
|
+
| **5.1.1** | A non-empty purpose string for every sensitive framework |
|
|
49
|
+
| **5.1.2** | ATT usage ↔ `NSUserTrackingUsageDescription` |
|
|
50
|
+
| **2.3.10** | No other-platform / competitor names in metadata |
|
|
51
|
+
| **2.3.1** | Metadata length limits (name, subtitle, keywords, promo, description) |
|
|
52
|
+
| **2.3.7** | Localized metadata parity across every locale |
|
|
53
|
+
| **2.3.3** | At least one screenshot per locale |
|
|
54
|
+
| **3.1.2** | Trial & auto-renew subscription disclosures |
|
|
55
|
+
| **3.1.2** | Restore Purchases + Terms (EULA) + Privacy Policy on the paywall |
|
|
56
|
+
| **2.5.1** | No private / banned APIs |
|
|
57
|
+
| **4.0** | Minimum functionality (real navigation) |
|
|
58
|
+
| **4.8** | Sign in with Apple offered when a third-party social login is used |
|
|
59
|
+
| **3.1.1(a)** | External purchase link entitlement + disclosure, when external purchase APIs are used |
|
|
60
|
+
| **5.1.5** | Sensitive-API justification *(opt-in)* |
|
|
61
|
+
| **5.1.2** | Tracking / IDFA SDK (AdMob, AppLovin, AppsFlyer, Adjust, …) shipped without an ATT prompt |
|
|
62
|
+
| **encryption** | `ITSAppUsesNonExemptEncryption` set, so App Store Connect skips the export-compliance question |
|
|
63
|
+
| **2.3** | A working support URL and a privacy URL in fastlane metadata (no placeholders) |
|
|
64
|
+
| **5.1.1** | Analytics SDK present ↔ `PrivacyInfo.xcprivacy` declares collected data / tracking domains |
|
|
65
|
+
| **2.1** | No placeholder / dummy copy (lorem ipsum, TODO, `example.com`) in store metadata |
|
|
66
|
+
|
|
67
|
+
Paywall checks are skipped automatically when no in-app-purchase signals are present.
|
|
68
|
+
|
|
69
|
+
## Quick start
|
|
70
|
+
|
|
71
|
+
**Claude Code**: install as a plugin:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
/plugin marketplace add berkayturk/appstore-precheck
|
|
75
|
+
/plugin install appstore-precheck@appstore-precheck
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Run instantly with npx** (no clone, no install):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx appstore-precheck # scans the current directory, prints the verdict
|
|
82
|
+
npx appstore-precheck --fail-on YELLOW
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
It runs the static scan over the current directory and exits non-zero on RED (or on YELLOW with
|
|
86
|
+
`--fail-on YELLOW`). Read-only, like everything else here.
|
|
87
|
+
|
|
88
|
+
**Codex, Cursor, Gemini, Claude**: clone, then run the installer from inside your iOS project:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
git clone https://github.com/berkayturk/appstore-precheck.git
|
|
92
|
+
/path/to/appstore-precheck/install.sh # → ./.claude/skills and ./.agents/skills
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Standalone**: the scanner is just Bash:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
bash skills/appstore-precheck/scripts/scan.sh
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**CI**: drop the static scan into a workflow with the bundled composite action. It fails the
|
|
102
|
+
job on a RED verdict; set `fail-on: YELLOW` to be stricter:
|
|
103
|
+
|
|
104
|
+
```yaml
|
|
105
|
+
- uses: berkayturk/appstore-precheck@v1.0.0
|
|
106
|
+
with:
|
|
107
|
+
working-directory: . # optional
|
|
108
|
+
fail-on: RED # optional (RED | YELLOW)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## How it works
|
|
112
|
+
|
|
113
|
+
| Phase | Step |
|
|
114
|
+
|-------|------|
|
|
115
|
+
| **0** | **Guideline drift**: diff the live App Store Review Guidelines against a tracked baseline. Never blocks. |
|
|
116
|
+
| **1** | **Static scan**: `scan.sh` over the 20 vectors above. |
|
|
117
|
+
| **2** | **`fastlane precheck`**: Apple's own metadata rule engine. |
|
|
118
|
+
| **3** | **Adversarial review**: Pierre role-plays a skeptical reviewer and drafts realistic rejection notices. |
|
|
119
|
+
| **4** | **Verdict**: GREEN / YELLOW / RED, and a `.precheck-pass` token the upload guard gates on. |
|
|
120
|
+
|
|
121
|
+
## Demo
|
|
122
|
+
|
|
123
|
+
<p align="center">
|
|
124
|
+
<img src="assets/demo.gif" width="760" alt="appstore-precheck running: a clean app passes GREEN, an app with rejection vectors is blocked RED">
|
|
125
|
+
</p>
|
|
126
|
+
|
|
127
|
+
A clean app passes **GREEN**; an app with rejection vectors is blocked **RED**. The verdict and
|
|
128
|
+
counts are deterministic ([`verdict.sh`](skills/appstore-precheck/scripts/verdict.sh)).
|
|
129
|
+
|
|
130
|
+
## Example
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
🔴 Pierre: "Non. Three faults. Apple would have found one. Suivant."
|
|
134
|
+
|
|
135
|
+
RED: 3 FAIL
|
|
136
|
+
• 3.1.2 Restore Purchases missing SubscriptionView.swift:14 → add restorePurchases()
|
|
137
|
+
• 2.3.10 "Android" in metadata en-US/description.txt:1 → remove the reference
|
|
138
|
+
• 5.1.1(v) FileTimestamp undeclared PrivacyInfo.xcprivacy → declare the reason
|
|
139
|
+
Submission blocked.
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
See [`examples/`](examples/) for full [GREEN](examples/green-pass.md) and [RED](examples/red-reject.md) runs,
|
|
143
|
+
plus real [Phase 0 drift-check](examples/drift-check.md) and [Phase 2 `fastlane precheck`](examples/fastlane-precheck.md) results.
|
|
144
|
+
|
|
145
|
+
## Output
|
|
146
|
+
|
|
147
|
+
| State | Meaning | Token | Upload guard |
|
|
148
|
+
|-------|---------|-------|--------------|
|
|
149
|
+
| 🟢 **GREEN** | 0 FAIL, ≤2 WARN | written (60 min) | allowed |
|
|
150
|
+
| 🟡 **YELLOW** | 0 FAIL, 3+ WARN | not written | blocked; needs confirmation |
|
|
151
|
+
| 🔴 **RED** | ≥1 FAIL | removed | blocked; shows the FAIL list |
|
|
152
|
+
|
|
153
|
+
## Configuration
|
|
154
|
+
|
|
155
|
+
Zero config for a standard fastlane + Xcode layout. The scanner auto-detects your source directory,
|
|
156
|
+
fastlane metadata, screenshots, String Catalog, paywall view, and locales. Override any of it with a
|
|
157
|
+
`.appstore-precheck.json` at your repo root (copy
|
|
158
|
+
[`config.example.json`](skills/appstore-precheck/config.example.json)).
|
|
159
|
+
|
|
160
|
+
## Cross-tool support
|
|
161
|
+
|
|
162
|
+
`SKILL.md` follows the [Agent Skills open standard](https://agentskills.io), with no per-tool conversion.
|
|
163
|
+
Hosts differ only in the directory they scan:
|
|
164
|
+
|
|
165
|
+
| Host | Reads from |
|
|
166
|
+
|------|-----------|
|
|
167
|
+
| Claude Code | `.claude/skills/` · `~/.claude/skills/` |
|
|
168
|
+
| OpenAI Codex | `.agents/skills/` · `~/.agents/skills/` |
|
|
169
|
+
| Cursor | `.agents/skills/`, `.cursor/skills/`, also `.claude/skills/` |
|
|
170
|
+
| Gemini CLI | `.agents/skills/`, `.gemini/skills/` |
|
|
171
|
+
|
|
172
|
+
A root [`AGENTS.md`](AGENTS.md) covers hosts that read always-on context instead of on-demand skills.
|
|
173
|
+
[`docs/cross-tool-verification.md`](docs/cross-tool-verification.md) records real per-host runs
|
|
174
|
+
(all four hosts verified end-to-end: Claude Code, Codex, Gemini, and Cursor), and
|
|
175
|
+
[`docs/field-tests.md`](docs/field-tests.md) records dogfooding the scanner against real
|
|
176
|
+
App Store apps (DuckDuckGo, Pocket Casts, Wikipedia).
|
|
177
|
+
|
|
178
|
+
## Requirements
|
|
179
|
+
|
|
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.
|
|
182
|
+
|
|
183
|
+
**Secrets**: the ASC API key is read from your environment at runtime and deleted immediately after
|
|
184
|
+
`fastlane precheck`. Never commit it; `.gitignore` blocks `*asc-key*.json` and `.env`.
|
|
185
|
+
|
|
186
|
+
## Uninstall
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
/plugin uninstall appstore-precheck@appstore-precheck # Claude Code plugin
|
|
190
|
+
rm -rf .claude/skills/appstore-precheck .agents/skills/appstore-precheck # manual install
|
|
191
|
+
rm -f .precheck-pass # runtime token
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Development
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
npm run lint # bash -n on every script
|
|
198
|
+
npm run check-versions # plugin.json / package.json / SKILL.md in lockstep
|
|
199
|
+
npm test # run scan.sh against fixture projects and assert
|
|
200
|
+
claude plugin validate .
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
CI runs ShellCheck, JSON validation, the version-consistency guard, and the fixture tests on every push.
|
|
204
|
+
|
|
205
|
+
## Disclaimer
|
|
206
|
+
|
|
207
|
+
This is a static heuristic tool. A GREEN result **lowers but does not eliminate** rejection risk;
|
|
208
|
+
Apple's guidelines change frequently and reviewer decisions are judgment calls. It performs no runtime
|
|
209
|
+
crash testing; always do a final manual review before you submit. Not affiliated with Apple.
|
|
210
|
+
|
|
211
|
+
## Star History
|
|
212
|
+
|
|
213
|
+
<a href="https://star-history.com/#berkayturk/appstore-precheck&Date">
|
|
214
|
+
<img src="https://api.star-history.com/svg?repos=berkayturk/appstore-precheck&type=Date" alt="Star History Chart" width="600">
|
|
215
|
+
</a>
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
[MIT](LICENSE) © Berkay Turk
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// appstore-precheck CLI: a thin Node wrapper so the scanner runs with no clone,
|
|
5
|
+
// via `npx appstore-precheck`. It shells out to the bundled scan.sh + verdict.sh,
|
|
6
|
+
// prints their output verbatim, and maps the verdict to an exit code (mirroring
|
|
7
|
+
// the GitHub Action). It does not reimplement any check; the Bash scripts are the
|
|
8
|
+
// single source of truth.
|
|
9
|
+
|
|
10
|
+
const { spawnSync } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
|
|
14
|
+
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
15
|
+
const SCAN = path.join(PKG_ROOT, 'skills', 'appstore-precheck', 'scripts', 'scan.sh');
|
|
16
|
+
const VERDICT = path.join(PKG_ROOT, 'skills', 'appstore-precheck', 'scripts', 'verdict.sh');
|
|
17
|
+
|
|
18
|
+
function pkgVersion() {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(fs.readFileSync(path.join(PKG_ROOT, 'package.json'), 'utf8')).version;
|
|
21
|
+
} catch (_) {
|
|
22
|
+
return 'unknown';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function printHelp() {
|
|
27
|
+
process.stdout.write(
|
|
28
|
+
`appstore-precheck ${pkgVersion()} - read-only iOS App Store pre-submission scan\n` +
|
|
29
|
+
`\n` +
|
|
30
|
+
`Usage:\n` +
|
|
31
|
+
` npx appstore-precheck [options]\n` +
|
|
32
|
+
`\n` +
|
|
33
|
+
`Runs the static scanner over the current directory and prints a\n` +
|
|
34
|
+
`GREEN / YELLOW / RED verdict. Read-only: it never edits your files.\n` +
|
|
35
|
+
`\n` +
|
|
36
|
+
`Options:\n` +
|
|
37
|
+
` --dir <path> Directory to scan (default: current directory)\n` +
|
|
38
|
+
` --fail-on <level> Exit non-zero at RED (default) or YELLOW\n` +
|
|
39
|
+
` -v, --version Print the version and exit\n` +
|
|
40
|
+
` -h, --help Show this help and exit\n` +
|
|
41
|
+
`\n` +
|
|
42
|
+
`Exit codes: 0 ok, 1 verdict at or past --fail-on, 64 bad usage,\n` +
|
|
43
|
+
`70 environment error (bash or scanner not available).\n` +
|
|
44
|
+
`\n` +
|
|
45
|
+
`Requires bash, git, grep, and find on PATH (macOS / Linux; on Windows\n` +
|
|
46
|
+
`use WSL or Git Bash). jq and python3 unlock the config + exact-length checks.\n`
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fail(message, code) {
|
|
51
|
+
process.stderr.write(`appstore-precheck: ${message}\n`);
|
|
52
|
+
process.exit(code);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseArgs(argv) {
|
|
56
|
+
const opts = { dir: process.cwd(), failOn: 'RED' };
|
|
57
|
+
for (let i = 0; i < argv.length; i++) {
|
|
58
|
+
const a = argv[i];
|
|
59
|
+
if (a === '-h' || a === '--help') { printHelp(); process.exit(0); }
|
|
60
|
+
if (a === '-v' || a === '--version') { process.stdout.write(pkgVersion() + '\n'); process.exit(0); }
|
|
61
|
+
if (a === '--dir') {
|
|
62
|
+
opts.dir = argv[++i];
|
|
63
|
+
if (!opts.dir) fail('--dir requires a path', 64);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (a === '--fail-on') {
|
|
67
|
+
const v = (argv[++i] || '').toUpperCase();
|
|
68
|
+
if (v !== 'RED' && v !== 'YELLOW') fail('--fail-on must be RED or YELLOW', 64);
|
|
69
|
+
opts.failOn = v;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
fail(`unknown option: ${a} (try --help)`, 64);
|
|
73
|
+
}
|
|
74
|
+
return opts;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function main() {
|
|
78
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
79
|
+
|
|
80
|
+
if (!fs.existsSync(SCAN) || !fs.existsSync(VERDICT)) {
|
|
81
|
+
fail('bundled scanner scripts are missing from the package', 70);
|
|
82
|
+
}
|
|
83
|
+
if (!fs.existsSync(opts.dir) || !fs.statSync(opts.dir).isDirectory()) {
|
|
84
|
+
fail(`not a directory: ${opts.dir}`, 64);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const scan = spawnSync('bash', [SCAN], {
|
|
88
|
+
cwd: opts.dir,
|
|
89
|
+
encoding: 'utf8',
|
|
90
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
91
|
+
});
|
|
92
|
+
if (scan.error && scan.error.code === 'ENOENT') {
|
|
93
|
+
fail('bash is required to run the scanner (install bash, or use WSL / Git Bash on Windows)', 70);
|
|
94
|
+
}
|
|
95
|
+
if (scan.error) fail(`failed to run the scanner: ${scan.error.message}`, 70);
|
|
96
|
+
|
|
97
|
+
const scanOut = scan.stdout || '';
|
|
98
|
+
process.stdout.write(scanOut);
|
|
99
|
+
|
|
100
|
+
const verdict = spawnSync('bash', [VERDICT], { input: scanOut, encoding: 'utf8' });
|
|
101
|
+
if (verdict.error) fail(`failed to compute the verdict: ${verdict.error.message}`, 70);
|
|
102
|
+
const summary = verdict.stdout || '';
|
|
103
|
+
|
|
104
|
+
process.stdout.write('----------------------------------------\n');
|
|
105
|
+
process.stdout.write(summary.endsWith('\n') ? summary : summary + '\n');
|
|
106
|
+
|
|
107
|
+
const m = summary.match(/^VERDICT:\s*(\w+)/m);
|
|
108
|
+
const v = m ? m[1] : 'UNKNOWN';
|
|
109
|
+
|
|
110
|
+
let code = 0;
|
|
111
|
+
if (opts.failOn === 'YELLOW') {
|
|
112
|
+
code = v === 'GREEN' ? 0 : 1;
|
|
113
|
+
} else {
|
|
114
|
+
code = v === 'RED' ? 1 : 0;
|
|
115
|
+
}
|
|
116
|
+
process.exit(code);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "appstore-precheck",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Read-only iOS App Store pre-submission check, packaged as a portable Agent Skill / Claude Code plugin and an npx CLI.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Berkay Turk (https://github.com/berkayturk)",
|
|
7
|
+
"homepage": "https://github.com/berkayturk/appstore-precheck",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/berkayturk/appstore-precheck.git"
|
|
11
|
+
},
|
|
12
|
+
"bin": {
|
|
13
|
+
"appstore-precheck": "bin/cli.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"bin/",
|
|
17
|
+
"skills/appstore-precheck/",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"CHANGELOG.md"
|
|
21
|
+
],
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=14"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"test": "bash tests/all.sh",
|
|
27
|
+
"check-versions": "bash scripts/check-versions.sh",
|
|
28
|
+
"lint": "node --check bin/cli.js && bash -n skills/appstore-precheck/scripts/scan.sh && bash -n skills/appstore-precheck/scripts/verdict.sh && bash -n skills/appstore-precheck/scripts/phase2-precheck.sh && bash -n hooks/fastlane-guard.sh && bash -n install.sh && bash -n tests/run.sh && bash -n tests/all.sh && bash -n tests/test-verdict.sh && bash -n tests/test-guard.sh && bash -n tests/test-config.sh && bash -n tests/test-install.sh && bash -n tests/test-phase2.sh && bash -n tests/test-cli.sh"
|
|
29
|
+
},
|
|
30
|
+
"keywords": ["ios", "app-store", "fastlane", "agent-skill", "claude-code", "precheck"]
|
|
31
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: appstore-precheck
|
|
3
|
+
description: Read-only pre-submission check for an iOS app before App Store review. Scans Swift code, fastlane metadata, screenshots, PrivacyInfo.xcprivacy, and the paywall for 20 rejection vectors, wraps Apple's official `fastlane precheck`, watches for live App Store Review Guideline drift, and runs an adversarial reviewer pass. Emits a GREEN/YELLOW/RED verdict and a `.precheck-pass` token an upload guard can gate on. Use when preparing an iOS App Store submission (before Archive, before "Submit for Review", before TestFlight, or before any `fastlane deliver/pilot/release`), or when the user mentions App Store rejection, app review, or fastlane upload.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: Berkay Turk
|
|
7
|
+
version: 1.1.0
|
|
8
|
+
allowed-tools: Bash Read Grep Glob WebFetch
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# App Store Precheck
|
|
12
|
+
|
|
13
|
+
A one-command gate to run before every iOS App Store submission. It minimizes the risk of
|
|
14
|
+
rejection by statically scanning the most common rejection vectors, running Apple's own
|
|
15
|
+
metadata linter, watching for guideline drift, and simulating an adversarial review pass.
|
|
16
|
+
|
|
17
|
+
**This skill is read-only.** It never edits code, metadata, or assets. It only reports and
|
|
18
|
+
writes a pass token. The detailed method (every rejection vector, the drift-check mechanics)
|
|
19
|
+
lives in [`references/methodology.md`](references/methodology.md); read it when you need the
|
|
20
|
+
specifics behind a check.
|
|
21
|
+
|
|
22
|
+
## When to run
|
|
23
|
+
|
|
24
|
+
- **Before** archiving for TestFlight.
|
|
25
|
+
- **Before** pressing "Submit for Review" in App Store Connect.
|
|
26
|
+
- **Before** any `fastlane deliver` / `release` / `pilot` (the optional upload guard hook gates this).
|
|
27
|
+
- On every point release.
|
|
28
|
+
|
|
29
|
+
Run it deliberately. This is a human-triggered gate, not an automatic background step.
|
|
30
|
+
|
|
31
|
+
## Configuration (optional)
|
|
32
|
+
|
|
33
|
+
The scanner auto-detects a standard fastlane + Xcode layout, so most projects need **zero
|
|
34
|
+
configuration**. To override detection, copy [`config.example.json`](config.example.json) to
|
|
35
|
+
`.appstore-precheck.json` at the repo root. Keys: `bundleId` (required for Phase 2),
|
|
36
|
+
`iosSourceDir`, `metadataDir`, `screenshotsDir`, `xcstringsPath`, `paywallGlobs`, `locales`,
|
|
37
|
+
`disclosureKeys.{subscription,trial}`, `optionalChecks.familyControls`, `reviewPrepNotes`.
|
|
38
|
+
See `config.example.json` for the full annotated list.
|
|
39
|
+
|
|
40
|
+
## Output contract
|
|
41
|
+
|
|
42
|
+
The skill reaches one of three terminal states:
|
|
43
|
+
|
|
44
|
+
| State | Meaning | `.precheck-pass` token | Guard behavior |
|
|
45
|
+
|-------|---------|------------------------|----------------|
|
|
46
|
+
| **GREEN** | No FAIL, ≤2 WARN | Written (valid 60 min) | Upload allowed |
|
|
47
|
+
| **YELLOW** | No FAIL but 3+ WARN | Not written | Guard blocks; ask for explicit confirmation |
|
|
48
|
+
| **RED** | At least 1 FAIL | Removed | Guard blocks; show the FAIL list |
|
|
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.
|
|
55
|
+
|
|
56
|
+
## Flow (5 phases: 0–4)
|
|
57
|
+
|
|
58
|
+
### Phase 0: Live guideline drift check
|
|
59
|
+
|
|
60
|
+
Diff the live [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/)
|
|
61
|
+
section numbers against `guidelines-baseline.json` to detect any section Apple **added or
|
|
62
|
+
removed** since the last reconciliation. **Always non-blocking (WARN at most).** Drift is a gap
|
|
63
|
+
in *our* coverage, never a fault of the build. The page truncates when fetched, so this needs a
|
|
64
|
+
two-pass technique; the exact prompts and the reconciliation procedure are in
|
|
65
|
+
[`references/methodology.md`](references/methodology.md#phase-0-guideline-drift-check). The
|
|
66
|
+
baseline is **never auto-updated**. Reconciliation is a deliberate human step.
|
|
67
|
+
|
|
68
|
+
### Phase 1: Static scan
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
bash skills/appstore-precheck/scripts/scan.sh
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Emits `FAIL:` / `WARN:` / `PASS:` lines covering 20 rejection vectors: Privacy Manifest parity
|
|
75
|
+
(5.1.1(v)), purpose strings (5.1.1), ATT (5.1.2), other-platform mentions (2.3.10), metadata
|
|
76
|
+
limits (2.3.1), localized parity (2.3.7), screenshots (2.3.3), trial & auto-renew disclosures
|
|
77
|
+
(3.1.2), Restore/Terms/Privacy links (3.1.2), private API (2.5.1), minimum functionality (4.0),
|
|
78
|
+
Sign in with Apple parity (4.8), external purchase links (3.1.1(a)), an opt-in Screen Time /
|
|
79
|
+
FamilyControls justification (5.1.5), tracking/IDFA SDK without an ATT prompt (5.1.2), the
|
|
80
|
+
export-compliance key (`ITSAppUsesNonExemptEncryption`), support/privacy URLs in fastlane
|
|
81
|
+
metadata (2.3), analytics SDK vs PrivacyInfo data-types (5.1.1), and placeholder/dummy metadata
|
|
82
|
+
copy (2.1). The IAP checks (8–10) are
|
|
83
|
+
skipped automatically when no in-app-purchase signals are present. The full check table is in
|
|
84
|
+
[`references/methodology.md`](references/methodology.md#phase-1-rejection-vectors).
|
|
85
|
+
|
|
86
|
+
The scanner is portable Bash, so you can also run it directly, outside any agent, for a quick CI
|
|
87
|
+
or pre-commit check.
|
|
88
|
+
|
|
89
|
+
### Phase 2: Apple's official `fastlane precheck`
|
|
90
|
+
|
|
91
|
+
Requires `bundleId` in config (or pass `app_identifier` directly) and App Store Connect API
|
|
92
|
+
credentials. **Never commit the key.** The bundled wrapper builds the ASC API key JSON from your
|
|
93
|
+
environment, runs precheck, and deletes the key on exit (use `--dry-run` to preview the command
|
|
94
|
+
with no credentials and no network):
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
ASC_KEY_ID=… ASC_ISSUER_ID=… ASC_P8_PATH=/path/AuthKey.p8 \
|
|
98
|
+
bash skills/appstore-precheck/scripts/phase2-precheck.sh com.example.app
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Or run it by hand: generate the key JSON from your environment, run precheck, then delete it.
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
fastlane run precheck \
|
|
105
|
+
app_identifier:"<YOUR_BUNDLE_ID>" \
|
|
106
|
+
api_key_path:"/tmp/asc-key.json" \
|
|
107
|
+
include_in_app_purchases:false \
|
|
108
|
+
default_rule_level:":error"
|
|
109
|
+
rm -f /tmp/asc-key.json # delete the secret immediately
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Apple's own rule engine checks URLs, GitHub mentions, profanity, Apple trademarks, pricing
|
|
113
|
+
language, and beta keywords. `Result: true` → PASS; any violation line → FAIL. IAP is already
|
|
114
|
+
covered by Phase 1, so `include_in_app_purchases:false` avoids the API-key IAP limitation.
|
|
115
|
+
|
|
116
|
+
### Phase 3: Adversarial review (most important)
|
|
117
|
+
|
|
118
|
+
Dispatch a subagent that role-plays **Pierre**, a skeptical Apple App Reviewer with the exacting
|
|
119
|
+
palate of a French critic. Use this prompt verbatim, filling in the app's specifics:
|
|
120
|
+
|
|
121
|
+
> You are **Pierre**, a veteran Apple App Reviewer who critiques like a French critic. You have
|
|
122
|
+
> seen ten thousand rejections and are impressed by none of them. A new submission just landed on
|
|
123
|
+
> your desk. Your job is to **realistically try to reject it**, with no approval bias. Pick **5 random
|
|
124
|
+
> guideline items** with a spread across sections (one 2.x, two 3.x weighted toward paywall,
|
|
125
|
+
> one 4.x, one 5.x); choose a different combination each run (include a seed line so reruns
|
|
126
|
+
> vary). For each item: (Pass A) grep the relevant files for at least 2 concrete pieces of
|
|
127
|
+
> 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
|
|
129
|
+
> I flag this?" (Pass C) write a rejection draft in Apple's real voice (Guideline X.Y.Z –
|
|
130
|
+
> Category / We noticed… / Specifically… / Next Steps… / Resources…). Assign each item a risk:
|
|
131
|
+
> REJECT-CERTAIN / REJECT-RISK / WARN / PASS. End with a submit recommendation (HOLD / SUBMIT
|
|
132
|
+
> WITH WARNINGS / GO) and the single most critical fix. Read-only: never modify files; if you
|
|
133
|
+
> can't find evidence, say so rather than inventing it. Keep it under 500 words. Include at
|
|
134
|
+
> least one PASS and at least one risk-bearing item, to give a realistic distribution.
|
|
135
|
+
|
|
136
|
+
### Phase 4: Consolidation + token
|
|
137
|
+
|
|
138
|
+
The GREEN/YELLOW/RED decision and token action are **deterministic**, derived purely from the
|
|
139
|
+
FAIL/WARN counts. [`scripts/verdict.sh`](scripts/verdict.sh) computes them so the verdict is
|
|
140
|
+
machine-testable, not just an agent judgement; pipe the scan into it:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
bash skills/appstore-precheck/scripts/verdict.sh < scan-output.txt # prints VERDICT / COUNTS / TOKEN
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
It exits 0 GREEN / 1 RED / 2 YELLOW, and with `--apply` writes or removes `.precheck-pass`
|
|
147
|
+
accordingly (YELLOW holds the token for explicit human confirmation). Phase 0–3 still produce the
|
|
148
|
+
narrative; verdict.sh just pins the threshold arithmetic.
|
|
149
|
+
|
|
150
|
+
1. Gather Phase 0–3 output; tally FAIL + WARN + PASS into the output-contract table.
|
|
151
|
+
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."*
|
|
155
|
+
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
|
|
161
|
+
and surgical, never paraphrased.
|
|
162
|
+
4. Print the final manual checklist (see
|
|
163
|
+
[`references/methodology.md`](references/methodology.md#pre-submit-manual-checklist)).
|
|
164
|
+
|
|
165
|
+
## Rules
|
|
166
|
+
|
|
167
|
+
- **READ-ONLY:** never change code or assets. Only report and write the token.
|
|
168
|
+
- **Speed > exhaustiveness:** `scan.sh` uses parallel grep/jq and finishes in seconds.
|
|
169
|
+
- **No error swallowing:** if any scan command fails, that line is reported as FAIL and the scan continues.
|
|
170
|
+
- **Token location:** `.precheck-pass` at the repo root; the guard tests it with an `mmin -60` filter.
|
|
171
|
+
- **Local-only:** designed for manual, local runs; keep it out of CI to avoid false signals.
|
|
172
|
+
|
|
173
|
+
## Known limits
|
|
174
|
+
|
|
175
|
+
- No runtime crash testing; that's TestFlight + a crash reporter. Static analysis only.
|
|
176
|
+
- Several checks are advisory WARNs gated on detected signals (Sign in with Apple 4.8,
|
|
177
|
+
external-purchase 3.1.1(a), tracking/IDFA without ATT, analytics vs privacy manifest, metadata
|
|
178
|
+
URLs and placeholder copy). The export-compliance key is flagged when absent, but the actual
|
|
179
|
+
encryption answer still belongs in App Store Connect.
|
|
180
|
+
- The adversarial reviewer is a heuristic simulation, not a guarantee of Apple's decision.
|
|
181
|
+
- iOS only. Android / Play Console is out of scope.
|
|
182
|
+
- Phase 0 detects only **structural** drift (added/removed section numbers); see the reference for why.
|
|
183
|
+
|
|
184
|
+
## Optional: upload guard hook
|
|
185
|
+
|
|
186
|
+
`hooks/fastlane-guard.sh` blocks `fastlane deliver/pilot/release` unless a fresh `.precheck-pass`
|
|
187
|
+
token exists. In Claude Code it auto-wires via `hooks/hooks.json`. In other environments, wire it
|
|
188
|
+
as a pre-command check yourself, or treat the token as a manual go/no-go signal.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_comment": "Copy to your repo root as .appstore-precheck.json. Every key is OPTIONAL; the scanner auto-detects a standard fastlane + Xcode layout. Use 'auto' or omit a key to keep auto-detection.",
|
|
3
|
+
|
|
4
|
+
"bundleId": "com.example.app",
|
|
5
|
+
|
|
6
|
+
"iosSourceDir": "auto",
|
|
7
|
+
"metadataDir": "auto",
|
|
8
|
+
"screenshotsDir": "auto",
|
|
9
|
+
"xcstringsPath": "auto",
|
|
10
|
+
|
|
11
|
+
"paywallGlobs": ["*ubscription*.swift", "*paywall*.swift", "*purchase*.swift"],
|
|
12
|
+
|
|
13
|
+
"locales": "auto",
|
|
14
|
+
|
|
15
|
+
"disclosureKeys": {
|
|
16
|
+
"subscription": "subscription_disclosure",
|
|
17
|
+
"trial": "subscription_trial_disclosure"
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
"optionalChecks": {
|
|
21
|
+
"familyControls": false
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
"reviewPrepNotes": ""
|
|
25
|
+
}
|