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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Evals
|
|
2
|
+
|
|
3
|
+
Behavioral evaluations for the `appstore-precheck` skill, in the
|
|
4
|
+
[Agent Skills eval format](https://agentskills.io/skill-creation/evaluating-skills)
|
|
5
|
+
([`evals.json`](evals.json) + self-contained inputs under [`files/`](files/)).
|
|
6
|
+
|
|
7
|
+
These test the **agent-behavior layer**: given a realistic submission-intent prompt and a throwaway
|
|
8
|
+
iOS project, does the agent invoke the skill, run the scanner, reach the correct **GREEN/YELLOW/RED**
|
|
9
|
+
verdict, and present it faithfully: one Pierre line as a thin wrapper, the scan's `FAIL`/`WARN`/`PASS`
|
|
10
|
+
lines verbatim, never paraphrased? Each case carries `assertions` describing what a correct run must
|
|
11
|
+
contain.
|
|
12
|
+
|
|
13
|
+
| # | Case | Input | Expected |
|
|
14
|
+
|---|------|-------|----------|
|
|
15
|
+
| 1 | `red-blocks-submission` | `files/red-app` (Android mention + paywall missing links) | **RED**, no token |
|
|
16
|
+
| 2 | `green-allows-submission` | `files/green-app` (clean + full paywall) | **GREEN**, token written |
|
|
17
|
+
| 3 | `no-iap-skips-paywall-checks` | `files/no-iap-app` (no StoreKit) | 3.1.2 skipped, not RED |
|
|
18
|
+
|
|
19
|
+
## Running them
|
|
20
|
+
|
|
21
|
+
The official [`skill-creator`](https://code.claude.com/docs/en/skills#run-evals-with-skill-creator)
|
|
22
|
+
plugin runs `evals.json` by spawning an isolated subagent per case (with-skill vs without-skill),
|
|
23
|
+
capturing assertion pass/fail evidence into `grading.json` and aggregate stats into `benchmark.json`.
|
|
24
|
+
|
|
25
|
+
You can also sanity-check the **mechanical** expectations directly: copy an input out of the repo
|
|
26
|
+
(so the scanner treats it as its own project root) and run the scan + deterministic verdict:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
tmp=$(mktemp -d); cp -R skills/appstore-precheck/evals/files/red-app/. "$tmp/"
|
|
30
|
+
( cd "$tmp" && bash "$OLDPWD/skills/appstore-precheck/scripts/scan.sh" \
|
|
31
|
+
| bash "$OLDPWD/skills/appstore-precheck/scripts/verdict.sh" )
|
|
32
|
+
# → VERDICT: RED (green-app → GREEN; no-iap-app → GREEN with paywall checks skipped)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Scope
|
|
36
|
+
|
|
37
|
+
Evals cover agent behavior; they complement, not replace, the deterministic scanner suite in
|
|
38
|
+
[`tests/`](../../../tests/) (verdict thresholds, the upload-guard hook, config overrides, the
|
|
39
|
+
installer) and the live cross-host runs in
|
|
40
|
+
[`docs/cross-tool-verification.md`](../../../docs/cross-tool-verification.md).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"skill_name": "appstore-precheck",
|
|
3
|
+
"description": "Behavioral evals for the appstore-precheck skill: does the agent invoke the skill, run the scanner, reach the correct GREEN/YELLOW/RED verdict, and present it faithfully (Pierre voice as a thin wrapper, scan output never paraphrased)? Inputs are self-contained throwaway iOS projects under evals/files/. The mechanical layer (scan.sh / verdict.sh / guard) is separately covered by the deterministic suite in tests/.",
|
|
4
|
+
"evals": [
|
|
5
|
+
{
|
|
6
|
+
"id": 1,
|
|
7
|
+
"name": "red-blocks-submission",
|
|
8
|
+
"prompt": "I'm about to submit this iOS app to the App Store. Run the appstore-precheck and tell me whether I can submit.",
|
|
9
|
+
"files": [
|
|
10
|
+
"evals/files/red-app/App/ContentView.swift",
|
|
11
|
+
"evals/files/red-app/App/SubscriptionView.swift",
|
|
12
|
+
"evals/files/red-app/App/Info.plist",
|
|
13
|
+
"evals/files/red-app/App/PrivacyInfo.xcprivacy",
|
|
14
|
+
"evals/files/red-app/fastlane/metadata/en-US/name.txt",
|
|
15
|
+
"evals/files/red-app/fastlane/metadata/en-US/subtitle.txt",
|
|
16
|
+
"evals/files/red-app/fastlane/metadata/en-US/description.txt",
|
|
17
|
+
"evals/files/red-app/fastlane/metadata/en-US/keywords.txt"
|
|
18
|
+
],
|
|
19
|
+
"expected_output": "A RED verdict that blocks submission: the metadata names another platform (2.3.10) and the paywall is missing Restore Purchases, Terms of Use, and Privacy Policy (3.1.2). No .precheck-pass token is written.",
|
|
20
|
+
"assertions": [
|
|
21
|
+
"The verdict is RED and submission is reported as blocked",
|
|
22
|
+
"The .precheck-pass token is NOT written (it is removed/withheld)",
|
|
23
|
+
"Flags Guideline 2.3.10: the 'Android' / 'Google Play' reference in the metadata description",
|
|
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",
|
|
26
|
+
"The scan's FAIL lines are shown verbatim (machine-faithful), not paraphrased or invented"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": 2,
|
|
31
|
+
"name": "green-allows-submission",
|
|
32
|
+
"prompt": "Run the App Store pre-submission check on this project before I upload with fastlane.",
|
|
33
|
+
"files": [
|
|
34
|
+
"evals/files/green-app/App/ContentView.swift",
|
|
35
|
+
"evals/files/green-app/App/SubscriptionView.swift",
|
|
36
|
+
"evals/files/green-app/App/Info.plist",
|
|
37
|
+
"evals/files/green-app/App/PrivacyInfo.xcprivacy",
|
|
38
|
+
"evals/files/green-app/fastlane/metadata/en-US/name.txt",
|
|
39
|
+
"evals/files/green-app/fastlane/metadata/en-US/subtitle.txt",
|
|
40
|
+
"evals/files/green-app/fastlane/metadata/en-US/description.txt",
|
|
41
|
+
"evals/files/green-app/fastlane/metadata/en-US/keywords.txt"
|
|
42
|
+
],
|
|
43
|
+
"expected_output": "A GREEN verdict: no FAILs. The paywall has Restore/Terms/Privacy and the metadata is clean, so the .precheck-pass token is written.",
|
|
44
|
+
"assertions": [
|
|
45
|
+
"The verdict is GREEN",
|
|
46
|
+
"The .precheck-pass token is written",
|
|
47
|
+
"Confirms Restore Purchases, Terms of Use, and Privacy Policy are present on the paywall",
|
|
48
|
+
"Reports zero FAIL lines",
|
|
49
|
+
"The response opens with exactly one in-character Pierre line"
|
|
50
|
+
]
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": 3,
|
|
54
|
+
"name": "no-iap-skips-paywall-checks",
|
|
55
|
+
"prompt": "Is this app ready for the App Store? Run the precheck.",
|
|
56
|
+
"files": [
|
|
57
|
+
"evals/files/no-iap-app/App/ContentView.swift",
|
|
58
|
+
"evals/files/no-iap-app/App/Info.plist",
|
|
59
|
+
"evals/files/no-iap-app/App/PrivacyInfo.xcprivacy",
|
|
60
|
+
"evals/files/no-iap-app/fastlane/metadata/en-US/name.txt",
|
|
61
|
+
"evals/files/no-iap-app/fastlane/metadata/en-US/subtitle.txt",
|
|
62
|
+
"evals/files/no-iap-app/fastlane/metadata/en-US/description.txt",
|
|
63
|
+
"evals/files/no-iap-app/fastlane/metadata/en-US/keywords.txt"
|
|
64
|
+
],
|
|
65
|
+
"expected_output": "No in-app-purchase signals are present, so the 3.1.2 paywall checks are skipped (not failed). The verdict is not RED.",
|
|
66
|
+
"assertions": [
|
|
67
|
+
"Detects no in-app-purchase / subscription signals and skips the 3.1.2 paywall checks",
|
|
68
|
+
"Does NOT report any 3.1.2 paywall FAIL (missing Restore/Terms/Privacy)",
|
|
69
|
+
"The verdict is not RED"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0"><dict><key>CFBundleDisplayName</key><string>Demo</string><key>ITSAppUsesNonExemptEncryption</key><false/></dict></plist>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import StoreKit
|
|
3
|
+
struct SubscriptionView: View {
|
|
4
|
+
var body: some View {
|
|
5
|
+
VStack {
|
|
6
|
+
Button("Subscribe") {}
|
|
7
|
+
Button("Restore Purchases") { restorePurchases() }
|
|
8
|
+
Link("Terms of Use", destination: URL(string: "https://example.com/terms")!)
|
|
9
|
+
Link("Privacy Policy", destination: URL(string: "https://example.com/privacy")!)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
func restorePurchases() {}
|
|
13
|
+
}
|
package/skills/appstore-precheck/evals/files/green-app/fastlane/metadata/en-US/description.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
A focus and habit app that helps you build better routines.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
focus,habits
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Demo App
|
package/skills/appstore-precheck/evals/files/green-app/fastlane/metadata/en-US/privacy_url.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
https://demo.example-app.app/privacy
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Focus
|
package/skills/appstore-precheck/evals/files/green-app/fastlane/metadata/en-US/support_url.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
https://demo.example-app.app/support
|
package/skills/appstore-precheck/evals/files/no-iap-app/fastlane/metadata/en-US/description.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
A simple offline notes app with no purchases.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
notes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Demo App
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Notes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
A great focus app. Also available on Android and Google Play.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
focus
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Demo App
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Focus
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"_doc": "Baseline of Apple App Store Review Guidelines section numbers, for drift detection. Phase 0 fetches the live guidelines page and diffs its section numbers against `all_sections`; any NEW/REMOVED number => WARN: guideline-drift. This file is updated ONLY by deliberate human reconciliation (see SKILL.md Phase 0), NEVER auto-edited by the skill, otherwise the warning would be silently swallowed.",
|
|
3
|
+
"source_url": "https://developer.apple.com/app-store/review/guidelines/",
|
|
4
|
+
"reconciled_on": "2026-06-24",
|
|
5
|
+
"apple_last_updated_seen": "not-exposed-in-DOM (the page shows no visible date stamp; drift signal = the set of section numbers)",
|
|
6
|
+
"drift_check_coverage": "Reliable for Sections 1-4 + 5.1-5.4 (high-churn regions: 2.5.x, 3.1.x, 4.7, 5.1.x). The 5.5-5.6.x tail can't be fetched (the page truncates ~5.4) and is structurally stable; review it by hand at reconciliation.",
|
|
7
|
+
"covered_by_scan": [
|
|
8
|
+
"2.3.1", "2.3.3", "2.3.7", "2.3.10",
|
|
9
|
+
"2.5.1",
|
|
10
|
+
"3.1.2",
|
|
11
|
+
"4.0",
|
|
12
|
+
"5.1.1", "5.1.2", "5.1.5"
|
|
13
|
+
],
|
|
14
|
+
"all_sections": [
|
|
15
|
+
"1.1", "1.1.1", "1.1.2", "1.1.3", "1.1.4", "1.1.5", "1.1.6", "1.1.7",
|
|
16
|
+
"1.2", "1.2.1", "1.3", "1.4", "1.4.1", "1.4.2", "1.4.3", "1.4.4", "1.4.5",
|
|
17
|
+
"1.5", "1.6", "1.7",
|
|
18
|
+
"2.1", "2.2", "2.3", "2.3.1", "2.3.2", "2.3.3", "2.3.4", "2.3.5", "2.3.6",
|
|
19
|
+
"2.3.7", "2.3.8", "2.3.9", "2.3.10", "2.3.11", "2.3.12", "2.3.13",
|
|
20
|
+
"2.4", "2.4.1", "2.4.2", "2.4.3", "2.4.4", "2.4.5",
|
|
21
|
+
"2.5", "2.5.1", "2.5.2", "2.5.3", "2.5.4", "2.5.5", "2.5.6", "2.5.8", "2.5.9",
|
|
22
|
+
"2.5.11", "2.5.12", "2.5.13", "2.5.14", "2.5.15", "2.5.16", "2.5.17", "2.5.18",
|
|
23
|
+
"3.1", "3.1.1", "3.1.2", "3.1.3", "3.1.4", "3.1.5",
|
|
24
|
+
"3.2", "3.2.1", "3.2.2",
|
|
25
|
+
"4.1", "4.2", "4.2.1", "4.2.2", "4.2.3", "4.2.6", "4.2.7",
|
|
26
|
+
"4.3", "4.4", "4.4.1", "4.4.2",
|
|
27
|
+
"4.5", "4.5.1", "4.5.2", "4.5.3", "4.5.4", "4.5.5", "4.5.6",
|
|
28
|
+
"4.7", "4.7.1", "4.7.2", "4.7.3", "4.7.4", "4.7.5",
|
|
29
|
+
"4.8", "4.9", "4.10",
|
|
30
|
+
"5.1", "5.1.1", "5.1.2", "5.1.3", "5.1.4", "5.1.5",
|
|
31
|
+
"5.2", "5.2.1", "5.2.2", "5.2.3", "5.2.4", "5.2.5",
|
|
32
|
+
"5.3", "5.3.1", "5.3.2", "5.3.3", "5.3.4",
|
|
33
|
+
"5.4", "5.5", "5.6", "5.6.1", "5.6.2", "5.6.3", "5.6.4", "5.6.5", "5.6.6", "5.6.7"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Methodology: App Store Precheck
|
|
2
|
+
|
|
3
|
+
The detailed reference behind the skill. Read the section you need; you do not need to read this
|
|
4
|
+
whole file to run the skill.
|
|
5
|
+
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- [Phase 0: Guideline drift check](#phase-0-guideline-drift-check)
|
|
9
|
+
- [Phase 1: Rejection vectors](#phase-1-rejection-vectors)
|
|
10
|
+
- [Auto-detection rules](#auto-detection-rules)
|
|
11
|
+
- [Verdict thresholds](#verdict-thresholds)
|
|
12
|
+
- [Pre-submit manual checklist](#pre-submit-manual-checklist)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Phase 0: Guideline drift check
|
|
17
|
+
|
|
18
|
+
**Why:** the scanner's checks and `fastlane precheck`'s rule engine are static and hand-maintained.
|
|
19
|
+
Apple changes the [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/)
|
|
20
|
+
without notice. This phase flags whether any guideline **section number was added or removed**
|
|
21
|
+
since the last reconciliation, catching drift without manual upkeep. It is **always
|
|
22
|
+
non-blocking (WARN at most)**: drift is a gap in *our* coverage, never a fault of the build.
|
|
23
|
+
|
|
24
|
+
**Mechanics.** Read `guidelines-baseline.json`, then fetch the live page and diff its section
|
|
25
|
+
numbers against `all_sections`. The fetcher's sub-model truncates this single long page after
|
|
26
|
+
~5.4, so do **two focused passes**, each embedding only the relevant slice of `all_sections`:
|
|
27
|
+
|
|
28
|
+
- **Pass A (Sections 1–3):** "Report ONLY (NEW) section numbers present on the live page but
|
|
29
|
+
missing from this list, and (REMOVED) numbers in the list but absent. Ignore parenthetical
|
|
30
|
+
(a)/(b) suffixes. If nothing differs, output NO DRIFT."
|
|
31
|
+
- **Pass B (Section 4 + 5.1–5.4):** same, plus "The page reliably truncates after ~5.4, that is
|
|
32
|
+
EXPECTED; do NOT report 5.5/5.6.x as removed and do NOT output TRUNCATED for the tail."
|
|
33
|
+
- **Tail 5.5–5.6.x** can't be fetched reliably and is structurally the most stable section
|
|
34
|
+
(Developer Code of Conduct); review it by hand during reconciliation.
|
|
35
|
+
|
|
36
|
+
**Interpretation (always WARN at most):**
|
|
37
|
+
|
|
38
|
+
- `NO DRIFT` → `PASS: guideline-drift none (baseline reconciled <reconciled_on>)`
|
|
39
|
+
- NEW/REMOVED → `WARN: guideline-drift — NEW: <…> / REMOVED: <…>. Review the live page; add a
|
|
40
|
+
check to scan.sh if relevant, then reconcile the baseline.`
|
|
41
|
+
- fetch failed/truncated → `WARN: guideline-drift-check degraded — verify manually.`
|
|
42
|
+
|
|
43
|
+
**Reconciliation (a deliberate human step):** review the drift, optionally add a scan check for a
|
|
44
|
+
newly relevant section, then update `guidelines-baseline.json`, add/remove the section numbers
|
|
45
|
+
in `all_sections` and set `reconciled_on` to today. **Never auto-update the baseline:** doing so
|
|
46
|
+
would silently swallow the warning and defeat the entire purpose of drift detection.
|
|
47
|
+
|
|
48
|
+
**Limit:** this detects only *structural* drift (the set of section numbers). A section whose
|
|
49
|
+
number is unchanged but whose *text* changed is not caught here, but is partly covered by
|
|
50
|
+
Phase 2 (Apple's own rule engine) and Phase 3 (the adversarial reviewer). Apple does not expose a
|
|
51
|
+
machine-readable "last updated" date in the page DOM, so the section-number set is the signal.
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Phase 1: Rejection vectors
|
|
56
|
+
|
|
57
|
+
`scripts/scan.sh` checks the following. Each emits `FAIL:` / `WARN:` / `PASS:` with a location.
|
|
58
|
+
|
|
59
|
+
| # | Guideline | What it checks |
|
|
60
|
+
|---|-----------|----------------|
|
|
61
|
+
| 1 | **5.1.1(v) Privacy Manifest** | Required Reason API usage (`UserDefaults`/`@AppStorage`, file timestamp, system boot time, disk space, active keyboard) ↔ `PrivacyInfo.xcprivacy` declaration parity |
|
|
62
|
+
| 2 | **5.1.1 Purpose Strings** | Every imported sensitive framework (FamilyControls, CoreLocation, AVFoundation, Photos, Contacts, HealthKit) has a non-empty `NS*UsageDescription` in Info.plist |
|
|
63
|
+
| 3 | **5.1.2 ATT** | If `AppTrackingTransparency`/`ATTrackingManager` is used, `NSUserTrackingUsageDescription` is present |
|
|
64
|
+
| 4 | **2.3.10 Other platforms** | No "Android" / "Google Play" / competitor store names in store metadata |
|
|
65
|
+
| 5 | **2.3.1 Metadata limits** | name ≤30, subtitle ≤30, keywords ≤100, promotional_text ≤170, description ≤4000 (Unicode codepoints, matching ASC) |
|
|
66
|
+
| 6 | **2.3.7 Localized parity** | Every detected locale has name + subtitle + description + keywords |
|
|
67
|
+
| 7 | **2.3.3 Screenshots** | Each locale folder has at least one screenshot (warns if <3) |
|
|
68
|
+
| 8 | **3.1.2 Trial disclosure** | If trial wording exists, a trial→paid auto-renew disclosure key exists |
|
|
69
|
+
| 9 | **3.1.2 Auto-renew disclosure** | A subscription disclosure string exists and covers each locale |
|
|
70
|
+
| 10 | **3.1.2 Required links** | The paywall view contains Restore Purchases + Terms of Use (EULA) + Privacy Policy |
|
|
71
|
+
| 11 | **2.5.1 Private API** | No banned identifiers (`UIWebView`, `setSelectionIndicatorImage`, `_UIBackdropView`, `NSURLConnection`, …) |
|
|
72
|
+
| 12 | **4.0 Minimum functionality** | At least one navigation hub (`TabView` / `NavigationStack` / `NavigationSplitView`) |
|
|
73
|
+
| 13 | **5.1.5 Sensitive APIs** *(opt-in)* | If FamilyControls is used and `optionalChecks.familyControls` is on, a reviewer-notes justification exists |
|
|
74
|
+
| 14 | **4.8 Sign in with Apple** *(advisory)* | If a third-party social login SDK (Google, Facebook, Auth0, …) is used, Sign in with Apple is offered too |
|
|
75
|
+
| 15 | **3.1.1(a) External purchase link** *(advisory)* | If StoreKit External Purchase APIs or the entitlement are present, the 3.1.1(a) disclosure/reporting requirements are flagged |
|
|
76
|
+
| 16 | **5.1.2 Tracking SDK / IDFA** *(advisory)* | If an ad / attribution SDK (AdMob, AppLovin, AppsFlyer, Adjust, Branch, IronSource) or raw IDFA access is present but no ATT prompt is, it is flagged (the reverse of vector 3) |
|
|
77
|
+
| 17 | **Export compliance** *(advisory)* | If a checked-in Info.plist lacks `ITSAppUsesNonExemptEncryption`, set it (true/false) to skip the App Store Connect encryption-export question |
|
|
78
|
+
| 18 | **2.3 Support / Privacy URL** *(advisory)* | fastlane metadata has a non-empty `support_url.txt` and `privacy_url.txt` across locales, with no placeholder URLs |
|
|
79
|
+
| 19 | **5.1.1 Privacy manifest** *(advisory)* | If an analytics SDK (Firebase, Amplitude, Mixpanel, Sentry, Segment, Bugsnag, App Center, Datadog) is linked but `PrivacyInfo.xcprivacy` declares no collected data types or tracking domains, it is flagged |
|
|
80
|
+
| 20 | **2.1 Placeholder content** *(advisory)* | No lorem ipsum / TODO / FIXME / `example.com` / "insert X here" / changeme in store metadata |
|
|
81
|
+
|
|
82
|
+
Vectors 8–10 only run when in-app-purchase signals are detected (StoreKit / RevenueCat import,
|
|
83
|
+
or a paywall view). Otherwise the scanner emits a single PASS and skips them. Vectors 16–20 are
|
|
84
|
+
signal-gated advisory WARNs: each emits nothing unless its triggering signal is present.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Auto-detection rules
|
|
89
|
+
|
|
90
|
+
When `.appstore-precheck.json` does not pin a path, the scanner derives it:
|
|
91
|
+
|
|
92
|
+
- **iOS source dir**: the `Info.plist` directory (excluding `.git`, Pods, Carthage, `.build`,
|
|
93
|
+
`build`, DerivedData, SwiftPM `SourcePackages`/`checkouts`/`.swiftmp`, `.claude`/`worktrees`,
|
|
94
|
+
`node_modules`, `vendor`) that holds the most Swift files. This is the app target, not a dependency.
|
|
95
|
+
- **metadata / screenshots dir**: the shallowest `**/fastlane/metadata` and `**/fastlane/screenshots`.
|
|
96
|
+
- **String Catalog**: the first `Localizable.xcstrings`, else the first `*.xcstrings`.
|
|
97
|
+
- **Paywall view**: first file matching `*SubscriptionView*`, `*PaywallView*`, `*Paywall*`,
|
|
98
|
+
`*…View.swift` (override with `paywallGlobs`).
|
|
99
|
+
- **Locales**: the directory names under `metadataDir` (override with a `locales` array).
|
|
100
|
+
|
|
101
|
+
All path matching prunes the dependency/build/worktree trees above and prefers the shallowest
|
|
102
|
+
match, so a vendored SwiftPM checkout is never mistaken for the app.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Verdict thresholds
|
|
107
|
+
|
|
108
|
+
| State | Rule |
|
|
109
|
+
|-------|------|
|
|
110
|
+
| **GREEN** | 0 FAIL and ≤2 WARN → write `.precheck-pass` (valid 60 min) |
|
|
111
|
+
| **YELLOW** | 0 FAIL and ≥3 WARN → no token; require explicit user confirmation to proceed |
|
|
112
|
+
| **RED** | ≥1 FAIL → no token; submission blocked until fixed |
|
|
113
|
+
|
|
114
|
+
The guideline-drift WARN from Phase 0 counts toward the same WARN threshold; on its own it never
|
|
115
|
+
blocks, but it can be the third WARN that tips GREEN into YELLOW.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Pre-submit manual checklist
|
|
120
|
+
|
|
121
|
+
Things the scanner cannot verify; confirm by hand before you submit:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
[ ] App Privacy Nutrition Labels in App Store Connect match PrivacyInfo.xcprivacy
|
|
125
|
+
[ ] ASC App Review Information: demo account / notes filled in
|
|
126
|
+
[ ] Sandbox tester: at least 1 purchase + 1 restore tested end to end
|
|
127
|
+
[ ] TestFlight crash-free over the last 24h > 99.5%
|
|
128
|
+
[ ] Build number incremented from the previous submission
|
|
129
|
+
[ ] Export compliance (encryption) answered
|
|
130
|
+
[ ] If using "Sign in with Apple" alongside other social logins, it is offered
|
|
131
|
+
```
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# appstore-precheck/scripts/phase2-precheck.sh
|
|
3
|
+
# Phase 2 wrapper: build the App Store Connect API key JSON from the environment,
|
|
4
|
+
# run Apple's `fastlane precheck`, then delete the key. The key is created with a
|
|
5
|
+
# restrictive mode and removed on exit (even on failure), so it never lingers and
|
|
6
|
+
# is never committed.
|
|
7
|
+
#
|
|
8
|
+
# Env (required): ASC_KEY_ID, ASC_ISSUER_ID, ASC_P8_PATH (path to the .p8 file).
|
|
9
|
+
# App identifier: first positional arg, or $APP_IDENTIFIER.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# ASC_KEY_ID=... ASC_ISSUER_ID=... ASC_P8_PATH=/path/AuthKey.p8 \
|
|
13
|
+
# bash phase2-precheck.sh com.example.app
|
|
14
|
+
# bash phase2-precheck.sh --dry-run com.example.app # build+validate the key JSON
|
|
15
|
+
# # and print the command, no
|
|
16
|
+
# # network and no real key needed
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
DRY=false
|
|
20
|
+
APP_ID="${APP_IDENTIFIER:-}"
|
|
21
|
+
for arg in "$@"; do
|
|
22
|
+
case "$arg" in
|
|
23
|
+
--dry-run) DRY=true ;;
|
|
24
|
+
-*) echo "phase2-precheck.sh: unknown option '$arg'" >&2; exit 64 ;;
|
|
25
|
+
*) APP_ID="$arg" ;;
|
|
26
|
+
esac
|
|
27
|
+
done
|
|
28
|
+
|
|
29
|
+
: "${ASC_KEY_ID:?set ASC_KEY_ID (App Store Connect API key id)}"
|
|
30
|
+
: "${ASC_ISSUER_ID:?set ASC_ISSUER_ID (App Store Connect issuer id)}"
|
|
31
|
+
: "${ASC_P8_PATH:?set ASC_P8_PATH (path to the .p8 private key)}"
|
|
32
|
+
[[ -n "$APP_ID" ]] || { echo "usage: phase2-precheck.sh <app_identifier> (or set APP_IDENTIFIER)" >&2; exit 64; }
|
|
33
|
+
|
|
34
|
+
command -v jq >/dev/null 2>&1 || { echo "phase2-precheck.sh: jq is required" >&2; exit 69; }
|
|
35
|
+
|
|
36
|
+
KEYJSON="$(mktemp -t asc-key.XXXXXX)"
|
|
37
|
+
trap 'rm -f "$KEYJSON"' EXIT
|
|
38
|
+
|
|
39
|
+
# Build the key JSON. A real run reads the .p8 from disk; --dry-run tolerates a
|
|
40
|
+
# missing file (treats ASC_P8_PATH as literal key text) so it can be exercised in
|
|
41
|
+
# tests and previews without real credentials.
|
|
42
|
+
if [[ -f "$ASC_P8_PATH" ]]; then
|
|
43
|
+
jq -n --arg kid "$ASC_KEY_ID" --arg iss "$ASC_ISSUER_ID" --rawfile key "$ASC_P8_PATH" \
|
|
44
|
+
'{key_id:$kid, issuer_id:$iss, key:$key, in_house:false}' > "$KEYJSON"
|
|
45
|
+
elif $DRY; then
|
|
46
|
+
jq -n --arg kid "$ASC_KEY_ID" --arg iss "$ASC_ISSUER_ID" --arg key "$ASC_P8_PATH" \
|
|
47
|
+
'{key_id:$kid, issuer_id:$iss, key:$key, in_house:false}' > "$KEYJSON"
|
|
48
|
+
else
|
|
49
|
+
echo "phase2-precheck.sh: .p8 not found at $ASC_P8_PATH" >&2; exit 66
|
|
50
|
+
fi
|
|
51
|
+
chmod 600 "$KEYJSON"
|
|
52
|
+
|
|
53
|
+
# The verbatim Phase 2 command (IAP is covered by Phase 1, so it is excluded here).
|
|
54
|
+
CMD=( fastlane run precheck
|
|
55
|
+
"app_identifier:$APP_ID"
|
|
56
|
+
"api_key_path:$KEYJSON"
|
|
57
|
+
include_in_app_purchases:false
|
|
58
|
+
default_rule_level::error )
|
|
59
|
+
|
|
60
|
+
if $DRY; then
|
|
61
|
+
echo "DRY-RUN: would run: ${CMD[*]}"
|
|
62
|
+
jq -e '.key_id and .issuer_id and .key' "$KEYJSON" >/dev/null \
|
|
63
|
+
&& echo "key json OK" || { echo "key json INVALID" >&2; exit 1; }
|
|
64
|
+
exit 0
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
command -v fastlane >/dev/null 2>&1 || { echo "phase2-precheck.sh: fastlane is required" >&2; exit 69; }
|
|
68
|
+
"${CMD[@]}"
|