ios-app-review-plugin 1.0.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/.claude/settings.local.json +42 -0
- package/.github/actions/ios-review/action.yml +106 -0
- package/.github/workflows/ci.yml +103 -0
- package/.github/workflows/publish.yml +57 -0
- package/CHANGELOG.md +66 -0
- package/CONTRIBUTING.md +175 -0
- package/LICENSE +21 -0
- package/README.md +205 -0
- package/bitrise/step.sh +128 -0
- package/bitrise/step.yml +101 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzers/asc-iap.d.ts.map +1 -0
- package/dist/analyzers/asc-metadata.d.ts.map +1 -0
- package/dist/analyzers/asc-screenshots.d.ts.map +1 -0
- package/dist/analyzers/asc-version.d.ts.map +1 -0
- package/dist/analyzers/code-scanner.d.ts.map +1 -0
- package/dist/analyzers/deprecated-api.d.ts.map +1 -0
- package/dist/analyzers/entitlements.d.ts.map +1 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/info-plist.d.ts.map +1 -0
- package/dist/analyzers/privacy.d.ts.map +1 -0
- package/dist/analyzers/private-api.d.ts.map +1 -0
- package/dist/analyzers/security.d.ts.map +1 -0
- package/dist/analyzers/ui-ux.d.ts.map +1 -0
- package/dist/asc/auth.d.ts.map +1 -0
- package/dist/asc/client.d.ts.map +1 -0
- package/dist/asc/endpoints/apps.d.ts.map +1 -0
- package/dist/asc/endpoints/iap.d.ts.map +1 -0
- package/dist/asc/endpoints/screenshots.d.ts.map +1 -0
- package/dist/asc/endpoints/versions.d.ts.map +1 -0
- package/dist/asc/errors.d.ts.map +1 -0
- package/dist/asc/index.d.ts.map +1 -0
- package/dist/asc/types.d.ts.map +1 -0
- package/dist/badge/generator.d.ts.map +1 -0
- package/dist/badge/index.d.ts.map +1 -0
- package/dist/badge/types.d.ts.map +1 -0
- package/dist/cache/file-cache.d.ts.map +1 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cli/commands/help.d.ts.map +1 -0
- package/dist/cli/commands/scan.d.ts.map +1 -0
- package/dist/cli/commands/version.d.ts.map +1 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/git/diff.d.ts.map +1 -0
- package/dist/git/index.d.ts.map +1 -0
- package/dist/git/types.d.ts.map +1 -0
- package/dist/guidelines/database.d.ts.map +1 -0
- package/dist/guidelines/index.d.ts.map +1 -0
- package/dist/guidelines/matcher.d.ts.map +1 -0
- package/dist/guidelines/types.d.ts.map +1 -0
- package/dist/history/comparator.d.ts.map +1 -0
- package/dist/history/index.d.ts.map +1 -0
- package/dist/history/store.d.ts.map +1 -0
- package/dist/history/types.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +994 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/plist.d.ts.map +1 -0
- package/dist/parsers/xcodeproj.d.ts.map +1 -0
- package/dist/progress/index.d.ts.map +1 -0
- package/dist/progress/reporter.d.ts.map +1 -0
- package/dist/progress/types.d.ts.map +1 -0
- package/dist/reports/html.d.ts.map +1 -0
- package/dist/reports/index.d.ts.map +1 -0
- package/dist/reports/json.d.ts.map +1 -0
- package/dist/reports/markdown.d.ts.map +1 -0
- package/dist/reports/types.d.ts.map +1 -0
- package/dist/rules/engine.d.ts.map +1 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/loader.d.ts.map +1 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/types/index.d.ts.map +1 -0
- package/docs/ANALYZERS.md +237 -0
- package/docs/API.md +308 -0
- package/docs/BADGES.md +130 -0
- package/docs/CI_CD.md +283 -0
- package/docs/CLI.md +140 -0
- package/docs/REPORTS.md +212 -0
- package/docs/ROADMAP.md +267 -0
- package/docs/RULES.md +182 -0
- package/docs/SECURITY.md +89 -0
- package/docs/TROUBLESHOOTING.md +227 -0
- package/docs/tutorials/ASC_SETUP.md +188 -0
- package/docs/tutorials/CI_INTEGRATION.md +292 -0
- package/docs/tutorials/CUSTOM_RULES.md +291 -0
- package/docs/tutorials/GETTING_STARTED.md +226 -0
- package/docs/video-scripts/01-introduction.md +106 -0
- package/docs/video-scripts/02-cli-usage.md +120 -0
- package/docs/video-scripts/03-ci-integration.md +198 -0
- package/eslint.config.js +33 -0
- package/examples/.ios-review-rules.json +82 -0
- package/examples/bitrise-workflow.yml +129 -0
- package/examples/fastlane-lane.rb +71 -0
- package/examples/github-action.yml +147 -0
- package/fastlane/Fastfile.example +114 -0
- package/fastlane/README.md +99 -0
- package/jest.config.js +36 -0
- package/package.json +65 -0
- package/scripts/benchmark.ts +112 -0
- package/scripts/debug-parser.ts +37 -0
- package/scripts/debug-pbxproj.ts +36 -0
- package/scripts/debug-specific.ts +47 -0
- package/scripts/test-analyze.ts +67 -0
- package/scripts/xcode-cloud-review.sh +167 -0
- package/src/analyzer.ts +227 -0
- package/src/analyzers/asc-iap.ts +300 -0
- package/src/analyzers/asc-metadata.ts +326 -0
- package/src/analyzers/asc-screenshots.ts +310 -0
- package/src/analyzers/asc-version.ts +368 -0
- package/src/analyzers/code-scanner.ts +408 -0
- package/src/analyzers/deprecated-api.ts +390 -0
- package/src/analyzers/entitlements.ts +345 -0
- package/src/analyzers/index.ts +12 -0
- package/src/analyzers/info-plist.ts +409 -0
- package/src/analyzers/privacy.ts +376 -0
- package/src/analyzers/private-api.ts +377 -0
- package/src/analyzers/security.ts +327 -0
- package/src/analyzers/ui-ux.ts +509 -0
- package/src/asc/auth.ts +204 -0
- package/src/asc/client.ts +258 -0
- package/src/asc/endpoints/apps.ts +115 -0
- package/src/asc/endpoints/iap.ts +171 -0
- package/src/asc/endpoints/screenshots.ts +164 -0
- package/src/asc/endpoints/versions.ts +174 -0
- package/src/asc/errors.ts +109 -0
- package/src/asc/index.ts +108 -0
- package/src/asc/types.ts +369 -0
- package/src/badge/generator.ts +48 -0
- package/src/badge/index.ts +2 -0
- package/src/badge/types.ts +5 -0
- package/src/cache/file-cache.ts +75 -0
- package/src/cache/index.ts +2 -0
- package/src/cache/types.ts +10 -0
- package/src/cli/commands/help.ts +41 -0
- package/src/cli/commands/scan.ts +44 -0
- package/src/cli/commands/version.ts +12 -0
- package/src/cli/index.ts +92 -0
- package/src/cli/types.ts +17 -0
- package/src/git/diff.ts +21 -0
- package/src/git/index.ts +2 -0
- package/src/git/types.ts +5 -0
- package/src/guidelines/database.ts +344 -0
- package/src/guidelines/index.ts +4 -0
- package/src/guidelines/matcher.ts +84 -0
- package/src/guidelines/types.ts +28 -0
- package/src/history/comparator.ts +114 -0
- package/src/history/index.ts +3 -0
- package/src/history/store.ts +135 -0
- package/src/history/types.ts +40 -0
- package/src/index.ts +1113 -0
- package/src/parsers/index.ts +3 -0
- package/src/parsers/plist.ts +253 -0
- package/src/parsers/xcodeproj.ts +265 -0
- package/src/progress/index.ts +2 -0
- package/src/progress/reporter.ts +65 -0
- package/src/progress/types.ts +9 -0
- package/src/reports/html.ts +322 -0
- package/src/reports/index.ts +20 -0
- package/src/reports/json.ts +92 -0
- package/src/reports/markdown.ts +187 -0
- package/src/reports/types.ts +26 -0
- package/src/rules/engine.ts +121 -0
- package/src/rules/index.ts +3 -0
- package/src/rules/loader.ts +83 -0
- package/src/rules/types.ts +25 -0
- package/src/types/index.ts +247 -0
- package/tests/analyzer.test.ts +142 -0
- package/tests/analyzers/asc-iap.test.ts +228 -0
- package/tests/analyzers/asc-metadata.test.ts +210 -0
- package/tests/analyzers/asc-screenshots.test.ts +135 -0
- package/tests/analyzers/asc-version.test.ts +259 -0
- package/tests/analyzers/code-scanner.test.ts +745 -0
- package/tests/analyzers/deprecated-api.test.ts +286 -0
- package/tests/analyzers/entitlements.test.ts +411 -0
- package/tests/analyzers/info-plist.test.ts +148 -0
- package/tests/analyzers/privacy.test.ts +623 -0
- package/tests/analyzers/private-api.test.ts +255 -0
- package/tests/analyzers/security.test.ts +300 -0
- package/tests/analyzers/ui-ux.test.ts +357 -0
- package/tests/asc/auth.test.ts +189 -0
- package/tests/asc/client.test.ts +207 -0
- package/tests/asc/endpoints.test.ts +1359 -0
- package/tests/badge/generator.test.ts +73 -0
- package/tests/cache/file-cache.test.ts +124 -0
- package/tests/cli/cli-index.test.ts +510 -0
- package/tests/cli/commands.test.ts +67 -0
- package/tests/cli/scan.test.ts +152 -0
- package/tests/git/diff.test.ts +69 -0
- package/tests/guidelines/matcher.test.ts +209 -0
- package/tests/history/comparator.test.ts +272 -0
- package/tests/history/store.test.ts +200 -0
- package/tests/integration/cli.test.ts +95 -0
- package/tests/integration/e2e.test.ts +130 -0
- package/tests/parsers/plist.test.ts +240 -0
- package/tests/parsers/xcodeproj.test.ts +289 -0
- package/tests/progress/reporter.test.ts +117 -0
- package/tests/reports/html.test.ts +176 -0
- package/tests/reports/json.test.ts +235 -0
- package/tests/reports/markdown.test.ts +196 -0
- package/tests/rules/engine.test.ts +229 -0
- package/tests/rules/loader.test.ts +187 -0
- package/tests/setup.ts +15 -0
- package/tsconfig.json +27 -0
- package/tsconfig.test.json +9 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# CI Integration Tutorial
|
|
2
|
+
|
|
3
|
+
This tutorial walks through setting up automated App Store review checks in your CI pipeline, step by step.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The goal: every pull request runs the review checker automatically, blocks merges when errors are found, and tracks your score over time.
|
|
8
|
+
|
|
9
|
+
**Time to set up:** 10-15 minutes per platform.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## GitHub Actions (Recommended)
|
|
14
|
+
|
|
15
|
+
### Step 1: Create the Workflow File
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
mkdir -p .github/workflows
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Create `.github/workflows/app-review.yml`:
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
name: App Store Review Check
|
|
25
|
+
|
|
26
|
+
on:
|
|
27
|
+
pull_request:
|
|
28
|
+
branches: [main, develop]
|
|
29
|
+
push:
|
|
30
|
+
branches: [main]
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
review-check:
|
|
34
|
+
runs-on: macos-latest
|
|
35
|
+
timeout-minutes: 10
|
|
36
|
+
|
|
37
|
+
steps:
|
|
38
|
+
- name: Checkout
|
|
39
|
+
uses: actions/checkout@v4
|
|
40
|
+
with:
|
|
41
|
+
fetch-depth: 0 # needed for --changed-since
|
|
42
|
+
|
|
43
|
+
- name: Setup Node.js
|
|
44
|
+
uses: actions/setup-node@v4
|
|
45
|
+
with:
|
|
46
|
+
node-version: '20'
|
|
47
|
+
cache: 'npm'
|
|
48
|
+
|
|
49
|
+
- name: Install ios-app-review
|
|
50
|
+
run: npm install -g ios-app-review-plugin
|
|
51
|
+
|
|
52
|
+
- name: Run review check
|
|
53
|
+
run: |
|
|
54
|
+
ios-app-review scan ./MyApp.xcodeproj \
|
|
55
|
+
--format json \
|
|
56
|
+
--output report.json \
|
|
57
|
+
--badge \
|
|
58
|
+
--save-history
|
|
59
|
+
|
|
60
|
+
- name: Upload report
|
|
61
|
+
if: always()
|
|
62
|
+
uses: actions/upload-artifact@v4
|
|
63
|
+
with:
|
|
64
|
+
name: review-report
|
|
65
|
+
path: |
|
|
66
|
+
report.json
|
|
67
|
+
badge.svg
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Step 2: Test It
|
|
71
|
+
|
|
72
|
+
Push the workflow file and open a pull request. The check appears under the PR's "Checks" tab.
|
|
73
|
+
|
|
74
|
+
### Step 3: Make It a Required Check
|
|
75
|
+
|
|
76
|
+
1. Go to Settings > Branches > Branch protection rules.
|
|
77
|
+
2. Edit the rule for `main`.
|
|
78
|
+
3. Enable "Require status checks to pass before merging".
|
|
79
|
+
4. Search for "App Store Review Check" and add it.
|
|
80
|
+
|
|
81
|
+
### Step 4: Add Incremental Scanning for PRs
|
|
82
|
+
|
|
83
|
+
For faster PR checks, scan only changed files:
|
|
84
|
+
|
|
85
|
+
```yaml
|
|
86
|
+
- name: Run incremental review
|
|
87
|
+
if: github.event_name == 'pull_request'
|
|
88
|
+
run: |
|
|
89
|
+
ios-app-review scan ./MyApp.xcodeproj \
|
|
90
|
+
--changed-since origin/${{ github.base_ref }} \
|
|
91
|
+
--format json \
|
|
92
|
+
--output report.json
|
|
93
|
+
|
|
94
|
+
- name: Run full review
|
|
95
|
+
if: github.event_name == 'push'
|
|
96
|
+
run: |
|
|
97
|
+
ios-app-review scan ./MyApp.xcodeproj \
|
|
98
|
+
--format json \
|
|
99
|
+
--output report.json \
|
|
100
|
+
--save-history
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Step 5: Add ASC Validation (Optional)
|
|
104
|
+
|
|
105
|
+
1. In your repo, go to Settings > Secrets and variables > Actions.
|
|
106
|
+
2. Add secrets: `ASC_KEY_ID`, `ASC_ISSUER_ID`, `ASC_PRIVATE_KEY` (paste the `.p8` file contents).
|
|
107
|
+
3. Update the workflow:
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
- name: Run full review with ASC
|
|
111
|
+
env:
|
|
112
|
+
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
|
113
|
+
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
|
114
|
+
ASC_PRIVATE_KEY_PATH: ${{ runner.temp }}/AuthKey.p8
|
|
115
|
+
run: |
|
|
116
|
+
echo "${{ secrets.ASC_PRIVATE_KEY }}" > "$ASC_PRIVATE_KEY_PATH"
|
|
117
|
+
ios-app-review scan ./MyApp.xcodeproj \
|
|
118
|
+
--include-asc \
|
|
119
|
+
--format json \
|
|
120
|
+
--output report.json
|
|
121
|
+
rm "$ASC_PRIVATE_KEY_PATH"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Fastlane
|
|
127
|
+
|
|
128
|
+
### Step 1: Add a Review Lane
|
|
129
|
+
|
|
130
|
+
Edit `fastlane/Fastfile`:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
desc "Check App Store review compliance"
|
|
134
|
+
lane :review_check do |options|
|
|
135
|
+
format = options[:format] || "json"
|
|
136
|
+
output = options[:output] || "../review-report.json"
|
|
137
|
+
|
|
138
|
+
sh("ios-app-review scan ../MyApp.xcodeproj --format #{format} --output #{output}")
|
|
139
|
+
|
|
140
|
+
if format == "json"
|
|
141
|
+
report = JSON.parse(File.read(output))
|
|
142
|
+
score = report["score"]
|
|
143
|
+
errors = report["summary"]["errors"]
|
|
144
|
+
|
|
145
|
+
UI.header("App Store Review Results")
|
|
146
|
+
UI.message("Score: #{score}/100")
|
|
147
|
+
UI.message("Errors: #{errors}")
|
|
148
|
+
|
|
149
|
+
if errors > 0
|
|
150
|
+
UI.user_error!("Review check failed with #{errors} error(s)")
|
|
151
|
+
else
|
|
152
|
+
UI.success("Review check passed!")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Step 2: Wire It Into Your Build Lane
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
lane :beta do
|
|
162
|
+
review_check
|
|
163
|
+
build_app(scheme: "MyApp")
|
|
164
|
+
upload_to_testflight
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
lane :release do
|
|
168
|
+
review_check(format: "html", output: "../review-report.html")
|
|
169
|
+
build_app(scheme: "MyApp")
|
|
170
|
+
upload_to_app_store
|
|
171
|
+
end
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Step 3: Run It
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
bundle exec fastlane review_check
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Bitrise
|
|
183
|
+
|
|
184
|
+
### Step 1: Add Steps to bitrise.yml
|
|
185
|
+
|
|
186
|
+
```yaml
|
|
187
|
+
workflows:
|
|
188
|
+
primary:
|
|
189
|
+
steps:
|
|
190
|
+
- git-clone@8: {}
|
|
191
|
+
|
|
192
|
+
- nvm@1:
|
|
193
|
+
inputs:
|
|
194
|
+
- node_version: "20"
|
|
195
|
+
|
|
196
|
+
- script@1:
|
|
197
|
+
title: Install and run ios-app-review
|
|
198
|
+
inputs:
|
|
199
|
+
- content: |
|
|
200
|
+
npm install -g ios-app-review-plugin
|
|
201
|
+
ios-app-review scan ./MyApp.xcodeproj \
|
|
202
|
+
--format json \
|
|
203
|
+
--output "$BITRISE_DEPLOY_DIR/review-report.json" \
|
|
204
|
+
--badge
|
|
205
|
+
cp badge.svg "$BITRISE_DEPLOY_DIR/" 2>/dev/null || true
|
|
206
|
+
|
|
207
|
+
- deploy-to-bitrise-io@2: {}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Step 2: Add Secrets
|
|
211
|
+
|
|
212
|
+
In Bitrise workflow editor, go to Secrets and add `ASC_KEY_ID`, `ASC_ISSUER_ID`, and the base64-encoded private key as `ASC_PRIVATE_KEY_B64`.
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
- script@1:
|
|
216
|
+
title: Setup ASC credentials
|
|
217
|
+
inputs:
|
|
218
|
+
- content: |
|
|
219
|
+
echo "$ASC_PRIVATE_KEY_B64" | base64 -d > /tmp/AuthKey.p8
|
|
220
|
+
export ASC_PRIVATE_KEY_PATH=/tmp/AuthKey.p8
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## Xcode Cloud
|
|
226
|
+
|
|
227
|
+
### Step 1: Create the Script
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
mkdir -p ci_scripts
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Create `ci_scripts/ci_post_clone.sh`:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
#!/bin/bash
|
|
237
|
+
set -e
|
|
238
|
+
|
|
239
|
+
# Install Node.js
|
|
240
|
+
brew install node 2>/dev/null || true
|
|
241
|
+
|
|
242
|
+
# Install the tool
|
|
243
|
+
npm install -g ios-app-review-plugin
|
|
244
|
+
|
|
245
|
+
# Run scan
|
|
246
|
+
ios-app-review scan "$CI_PRIMARY_REPOSITORY_PATH/MyApp.xcodeproj" \
|
|
247
|
+
--format json \
|
|
248
|
+
--output "$CI_PRIMARY_REPOSITORY_PATH/review-report.json"
|
|
249
|
+
|
|
250
|
+
# Read results
|
|
251
|
+
ERRORS=$(python3 -c "import json; print(json.load(open('$CI_PRIMARY_REPOSITORY_PATH/review-report.json'))['summary']['errors'])")
|
|
252
|
+
|
|
253
|
+
if [ "$ERRORS" -gt 0 ]; then
|
|
254
|
+
echo "error: App Store review check failed with $ERRORS error(s)"
|
|
255
|
+
exit 1
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
echo "App Store review check passed"
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Step 2: Make It Executable
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
chmod +x ci_scripts/ci_post_clone.sh
|
|
265
|
+
git add ci_scripts/
|
|
266
|
+
git commit -m "Add App Store review CI check"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Step 3: Configure in Xcode Cloud
|
|
270
|
+
|
|
271
|
+
The script runs automatically during the post-clone phase. No additional Xcode Cloud configuration is needed beyond ensuring the script is in the `ci_scripts/` directory.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## Verifying Your Setup
|
|
276
|
+
|
|
277
|
+
After setting up on any platform:
|
|
278
|
+
|
|
279
|
+
1. **Introduce a deliberate error** -- add `UIWebView()` somewhere in your code.
|
|
280
|
+
2. **Push and trigger the CI.**
|
|
281
|
+
3. **Verify the check fails** with exit code 1.
|
|
282
|
+
4. **Remove the error and push again.**
|
|
283
|
+
5. **Verify the check passes** with exit code 0.
|
|
284
|
+
|
|
285
|
+
## Best Practices
|
|
286
|
+
|
|
287
|
+
1. **Start with warnings as non-blocking.** Only fail the build on errors. The exit code already does this.
|
|
288
|
+
2. **Use incremental scanning** on PRs to keep checks fast (< 10 seconds for typical diffs).
|
|
289
|
+
3. **Run full scans on main** with `--save-history` to track trends.
|
|
290
|
+
4. **Generate HTML reports** for release branches -- they are easier for non-developers to read.
|
|
291
|
+
5. **Cache the npm install** to avoid re-downloading on every build.
|
|
292
|
+
6. **Keep ASC credentials in secrets**, never in the repo.
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# Custom Rules Tutorial
|
|
2
|
+
|
|
3
|
+
This tutorial walks through creating, testing, and sharing custom rules for your team.
|
|
4
|
+
|
|
5
|
+
## Why Custom Rules?
|
|
6
|
+
|
|
7
|
+
Built-in analyzers catch universal App Store issues. Custom rules let you enforce:
|
|
8
|
+
|
|
9
|
+
- Team coding conventions (no force casts, required copyright headers)
|
|
10
|
+
- Project-specific patterns (no direct Core Data access outside the data layer)
|
|
11
|
+
- Compliance requirements (no logging PII, required audit comments)
|
|
12
|
+
- Migration tracking (flag old API wrappers that should be replaced)
|
|
13
|
+
|
|
14
|
+
## Step 1: Create the Config File
|
|
15
|
+
|
|
16
|
+
Create `.ios-review-rules.json` in your project root (the directory containing your `.xcodeproj`):
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"version": 1,
|
|
21
|
+
"rules": [],
|
|
22
|
+
"disabledRules": [],
|
|
23
|
+
"severityOverrides": {}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Step 2: Write Your First Rule
|
|
28
|
+
|
|
29
|
+
Add a rule that flags `as!` force casts:
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"version": 1,
|
|
34
|
+
"rules": [
|
|
35
|
+
{
|
|
36
|
+
"id": "team-no-force-cast",
|
|
37
|
+
"title": "Force cast detected",
|
|
38
|
+
"description": "Force casts (as!) can cause crashes. Use conditional casting (as?) instead.",
|
|
39
|
+
"severity": "warning",
|
|
40
|
+
"pattern": "\\bas!\\s",
|
|
41
|
+
"flags": "g",
|
|
42
|
+
"fileTypes": [".swift"],
|
|
43
|
+
"suggestion": "Replace 'as!' with 'as?' and handle the optional."
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Step 3: Test the Rule
|
|
50
|
+
|
|
51
|
+
Run a scan to see it in action:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
ios-app-review scan ./MyApp.xcodeproj --config .ios-review-rules.json
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or validate the config without running a full scan (via MCP):
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
Validate my custom rules at ./MyApp.xcodeproj
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Step 4: Add More Rules
|
|
64
|
+
|
|
65
|
+
Here are practical rules you can adapt:
|
|
66
|
+
|
|
67
|
+
### Require Logger Instead of print
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"id": "team-use-logger",
|
|
72
|
+
"title": "Use Logger instead of print",
|
|
73
|
+
"description": "Use os.Logger for structured logging instead of print().",
|
|
74
|
+
"severity": "warning",
|
|
75
|
+
"pattern": "\\bprint\\s*\\(",
|
|
76
|
+
"flags": "g",
|
|
77
|
+
"fileTypes": [".swift"],
|
|
78
|
+
"suggestion": "Import os and use Logger.log() for production-appropriate logging."
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Flag Direct UserDefaults Access
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"id": "team-no-direct-userdefaults",
|
|
87
|
+
"title": "Direct UserDefaults access",
|
|
88
|
+
"description": "Use AppSettings wrapper instead of accessing UserDefaults directly.",
|
|
89
|
+
"severity": "warning",
|
|
90
|
+
"pattern": "UserDefaults\\.standard",
|
|
91
|
+
"flags": "g",
|
|
92
|
+
"fileTypes": [".swift"],
|
|
93
|
+
"suggestion": "Use the AppSettings singleton for consistent defaults access."
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Require Accessibility Labels on Buttons
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"id": "team-button-accessibility",
|
|
102
|
+
"title": "Button without accessibility setup",
|
|
103
|
+
"description": "UIButton created without accessibility label in the same scope.",
|
|
104
|
+
"severity": "info",
|
|
105
|
+
"pattern": "UIButton\\(",
|
|
106
|
+
"flags": "g",
|
|
107
|
+
"fileTypes": [".swift"],
|
|
108
|
+
"suggestion": "Set accessibilityLabel on all interactive UI elements."
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Flag Hardcoded Colors
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"id": "team-no-hardcoded-colors",
|
|
117
|
+
"title": "Hardcoded UIColor",
|
|
118
|
+
"description": "Use named colors from the asset catalog for consistent theming and dark mode support.",
|
|
119
|
+
"severity": "info",
|
|
120
|
+
"pattern": "UIColor\\(red:|UIColor\\(white:|#colorLiteral",
|
|
121
|
+
"flags": "g",
|
|
122
|
+
"fileTypes": [".swift"],
|
|
123
|
+
"suggestion": "Use UIColor(named:) with colors defined in Assets.xcassets."
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Detect Missing Error Handling
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"id": "team-no-try-bang",
|
|
132
|
+
"title": "try! without error handling",
|
|
133
|
+
"description": "try! will crash if the operation throws. Use do/catch instead.",
|
|
134
|
+
"severity": "warning",
|
|
135
|
+
"pattern": "\\btry!\\s",
|
|
136
|
+
"flags": "g",
|
|
137
|
+
"fileTypes": [".swift"],
|
|
138
|
+
"suggestion": "Wrap in do/catch block or use try? if the error can be safely ignored."
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Step 5: Override Built-in Severities
|
|
143
|
+
|
|
144
|
+
Promote built-in warnings to errors or demote info items:
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"version": 1,
|
|
149
|
+
"rules": [],
|
|
150
|
+
"severityOverrides": {
|
|
151
|
+
"hardcoded-ipv4": "error",
|
|
152
|
+
"insecure-http": "error",
|
|
153
|
+
"print-statement": "warning",
|
|
154
|
+
"todo-comment": "warning"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
This makes hardcoded IPs and HTTP URLs fail the build (error), and promotes print statements and TODOs to warnings.
|
|
160
|
+
|
|
161
|
+
## Step 6: Disable Noisy Rules
|
|
162
|
+
|
|
163
|
+
If certain built-in rules generate too much noise for your project:
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
{
|
|
167
|
+
"version": 1,
|
|
168
|
+
"rules": [],
|
|
169
|
+
"disabledRules": [
|
|
170
|
+
"force-unwrap",
|
|
171
|
+
"debug-ifdef",
|
|
172
|
+
"print-statement"
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Step 7: Use Inline Suppression
|
|
178
|
+
|
|
179
|
+
When a rule fires on a legitimate use:
|
|
180
|
+
|
|
181
|
+
```swift
|
|
182
|
+
// ios-review-disable-next-line team-no-force-cast
|
|
183
|
+
let vc = storyboard.instantiateViewController(withIdentifier: "Main") as! MainViewController
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Suppress multiple rules on one line:
|
|
187
|
+
|
|
188
|
+
```swift
|
|
189
|
+
// ios-review-disable-next-line team-no-force-cast, team-no-try-bang
|
|
190
|
+
let data = try! JSONEncoder().encode(model as! Encodable)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Step 8: Share Rules Across Projects
|
|
194
|
+
|
|
195
|
+
### Monorepo Approach
|
|
196
|
+
|
|
197
|
+
Place `.ios-review-rules.json` in the repository root. The rule loader walks up from the project directory, so all projects in the repo share the same rules.
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
monorepo/
|
|
201
|
+
.ios-review-rules.json <-- shared rules
|
|
202
|
+
AppA/
|
|
203
|
+
AppA.xcodeproj
|
|
204
|
+
AppB/
|
|
205
|
+
AppB.xcodeproj
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Per-project Overrides
|
|
209
|
+
|
|
210
|
+
Place a project-specific config that takes precedence:
|
|
211
|
+
|
|
212
|
+
```
|
|
213
|
+
monorepo/
|
|
214
|
+
.ios-review-rules.json <-- base rules
|
|
215
|
+
AppA/
|
|
216
|
+
.ios-review-rules.json <-- AppA overrides (found first)
|
|
217
|
+
AppA.xcodeproj
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### External Config
|
|
221
|
+
|
|
222
|
+
Point to any file path:
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
ios-app-review scan ./MyApp.xcodeproj --config ~/team-configs/strict-rules.json
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Complete Example Config
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"version": 1,
|
|
233
|
+
"rules": [
|
|
234
|
+
{
|
|
235
|
+
"id": "team-no-force-cast",
|
|
236
|
+
"title": "Force cast detected",
|
|
237
|
+
"description": "Force casts (as!) can cause crashes at runtime.",
|
|
238
|
+
"severity": "warning",
|
|
239
|
+
"pattern": "\\bas!\\s",
|
|
240
|
+
"flags": "g",
|
|
241
|
+
"fileTypes": [".swift"],
|
|
242
|
+
"suggestion": "Use 'as?' with optional binding instead."
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"id": "team-no-try-bang",
|
|
246
|
+
"title": "try! without error handling",
|
|
247
|
+
"description": "try! crashes on throw. Use do/catch.",
|
|
248
|
+
"severity": "warning",
|
|
249
|
+
"pattern": "\\btry!\\s",
|
|
250
|
+
"flags": "g",
|
|
251
|
+
"fileTypes": [".swift"],
|
|
252
|
+
"suggestion": "Wrap in do/catch or use try?."
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"id": "team-use-logger",
|
|
256
|
+
"title": "Use Logger instead of print",
|
|
257
|
+
"description": "print() output is not structured and cannot be filtered.",
|
|
258
|
+
"severity": "warning",
|
|
259
|
+
"pattern": "\\bprint\\s*\\(",
|
|
260
|
+
"flags": "g",
|
|
261
|
+
"fileTypes": [".swift"],
|
|
262
|
+
"suggestion": "Use os.Logger for production logging."
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"id": "team-copyright",
|
|
266
|
+
"title": "Missing copyright header",
|
|
267
|
+
"description": "All Swift files must include the team copyright header.",
|
|
268
|
+
"severity": "info",
|
|
269
|
+
"pattern": "^(?!.*Copyright.*ACME)",
|
|
270
|
+
"flags": "",
|
|
271
|
+
"fileTypes": [".swift"],
|
|
272
|
+
"suggestion": "Add '// Copyright ACME Corp' to the file header."
|
|
273
|
+
}
|
|
274
|
+
],
|
|
275
|
+
"disabledRules": [
|
|
276
|
+
"force-unwrap"
|
|
277
|
+
],
|
|
278
|
+
"severityOverrides": {
|
|
279
|
+
"insecure-http": "error",
|
|
280
|
+
"hardcoded-ipv4": "error"
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Debugging Tips
|
|
286
|
+
|
|
287
|
+
1. **Validate first:** Use `validate_custom_rules` before running full scans.
|
|
288
|
+
2. **Test regex in JS:** Open a browser console and test `new RegExp("your-pattern", "g").test("sample code")`.
|
|
289
|
+
3. **Start with `info` severity** while tuning patterns, then promote to `warning` or `error`.
|
|
290
|
+
4. **Check escaping:** JSON requires `\\` for regex backslashes. `\b` in regex becomes `"\\b"` in JSON.
|
|
291
|
+
5. **Limit scope with fileTypes:** Avoid false positives in headers or Objective-C by restricting to `.swift`.
|