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,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Store Connect Version Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Validates version information including local vs ASC version comparison,
|
|
5
|
+
* build numbers, version format, and submission status.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
Analyzer,
|
|
10
|
+
AnalysisResult,
|
|
11
|
+
AnalyzerOptions,
|
|
12
|
+
Issue,
|
|
13
|
+
XcodeProject,
|
|
14
|
+
} from '../types/index.js';
|
|
15
|
+
import {
|
|
16
|
+
hasCredentials,
|
|
17
|
+
getAppByBundleId,
|
|
18
|
+
getLatestVersion,
|
|
19
|
+
getLatestBuild,
|
|
20
|
+
getEditableVersion,
|
|
21
|
+
getVersionWithLocalizations,
|
|
22
|
+
isASCError,
|
|
23
|
+
type AppStoreState,
|
|
24
|
+
} from '../asc/index.js';
|
|
25
|
+
import { parsePlist } from '../parsers/plist.js';
|
|
26
|
+
import type { InfoPlist } from '../types/index.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Version states that indicate issues
|
|
30
|
+
*/
|
|
31
|
+
const REJECTED_STATES: AppStoreState[] = ['REJECTED', 'METADATA_REJECTED', 'INVALID_BINARY', 'DEVELOPER_REJECTED'];
|
|
32
|
+
const IN_REVIEW_STATES: AppStoreState[] = ['WAITING_FOR_REVIEW', 'IN_REVIEW'];
|
|
33
|
+
const PENDING_STATES: AppStoreState[] = ['PENDING_APPLE_RELEASE', 'PENDING_DEVELOPER_RELEASE', 'PENDING_CONTRACT'];
|
|
34
|
+
|
|
35
|
+
export class ASCVersionAnalyzer implements Analyzer {
|
|
36
|
+
name = 'ASC Version Analyzer';
|
|
37
|
+
description = 'Validates version and build information against App Store Connect';
|
|
38
|
+
|
|
39
|
+
async analyze(project: XcodeProject, options?: AnalyzerOptions): Promise<AnalysisResult> {
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
const issues: Issue[] = [];
|
|
42
|
+
|
|
43
|
+
if (!hasCredentials()) {
|
|
44
|
+
issues.push({
|
|
45
|
+
id: 'asc-credentials-not-configured',
|
|
46
|
+
title: 'App Store Connect credentials not configured',
|
|
47
|
+
description:
|
|
48
|
+
'ASC credentials are not configured. Set environment variables to enable version validation.',
|
|
49
|
+
severity: 'info',
|
|
50
|
+
category: 'version',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
analyzer: this.name,
|
|
55
|
+
passed: true,
|
|
56
|
+
issues,
|
|
57
|
+
duration: Date.now() - startTime,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const bundleId = options?.bundleId ?? this.getBundleIdFromProject(project);
|
|
62
|
+
if (!bundleId) {
|
|
63
|
+
issues.push({
|
|
64
|
+
id: 'asc-no-bundle-id',
|
|
65
|
+
title: 'No bundle ID found',
|
|
66
|
+
description: 'Could not determine bundle ID from project.',
|
|
67
|
+
severity: 'warning',
|
|
68
|
+
category: 'version',
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
analyzer: this.name,
|
|
73
|
+
passed: true,
|
|
74
|
+
issues,
|
|
75
|
+
duration: Date.now() - startTime,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get local version info
|
|
80
|
+
const localVersion = await this.getLocalVersion(project);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const versionIssues = await this.validateVersionsForBundleId(bundleId, localVersion);
|
|
84
|
+
issues.push(...versionIssues);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (isASCError(error)) {
|
|
87
|
+
issues.push({
|
|
88
|
+
id: error.code,
|
|
89
|
+
title: error.name,
|
|
90
|
+
description: error.message,
|
|
91
|
+
severity: 'error',
|
|
92
|
+
category: 'version',
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
issues.push({
|
|
96
|
+
id: 'asc-api-error',
|
|
97
|
+
title: 'App Store Connect API Error',
|
|
98
|
+
description: error instanceof Error ? error.message : String(error),
|
|
99
|
+
severity: 'error',
|
|
100
|
+
category: 'version',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
analyzer: this.name,
|
|
107
|
+
passed: issues.filter((i) => i.severity === 'error').length === 0,
|
|
108
|
+
issues,
|
|
109
|
+
duration: Date.now() - startTime,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Compare versions for a bundle ID
|
|
115
|
+
*/
|
|
116
|
+
async compareVersions(
|
|
117
|
+
bundleId: string,
|
|
118
|
+
localVersionString?: string,
|
|
119
|
+
localBuildNumber?: string
|
|
120
|
+
): Promise<AnalysisResult> {
|
|
121
|
+
const startTime = Date.now();
|
|
122
|
+
const issues: Issue[] = [];
|
|
123
|
+
|
|
124
|
+
if (!hasCredentials()) {
|
|
125
|
+
return {
|
|
126
|
+
analyzer: this.name,
|
|
127
|
+
passed: false,
|
|
128
|
+
issues: [
|
|
129
|
+
{
|
|
130
|
+
id: 'asc-credentials-not-configured',
|
|
131
|
+
title: 'App Store Connect credentials not configured',
|
|
132
|
+
description: 'Set ASC environment variables to enable validation.',
|
|
133
|
+
severity: 'error',
|
|
134
|
+
category: 'version',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
duration: Date.now() - startTime,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const versionIssues = await this.validateVersionsForBundleId(bundleId, {
|
|
143
|
+
version: localVersionString,
|
|
144
|
+
build: localBuildNumber,
|
|
145
|
+
});
|
|
146
|
+
issues.push(...versionIssues);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if (isASCError(error)) {
|
|
149
|
+
issues.push({
|
|
150
|
+
id: error.code,
|
|
151
|
+
title: error.name,
|
|
152
|
+
description: error.message,
|
|
153
|
+
severity: 'error',
|
|
154
|
+
category: 'version',
|
|
155
|
+
});
|
|
156
|
+
} else {
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
analyzer: this.name,
|
|
163
|
+
passed: issues.filter((i) => i.severity === 'error').length === 0,
|
|
164
|
+
issues,
|
|
165
|
+
duration: Date.now() - startTime,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate versions
|
|
171
|
+
*/
|
|
172
|
+
private async validateVersionsForBundleId(
|
|
173
|
+
bundleId: string,
|
|
174
|
+
localVersion?: { version?: string | undefined; build?: string | undefined }
|
|
175
|
+
): Promise<Issue[]> {
|
|
176
|
+
const issues: Issue[] = [];
|
|
177
|
+
|
|
178
|
+
const app = await getAppByBundleId(bundleId);
|
|
179
|
+
|
|
180
|
+
// Get latest version and build from ASC
|
|
181
|
+
const [latestVersion, latestBuild, editableVersion] = await Promise.all([
|
|
182
|
+
getLatestVersion(app.id),
|
|
183
|
+
getLatestBuild(app.id),
|
|
184
|
+
getEditableVersion(app.id),
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
// Check submission status of editable version
|
|
188
|
+
if (editableVersion) {
|
|
189
|
+
const state = editableVersion.attributes.appStoreState;
|
|
190
|
+
|
|
191
|
+
if (REJECTED_STATES.includes(state)) {
|
|
192
|
+
issues.push({
|
|
193
|
+
id: 'asc-version-rejected',
|
|
194
|
+
title: 'App version rejected',
|
|
195
|
+
description: `Version ${editableVersion.attributes.versionString} has been rejected (${state}). Review the rejection reason in App Store Connect.`,
|
|
196
|
+
severity: 'error',
|
|
197
|
+
category: 'version',
|
|
198
|
+
suggestion: 'Check App Store Connect for rejection details and required changes.',
|
|
199
|
+
});
|
|
200
|
+
} else if (IN_REVIEW_STATES.includes(state)) {
|
|
201
|
+
issues.push({
|
|
202
|
+
id: 'asc-version-in-review',
|
|
203
|
+
title: 'App version in review',
|
|
204
|
+
description: `Version ${editableVersion.attributes.versionString} is currently ${state === 'WAITING_FOR_REVIEW' ? 'waiting for review' : 'in review'}.`,
|
|
205
|
+
severity: 'info',
|
|
206
|
+
category: 'version',
|
|
207
|
+
});
|
|
208
|
+
} else if (PENDING_STATES.includes(state)) {
|
|
209
|
+
issues.push({
|
|
210
|
+
id: 'asc-version-pending',
|
|
211
|
+
title: 'App version pending release',
|
|
212
|
+
description: `Version ${editableVersion.attributes.versionString} is ${state.toLowerCase().replace(/_/g, ' ')}.`,
|
|
213
|
+
severity: 'info',
|
|
214
|
+
category: 'version',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check version localizations
|
|
219
|
+
const versionData = await getVersionWithLocalizations(editableVersion.id);
|
|
220
|
+
|
|
221
|
+
// Check "What's New" text
|
|
222
|
+
for (const loc of versionData.localizations) {
|
|
223
|
+
if (!loc.attributes.whatsNew) {
|
|
224
|
+
issues.push({
|
|
225
|
+
id: 'asc-missing-whats-new',
|
|
226
|
+
title: `Missing "What's New" text (${loc.attributes.locale})`,
|
|
227
|
+
description: 'Release notes are required for App Store updates.',
|
|
228
|
+
severity: 'warning',
|
|
229
|
+
category: 'version',
|
|
230
|
+
suggestion: 'Add release notes describing what changed in this version.',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Check description
|
|
235
|
+
if (!loc.attributes.description) {
|
|
236
|
+
issues.push({
|
|
237
|
+
id: 'asc-missing-description',
|
|
238
|
+
title: `Missing description (${loc.attributes.locale})`,
|
|
239
|
+
description: 'App description is required.',
|
|
240
|
+
severity: 'error',
|
|
241
|
+
category: 'version',
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Check support URL
|
|
246
|
+
if (!loc.attributes.supportUrl) {
|
|
247
|
+
issues.push({
|
|
248
|
+
id: 'asc-missing-support-url',
|
|
249
|
+
title: `Missing support URL (${loc.attributes.locale})`,
|
|
250
|
+
description: 'Support URL is required for App Store submission.',
|
|
251
|
+
severity: 'error',
|
|
252
|
+
category: 'version',
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Check if build is attached
|
|
258
|
+
if (!versionData.build) {
|
|
259
|
+
issues.push({
|
|
260
|
+
id: 'asc-no-build-attached',
|
|
261
|
+
title: 'No build attached to version',
|
|
262
|
+
description: `Version ${editableVersion.attributes.versionString} does not have a build attached.`,
|
|
263
|
+
severity: 'warning',
|
|
264
|
+
category: 'version',
|
|
265
|
+
suggestion: 'Upload a build and attach it to this version in App Store Connect.',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Compare local vs ASC versions
|
|
271
|
+
if (localVersion?.version && latestVersion) {
|
|
272
|
+
const ascVersion = latestVersion.attributes.versionString;
|
|
273
|
+
const comparison = this.compareVersionStrings(localVersion.version, ascVersion);
|
|
274
|
+
|
|
275
|
+
if (comparison < 0) {
|
|
276
|
+
issues.push({
|
|
277
|
+
id: 'asc-local-version-lower',
|
|
278
|
+
title: 'Local version is lower than ASC',
|
|
279
|
+
description: `Local version (${localVersion.version}) is lower than the latest ASC version (${ascVersion}).`,
|
|
280
|
+
severity: 'warning',
|
|
281
|
+
category: 'version',
|
|
282
|
+
suggestion: 'Update your local version number before submitting.',
|
|
283
|
+
});
|
|
284
|
+
} else if (comparison === 0 && editableVersion) {
|
|
285
|
+
issues.push({
|
|
286
|
+
id: 'asc-version-match',
|
|
287
|
+
title: 'Version already exists in ASC',
|
|
288
|
+
description: `Local version (${localVersion.version}) matches an existing version in App Store Connect.`,
|
|
289
|
+
severity: 'info',
|
|
290
|
+
category: 'version',
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Check build number
|
|
296
|
+
if (localVersion?.build && latestBuild) {
|
|
297
|
+
const ascBuild = latestBuild.attributes.version;
|
|
298
|
+
const localBuildNum = parseInt(localVersion.build, 10);
|
|
299
|
+
const ascBuildNum = parseInt(ascBuild, 10);
|
|
300
|
+
|
|
301
|
+
if (!isNaN(localBuildNum) && !isNaN(ascBuildNum) && localBuildNum <= ascBuildNum) {
|
|
302
|
+
issues.push({
|
|
303
|
+
id: 'asc-build-number-not-incremented',
|
|
304
|
+
title: 'Build number not incremented',
|
|
305
|
+
description: `Local build number (${localVersion.build}) must be greater than the latest ASC build (${ascBuild}).`,
|
|
306
|
+
severity: 'error',
|
|
307
|
+
category: 'version',
|
|
308
|
+
suggestion: `Increment your build number to at least ${ascBuildNum + 1}.`,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return issues;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Compare version strings (semver-like)
|
|
318
|
+
* Returns: -1 if a < b, 0 if a == b, 1 if a > b
|
|
319
|
+
*/
|
|
320
|
+
private compareVersionStrings(a: string, b: string): number {
|
|
321
|
+
const partsA = a.split('.').map((n) => parseInt(n, 10) || 0);
|
|
322
|
+
const partsB = b.split('.').map((n) => parseInt(n, 10) || 0);
|
|
323
|
+
|
|
324
|
+
const maxLength = Math.max(partsA.length, partsB.length);
|
|
325
|
+
|
|
326
|
+
for (let i = 0; i < maxLength; i++) {
|
|
327
|
+
const numA = partsA[i] ?? 0;
|
|
328
|
+
const numB = partsB[i] ?? 0;
|
|
329
|
+
|
|
330
|
+
if (numA < numB) {
|
|
331
|
+
return -1;
|
|
332
|
+
}
|
|
333
|
+
if (numA > numB) {
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get local version from project Info.plist
|
|
343
|
+
*/
|
|
344
|
+
private async getLocalVersion(
|
|
345
|
+
project: XcodeProject
|
|
346
|
+
): Promise<{ version?: string | undefined; build?: string | undefined }> {
|
|
347
|
+
const appTarget = project.targets.find((t) => t.type === 'application');
|
|
348
|
+
|
|
349
|
+
if (!appTarget?.infoPlistPath) {
|
|
350
|
+
return {};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const plist = await parsePlist<InfoPlist>(appTarget.infoPlistPath);
|
|
355
|
+
return {
|
|
356
|
+
version: plist.CFBundleShortVersionString,
|
|
357
|
+
build: plist.CFBundleVersion,
|
|
358
|
+
};
|
|
359
|
+
} catch {
|
|
360
|
+
return {};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private getBundleIdFromProject(project: XcodeProject): string | undefined {
|
|
365
|
+
const appTarget = project.targets.find((t) => t.type === 'application');
|
|
366
|
+
return appTarget?.bundleIdentifier;
|
|
367
|
+
}
|
|
368
|
+
}
|