@mpgd/cli 0.61.1 → 0.63.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/dist/app-store-iap-asc.d.ts +23 -0
- package/dist/app-store-iap-asc.js +326 -0
- package/dist/app-store-iap-port.d.ts +125 -0
- package/dist/app-store-iap-port.js +14 -0
- package/dist/capacitor-shell-starter.js +18 -1
- package/dist/crash-reporting-starter.d.ts +5 -3
- package/dist/crash-reporting-starter.js +10 -7
- package/dist/crash-symbols-upload.d.ts +67 -0
- package/dist/crash-symbols-upload.js +232 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +143 -2
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-symbols.d.ts +95 -0
- package/dist/native-build-symbols.js +261 -0
- package/dist/native-build-target.js +1044 -767
- package/dist/native-deploy-run.d.ts +2 -0
- package/dist/native-deploy-run.js +60 -10
- package/dist/native-deploy-submission.d.ts +8 -0
- package/dist/native-deploy-submission.js +50 -8
- package/dist/play-product-port.d.ts +101 -0
- package/dist/play-product-port.js +7 -0
- package/dist/play-sdk-adapter.js +67 -0
- package/dist/pnpm-build-approvals.d.ts +59 -0
- package/dist/pnpm-build-approvals.js +157 -0
- package/dist/release-state.d.ts +69 -0
- package/dist/release-state.js +150 -5
- package/dist/release-workspace.d.ts +3 -0
- package/dist/release-workspace.js +44 -1
- package/dist/store-products-app-store.d.ts +144 -0
- package/dist/store-products-app-store.js +632 -0
- package/dist/store-products-command.d.ts +50 -0
- package/dist/store-products-command.js +182 -0
- package/dist/store-products-play.d.ts +112 -0
- package/dist/store-products-play.js +622 -0
- package/dist/store-products.d.ts +46 -0
- package/dist/store-products.js +124 -0
- package/dist/testflight-submission.d.ts +1 -1
- package/package.json +11 -11
- package/templates/phaser-game/pnpm-workspace.yaml +2 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, mkdtempSync, rmSync, statSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { ReleaseProcessError, runReleaseProcess, } from './deploy-process.js';
|
|
5
|
+
import { resolveRecordedArtifact } from './native-deploy-submission.js';
|
|
6
|
+
import { readNativeReleaseStatus, recordCrashSymbolUpload, sha256File, } from './release-state.js';
|
|
7
|
+
// Uploads the dSYMs recorded with an iOS build to Firebase Crashlytics with
|
|
8
|
+
// Firebase's own `upload-symbols` (the copy recorded from the build's package
|
|
9
|
+
// checkout) and the `GoogleService-Info.plist` the archived app bundles:
|
|
10
|
+
//
|
|
11
|
+
// upload-symbols -gsp GoogleService-Info.plist -p ios dSYMs.zip
|
|
12
|
+
//
|
|
13
|
+
// Firebase documents this command for uploading dSYMs outside the Xcode run
|
|
14
|
+
// script (https://firebase.google.com/docs/crashlytics/ios/get-deobfuscated-reports);
|
|
15
|
+
// the tool's usage text accepts "zip archives containing dSYMs". The outcome is
|
|
16
|
+
// recorded next to the immutable build record, so an upload is done once and a
|
|
17
|
+
// failed one can be retried with `mpgd deploy symbols`. It never touches a store
|
|
18
|
+
// submission.
|
|
19
|
+
const uploadTimeoutMs = 10 * 60_000;
|
|
20
|
+
const productionPorts = {
|
|
21
|
+
readStatus: readNativeReleaseStatus,
|
|
22
|
+
recordUpload: recordCrashSymbolUpload,
|
|
23
|
+
run: runReleaseProcess,
|
|
24
|
+
platform: process.platform,
|
|
25
|
+
};
|
|
26
|
+
/** Upload (or, with `dryRun`, plan) the Crashlytics dSYMs of one recorded iOS build. */
|
|
27
|
+
export async function uploadRecordedCrashSymbols(input, ports = productionPorts) {
|
|
28
|
+
const stateInput = {
|
|
29
|
+
gameRoot: input.gameRoot,
|
|
30
|
+
gameId: input.gameId,
|
|
31
|
+
releaseKey: input.releaseKey,
|
|
32
|
+
...(input.environment === undefined ? {} : { environment: input.environment }),
|
|
33
|
+
...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
34
|
+
};
|
|
35
|
+
const status = await ports.readStatus(stateInput);
|
|
36
|
+
const build = status.builds.ios;
|
|
37
|
+
if (build === undefined) {
|
|
38
|
+
throw new Error('This release has no recorded iOS build.');
|
|
39
|
+
}
|
|
40
|
+
const symbols = build.crashlyticsSymbols;
|
|
41
|
+
if (symbols === undefined) {
|
|
42
|
+
return {
|
|
43
|
+
status: 'not-applicable',
|
|
44
|
+
reason: 'The iOS build recorded no Crashlytics dSYMs: crash reporting is off, or the build '
|
|
45
|
+
+ 'was recorded before deploy run recorded dSYMs (upload those by hand, see '
|
|
46
|
+
+ 'docs/CRASH_REPORTING.md).',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const previous = status.symbolUploads?.ios;
|
|
50
|
+
if (previous?.status === 'uploaded' && previous.buildRunId === build.buildRunId
|
|
51
|
+
&& previous.dsymArchiveSha256 === symbols.dsymArchive.sha256) {
|
|
52
|
+
return { status: 'already-uploaded', upload: previous };
|
|
53
|
+
}
|
|
54
|
+
const plan = {
|
|
55
|
+
releaseKey: input.releaseKey,
|
|
56
|
+
buildRunId: build.buildRunId,
|
|
57
|
+
dsymArchiveSha256: symbols.dsymArchive.sha256,
|
|
58
|
+
dsyms: symbols.dsyms,
|
|
59
|
+
command: [
|
|
60
|
+
symbols.uploadTool.location,
|
|
61
|
+
'-gsp',
|
|
62
|
+
symbols.googleServiceInfo.location,
|
|
63
|
+
'-p',
|
|
64
|
+
'ios',
|
|
65
|
+
symbols.dsymArchive.location,
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
// The recorded files must be the build's bytes, also for a dry run.
|
|
69
|
+
const tool = await verifiedRecordedFile(input.gameRoot, symbols.uploadTool, 'upload-symbols');
|
|
70
|
+
const plist = await verifiedRecordedFile(input.gameRoot, symbols.googleServiceInfo, 'GoogleService-Info.plist');
|
|
71
|
+
const archive = await verifiedRecordedFile(input.gameRoot, symbols.dsymArchive, 'dSYM archive');
|
|
72
|
+
if (input.dryRun === true) {
|
|
73
|
+
return { status: 'planned', plan };
|
|
74
|
+
}
|
|
75
|
+
if (ports.platform !== 'darwin') {
|
|
76
|
+
throw new Error('Firebase upload-symbols is a macOS tool; upload the dSYMs from a Mac.');
|
|
77
|
+
}
|
|
78
|
+
const record = async (outcome, failure) => ports.recordUpload({
|
|
79
|
+
...stateInput,
|
|
80
|
+
buildRunId: build.buildRunId,
|
|
81
|
+
dsymArchiveSha256: symbols.dsymArchive.sha256,
|
|
82
|
+
status: outcome,
|
|
83
|
+
...(failure === undefined ? {} : { failure }),
|
|
84
|
+
});
|
|
85
|
+
// Run a private executable copy: a restored workflow artifact loses its mode
|
|
86
|
+
// bits, and the recorded file itself stays untouched.
|
|
87
|
+
const workDirectory = mkdtempSync(path.join(tmpdir(), 'mpgd-crash-symbols-'));
|
|
88
|
+
let output = '';
|
|
89
|
+
try {
|
|
90
|
+
const executable = path.join(workDirectory, 'upload-symbols');
|
|
91
|
+
copyFileSync(tool, executable);
|
|
92
|
+
chmodSync(executable, 0o700);
|
|
93
|
+
if (await sha256File(executable) !== symbols.uploadTool.sha256) {
|
|
94
|
+
throw new Error('The copied upload-symbols differs from the recorded tool.');
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const result = await ports.run({
|
|
98
|
+
command: executable,
|
|
99
|
+
args: ['-gsp', plist, '-p', 'ios', archive],
|
|
100
|
+
cwd: workDirectory,
|
|
101
|
+
environment: uploadEnvironment(input.environment ?? process.env, workDirectory),
|
|
102
|
+
timeoutMs: uploadTimeoutMs,
|
|
103
|
+
signal: input.signal,
|
|
104
|
+
});
|
|
105
|
+
output = result.output;
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
output = error instanceof ReleaseProcessError ? error.output : '';
|
|
109
|
+
const reason = describeFailure(error);
|
|
110
|
+
let upload;
|
|
111
|
+
try {
|
|
112
|
+
upload = await record('failed', reason);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
upload = undefined;
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
status: 'failed',
|
|
119
|
+
plan,
|
|
120
|
+
error: reason,
|
|
121
|
+
output,
|
|
122
|
+
...(upload === undefined ? {} : { upload }),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
rmSync(workDirectory, { recursive: true, force: true });
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return { status: 'uploaded', plan, upload: await record('uploaded'), output };
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
// The dSYMs reached Crashlytics; only the release state is behind.
|
|
134
|
+
return {
|
|
135
|
+
status: 'uploaded',
|
|
136
|
+
plan,
|
|
137
|
+
recordError: error instanceof Error ? error.message : String(error),
|
|
138
|
+
output,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Only what the tool needs (and network proxy settings): no store, signing or Git credentials. */
|
|
143
|
+
function uploadEnvironment(source, temporary) {
|
|
144
|
+
const environment = { TMPDIR: temporary };
|
|
145
|
+
for (const name of [
|
|
146
|
+
'PATH',
|
|
147
|
+
'HOME',
|
|
148
|
+
'USER',
|
|
149
|
+
'LANG',
|
|
150
|
+
'LC_ALL',
|
|
151
|
+
'HTTPS_PROXY',
|
|
152
|
+
'https_proxy',
|
|
153
|
+
'HTTP_PROXY',
|
|
154
|
+
'http_proxy',
|
|
155
|
+
'NO_PROXY',
|
|
156
|
+
'no_proxy',
|
|
157
|
+
'SSL_CERT_FILE',
|
|
158
|
+
]) {
|
|
159
|
+
const value = source[name];
|
|
160
|
+
if (value !== undefined) {
|
|
161
|
+
environment[name] = value;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return environment;
|
|
165
|
+
}
|
|
166
|
+
function describeFailure(error) {
|
|
167
|
+
if (error instanceof ReleaseProcessError) {
|
|
168
|
+
if (error.reason === 'exit') {
|
|
169
|
+
return `upload-symbols exited with code ${String(error.exitCode ?? 'unknown')}`;
|
|
170
|
+
}
|
|
171
|
+
return error.reason === 'timeout'
|
|
172
|
+
? 'upload-symbols timed out'
|
|
173
|
+
: `upload-symbols ${error.reason === 'spawn' ? 'could not start' : 'was aborted'}`;
|
|
174
|
+
}
|
|
175
|
+
return error instanceof Error ? error.message : String(error);
|
|
176
|
+
}
|
|
177
|
+
async function verifiedRecordedFile(gameRoot, recorded, label) {
|
|
178
|
+
const candidate = path.resolve(gameRoot, recorded.location);
|
|
179
|
+
if (!existsSync(candidate) || !statSync(candidate).isFile()) {
|
|
180
|
+
throw new Error(`The recorded ${label} is missing: ${recorded.location}. Restore the release bundle `
|
|
181
|
+
+ '(.mpgd/releases/<release>) the build wrote.');
|
|
182
|
+
}
|
|
183
|
+
let file;
|
|
184
|
+
try {
|
|
185
|
+
file = resolveRecordedArtifact(gameRoot, recorded.location);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
throw new Error(`The recorded ${label} resolves outside the game directory.`);
|
|
189
|
+
}
|
|
190
|
+
if (await sha256File(file) !== recorded.sha256) {
|
|
191
|
+
throw new Error(`The recorded ${label} differs from the immutable build record.`);
|
|
192
|
+
}
|
|
193
|
+
return file;
|
|
194
|
+
}
|
|
195
|
+
/** The command that retries a failed upload for one release, for the game directory used. */
|
|
196
|
+
export function crashSymbolsRetryCommand(input) {
|
|
197
|
+
return `mpgd deploy symbols --game ${shellWord(input.gameRoot)} --game-id ${shellWord(input.gameId)} --release ${shellWord(input.releaseKey)}`;
|
|
198
|
+
}
|
|
199
|
+
function shellWord(value) {
|
|
200
|
+
return /^[A-Za-z0-9_./:@%+=,-]+$/u.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The `mpgd deploy run` step: upload the recorded dSYMs of the release's iOS
|
|
204
|
+
* build once. A failure (or an error reading the files or the state) is
|
|
205
|
+
* reported with the retry command and never thrown, so it cannot fail or
|
|
206
|
+
* block the store submission.
|
|
207
|
+
*/
|
|
208
|
+
export async function uploadCrashSymbolsForDeployRun(input, report = (message) => console.warn(message), upload = uploadRecordedCrashSymbols) {
|
|
209
|
+
const retry = crashSymbolsRetryCommand(input);
|
|
210
|
+
try {
|
|
211
|
+
const result = await upload(input);
|
|
212
|
+
if (result.status === 'uploaded' && result.recordError !== undefined) {
|
|
213
|
+
report(`warning: Crashlytics: uploaded the iOS dSYMs, but the release state could not record `
|
|
214
|
+
+ `it (${result.recordError}); "mpgd deploy status" still shows it as not uploaded, and `
|
|
215
|
+
+ `${retry} would upload them again.`);
|
|
216
|
+
}
|
|
217
|
+
else if (result.status === 'uploaded') {
|
|
218
|
+
report(`Crashlytics: uploaded the iOS dSYMs (${result.plan.dsyms
|
|
219
|
+
.map((entry) => entry.bundle).join(', ')}).`);
|
|
220
|
+
}
|
|
221
|
+
else if (result.status === 'failed') {
|
|
222
|
+
report(`warning: Crashlytics dSYM upload failed (${result.error}); the store submission `
|
|
223
|
+
+ `is not affected. Retry with: ${retry}${result.output === '' ? '' : `\n${result.output}`}`);
|
|
224
|
+
}
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
report(`warning: Crashlytics dSYM upload did not run (${error instanceof Error ? error.message : String(error)}); the store submission is not `
|
|
229
|
+
+ `affected. Retry with: ${retry}`);
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export { runNativeDeployment, readStoreCredential, type RunNativeDeploymentInput
|
|
|
9
9
|
export { planRecordedAndroidSubmission, submitRecordedNativeTarget, type NativeStoreCredential, type SubmitRecordedNativeTargetInput, } from './native-deploy-submission.js';
|
|
10
10
|
export { PlaySubmissionUncertainError, planVerifiedAndroidBundleSubmission, submitVerifiedAndroidBundle, type PlayInternalSubmissionInput, type PlayInternalSubmissionResult, type PlaySubmissionPlan, } from './play-internal-submission.js';
|
|
11
11
|
export { createPlayReleaseRequest, defaultPlayReleaseRequest, maximumPlayReleaseNoteCharacters, parsePlayReleaseNotes, type CreatePlayReleaseRequestInput, type PlayNewReleaseStatus, type PlayReleaseNote, type PlayReleaseRequest, type PlayReleaseStatus, } from './play-release-options.js';
|
|
12
|
+
export { applyStoreProducts, planStoreProducts, saveStoreProductPlan, type StoreProductApplyOptions, type StoreProductPlanFile, type StoreProductPlanOptions, type StoreProductPorts, type StoreProductStore, } from './store-products-command.js';
|
|
13
|
+
export { applyPlayProductPlan, formatPlayProductPlan, planPlayProducts, PlayProductApplyError, type PlannedPlayProduct, type PlayProductApplyResult, type PlayProductChange, type PlayProductPlan, type PlayProductPlanInput, } from './store-products-play.js';
|
|
14
|
+
export { AppStoreProductApplyError, applyAppStoreProductPlan, formatAppStoreProductPlan, planAppStoreProducts, type AppStoreProductApplyResult, type AppStoreProductChange, type AppStoreProductPlan, type AppStoreProductPlanInput, type PlannedAppStoreProduct, } from './store-products-app-store.js';
|
|
15
|
+
export { StoreProductPlanStaleError } from './store-products.js';
|
|
12
16
|
export { PlayPromoteUncertainError, promotePlayRelease, type PlayPromoteInput, type PlayPromoteResult, } from './play-release-promote.js';
|
|
13
17
|
export { PlayRolloutUncertainError, changePlayRollout, type PlayRolloutAction, type PlayRolloutInput, type PlayRolloutResult, } from './play-release-rollout.js';
|
|
14
18
|
export { IosSubmissionUncertainError, submitVerifiedIosBuild, type IosTestFlightStatus, type IosTestFlightSubmissionInput, type IosTestFlightSubmissionResult, } from './testflight-submission.js';
|
package/dist/index.js
CHANGED
|
@@ -36,8 +36,12 @@ import { readStoreCredential, runNativeDeployment } from './native-deploy-run.js
|
|
|
36
36
|
import { isNativeDeployApproved, planRecordedAndroidSubmission, submitRecordedNativeTarget, } from './native-deploy-submission.js';
|
|
37
37
|
import { createPlayReleaseRequest, parsePlayUserFraction, readPlayReleaseNotesFile, } from './play-release-options.js';
|
|
38
38
|
import { promotePlayRelease } from './play-release-promote.js';
|
|
39
|
+
import { applyStoreProducts, planStoreProducts, saveStoreProductPlan, } from './store-products-command.js';
|
|
40
|
+
import { formatAppStoreProductPlan } from './store-products-app-store.js';
|
|
41
|
+
import { formatPlayProductPlan } from './store-products-play.js';
|
|
39
42
|
import { changePlayRollout } from './play-release-rollout.js';
|
|
40
43
|
import { readNativeReleaseStatus as readNativeReleaseStatusForCli } from './release-state.js';
|
|
44
|
+
import { uploadRecordedCrashSymbols } from './crash-symbols-upload.js';
|
|
41
45
|
export { renderGameAcceptanceMarkdown, resolveGameAcceptanceReleaseManifestFile, runGameAcceptance, defaultGameAcceptanceCommandTimeoutMs, maximumGameplayE2EReportBytes, } from './game-acceptance.js';
|
|
42
46
|
export { maximumPerformanceEvidenceBytes, preparePerformanceAcceptance, readPerformanceAcceptanceEvidence, validatePerformanceBudget, } from './performance-acceptance.js';
|
|
43
47
|
export { applyCapacitorShellStarter, materializeCapacitorShellStarter, planCapacitorShellStarter, } from './capacitor-shell-starter.js';
|
|
@@ -49,6 +53,10 @@ export { runNativeDeployment, readStoreCredential, } from './native-deploy-run.j
|
|
|
49
53
|
export { planRecordedAndroidSubmission, submitRecordedNativeTarget, } from './native-deploy-submission.js';
|
|
50
54
|
export { PlaySubmissionUncertainError, planVerifiedAndroidBundleSubmission, submitVerifiedAndroidBundle, } from './play-internal-submission.js';
|
|
51
55
|
export { createPlayReleaseRequest, defaultPlayReleaseRequest, maximumPlayReleaseNoteCharacters, parsePlayReleaseNotes, } from './play-release-options.js';
|
|
56
|
+
export { applyStoreProducts, planStoreProducts, saveStoreProductPlan, } from './store-products-command.js';
|
|
57
|
+
export { applyPlayProductPlan, formatPlayProductPlan, planPlayProducts, PlayProductApplyError, } from './store-products-play.js';
|
|
58
|
+
export { AppStoreProductApplyError, applyAppStoreProductPlan, formatAppStoreProductPlan, planAppStoreProducts, } from './store-products-app-store.js';
|
|
59
|
+
export { StoreProductPlanStaleError } from './store-products.js';
|
|
52
60
|
export { PlayPromoteUncertainError, promotePlayRelease, } from './play-release-promote.js';
|
|
53
61
|
export { PlayRolloutUncertainError, changePlayRollout, } from './play-release-rollout.js';
|
|
54
62
|
export { IosSubmissionUncertainError, submitVerifiedIosBuild, } from './testflight-submission.js';
|
|
@@ -209,6 +217,7 @@ export async function runMpgdCli(args) {
|
|
|
209
217
|
assets: assetsCommand,
|
|
210
218
|
game: gameCommand,
|
|
211
219
|
deploy: deployCommand,
|
|
220
|
+
store: storeCommand,
|
|
212
221
|
legal: legalCommand,
|
|
213
222
|
target: targetCommand,
|
|
214
223
|
kit: kitCommand,
|
|
@@ -261,7 +270,7 @@ const entryCommand = defineI18n({
|
|
|
261
270
|
ko: 'mpgd-kit 스타터와 타깃 워크플로우를 관리합니다.',
|
|
262
271
|
}),
|
|
263
272
|
run: () => {
|
|
264
|
-
console.info('Use a sub-command: assets, game, deploy, legal, target, kit.');
|
|
273
|
+
console.info('Use a sub-command: assets, game, deploy, store, legal, target, kit.');
|
|
265
274
|
console.info('Run "pnpm mpgd --help" for available commands.');
|
|
266
275
|
},
|
|
267
276
|
});
|
|
@@ -439,6 +448,42 @@ const deployCommand = defineI18n({
|
|
|
439
448
|
console.info(JSON.stringify(status, null, 2));
|
|
440
449
|
},
|
|
441
450
|
}),
|
|
451
|
+
symbols: defineI18n({
|
|
452
|
+
name: 'symbols',
|
|
453
|
+
description: "Upload the Crashlytics dSYMs recorded with a release's iOS build.",
|
|
454
|
+
resource: commandResource({
|
|
455
|
+
en: "Upload the Crashlytics dSYMs recorded with a release's iOS build.",
|
|
456
|
+
ko: '릴리스의 iOS 빌드에 기록된 Crashlytics dSYM을 업로드합니다.',
|
|
457
|
+
}),
|
|
458
|
+
args: {
|
|
459
|
+
game: { type: 'string', required: false, description: 'Game project directory holding .mpgd/releases.' },
|
|
460
|
+
'game-id': { type: 'string', required: true, description: 'Explicit release ledger game ID.' },
|
|
461
|
+
release: { type: 'string', required: true, description: 'Explicit release ID.' },
|
|
462
|
+
'dry-run': {
|
|
463
|
+
type: 'boolean', required: false,
|
|
464
|
+
description: 'Verify the recorded files and print the planned upload-symbols command; run nothing.',
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
run: async (ctx) => {
|
|
468
|
+
const result = await uploadRecordedCrashSymbols({
|
|
469
|
+
gameRoot: path.resolve(readOptionalString(ctx.values.game) ?? '.'),
|
|
470
|
+
gameId: readRequiredCliOption(ctx.values['game-id'], '--game-id'),
|
|
471
|
+
releaseKey: readRequiredCliOption(ctx.values.release, '--release'),
|
|
472
|
+
dryRun: ctx.values['dry-run'] === true,
|
|
473
|
+
});
|
|
474
|
+
const { output, ...summary } = 'output' in result ? result : { ...result, output: '' };
|
|
475
|
+
if (output !== '') {
|
|
476
|
+
console.info(output);
|
|
477
|
+
}
|
|
478
|
+
console.info(JSON.stringify(summary, null, 2));
|
|
479
|
+
if (result.status === 'failed') {
|
|
480
|
+
throw new Error(`Crashlytics dSYM upload failed: ${result.error}`);
|
|
481
|
+
}
|
|
482
|
+
if (result.status === 'uploaded' && result.recordError !== undefined) {
|
|
483
|
+
throw new Error(`The dSYMs were uploaded, but the release state could not record it: ${result.recordError}`);
|
|
484
|
+
}
|
|
485
|
+
},
|
|
486
|
+
}),
|
|
442
487
|
submit: defineI18n({
|
|
443
488
|
name: 'submit',
|
|
444
489
|
description: 'Resume store submission of one already verified native build.',
|
|
@@ -706,6 +751,92 @@ const deployCommand = defineI18n({
|
|
|
706
751
|
}),
|
|
707
752
|
},
|
|
708
753
|
});
|
|
754
|
+
const storeCommand = defineI18n({
|
|
755
|
+
name: 'store',
|
|
756
|
+
description: 'Sync store-side configuration from game files.',
|
|
757
|
+
resource: commandResource({
|
|
758
|
+
en: 'Sync store-side configuration from game files.',
|
|
759
|
+
ko: '게임 파일에서 스토어 쪽 설정을 동기화합니다.',
|
|
760
|
+
}),
|
|
761
|
+
subCommands: {
|
|
762
|
+
products: defineI18n({
|
|
763
|
+
name: 'products',
|
|
764
|
+
description: 'Plan and apply one-time products from the catalog and store listing file.',
|
|
765
|
+
resource: commandResource({
|
|
766
|
+
en: 'Plan and apply one-time products from the catalog and store listing file.',
|
|
767
|
+
ko: '카탈로그와 스토어 리스팅 파일로 일회성 상품을 계획하고 적용합니다.',
|
|
768
|
+
}),
|
|
769
|
+
subCommands: {
|
|
770
|
+
plan: defineI18n({
|
|
771
|
+
name: 'plan',
|
|
772
|
+
description: 'Read the store products and print the changes; never writes to the store.',
|
|
773
|
+
resource: commandResource({
|
|
774
|
+
en: 'Read the store products and print the changes; never writes to the store.',
|
|
775
|
+
ko: '스토어 상품을 읽고 변경 사항을 출력합니다. 스토어에 쓰지 않습니다.',
|
|
776
|
+
}),
|
|
777
|
+
args: {
|
|
778
|
+
game: { type: 'string', required: false, description: 'Game project directory.' },
|
|
779
|
+
profile: { type: 'string', required: false, description: 'Deployment profile naming the app and credential; defaults to beta.' },
|
|
780
|
+
store: { type: 'string', required: true, description: 'google-play or app-store.' },
|
|
781
|
+
catalog: { type: 'string', required: false, description: 'Product catalog path, relative to the current directory; defaults to mpgd.catalog.json in the game.' },
|
|
782
|
+
listings: { type: 'string', required: false, description: 'Store listing file path, relative to the current directory; defaults to mpgd.store-listings.json in the game.' },
|
|
783
|
+
'catalog-target': { type: 'string', required: false, description: 'Catalog platformProductIds key of the store; defaults to android (Google Play) or ios (App Store).' },
|
|
784
|
+
'keep-live-prices': { type: 'boolean', required: false, description: 'Leave the prices of products players can already buy unchanged.' },
|
|
785
|
+
out: { type: 'string', required: false, description: 'New plan JSON path for apply; existing files are not overwritten.' },
|
|
786
|
+
},
|
|
787
|
+
run: async (ctx) => {
|
|
788
|
+
const catalog = readOptionalString(ctx.values.catalog);
|
|
789
|
+
const listings = readOptionalString(ctx.values.listings);
|
|
790
|
+
const catalogTarget = readOptionalString(ctx.values['catalog-target']);
|
|
791
|
+
const result = await planStoreProducts({
|
|
792
|
+
game: readOptionalString(ctx.values.game) ?? '.',
|
|
793
|
+
profile: readOptionalString(ctx.values.profile) ?? 'beta',
|
|
794
|
+
store: readRequiredCliOption(ctx.values.store, '--store'),
|
|
795
|
+
...(catalog === undefined ? {} : { catalogFile: catalog }),
|
|
796
|
+
...(listings === undefined ? {} : { listingsFile: listings }),
|
|
797
|
+
...(catalogTarget === undefined ? {} : { catalogTarget }),
|
|
798
|
+
...(ctx.values['keep-live-prices'] === true ? { keepLivePrices: true } : {}),
|
|
799
|
+
});
|
|
800
|
+
console.info(result.plan.store === 'google-play'
|
|
801
|
+
? formatPlayProductPlan(result.plan)
|
|
802
|
+
: formatAppStoreProductPlan(result.plan));
|
|
803
|
+
const out = readOptionalString(ctx.values.out);
|
|
804
|
+
if (out !== undefined) {
|
|
805
|
+
console.info(`Wrote store product plan: ${saveStoreProductPlan(out, result)}`);
|
|
806
|
+
}
|
|
807
|
+
},
|
|
808
|
+
}),
|
|
809
|
+
apply: defineI18n({
|
|
810
|
+
name: 'apply',
|
|
811
|
+
description: 'Write the reviewed plan; refuses when the store or the files changed since.',
|
|
812
|
+
resource: commandResource({
|
|
813
|
+
en: 'Write the reviewed plan; refuses when the store or the files changed since.',
|
|
814
|
+
ko: '검토한 계획을 적용합니다. 이후 스토어나 파일이 바뀌었으면 거부합니다.',
|
|
815
|
+
}),
|
|
816
|
+
args: {
|
|
817
|
+
plan: { type: 'string', required: true, description: 'Plan JSON written by "store products plan --out".' },
|
|
818
|
+
game: { type: 'string', required: false, description: 'Current game directory if the game moved.' },
|
|
819
|
+
approve: { type: 'boolean', required: false, description: 'Approve the store writes.' },
|
|
820
|
+
'confirm-price-change': { type: 'boolean', required: false, description: 'Confirm price changes on products players can already buy.' },
|
|
821
|
+
},
|
|
822
|
+
run: async (ctx) => {
|
|
823
|
+
const game = readOptionalString(ctx.values.game);
|
|
824
|
+
const result = await applyStoreProducts({
|
|
825
|
+
planFile: readRequiredCliOption(ctx.values.plan, '--plan'),
|
|
826
|
+
...(game === undefined ? {} : { game }),
|
|
827
|
+
approved: ctx.values.approve === true,
|
|
828
|
+
...(ctx.values['confirm-price-change'] === true ? { confirmPriceChange: true } : {}),
|
|
829
|
+
});
|
|
830
|
+
console.info(JSON.stringify(result, null, 2));
|
|
831
|
+
},
|
|
832
|
+
}),
|
|
833
|
+
},
|
|
834
|
+
}),
|
|
835
|
+
},
|
|
836
|
+
run: () => {
|
|
837
|
+
console.info('Use "mpgd store products plan" or "mpgd store products apply".');
|
|
838
|
+
},
|
|
839
|
+
});
|
|
709
840
|
const gameCommand = defineI18n({
|
|
710
841
|
name: 'game',
|
|
711
842
|
description: 'Create standalone Phaser game starters.',
|
|
@@ -1891,7 +2022,17 @@ const targetCommand = defineI18n({
|
|
|
1891
2022
|
}),
|
|
1892
2023
|
});
|
|
1893
2024
|
if (!dryRun) {
|
|
1894
|
-
|
|
2025
|
+
try {
|
|
2026
|
+
materializeCapacitorShellStarter(plan);
|
|
2027
|
+
}
|
|
2028
|
+
catch (error) {
|
|
2029
|
+
// A failed install may be the one the warnings explain (for example
|
|
2030
|
+
// the allowBuilds entries an unsupported pnpm-workspace.yaml needs).
|
|
2031
|
+
for (const warning of plan.warnings) {
|
|
2032
|
+
console.warn(`Warning: ${warning}`);
|
|
2033
|
+
}
|
|
2034
|
+
throw error;
|
|
2035
|
+
}
|
|
1895
2036
|
}
|
|
1896
2037
|
console.info(`${dryRun ? 'Would update' : 'Updated'} game-owned Capacitor shell: ${plan.changedFiles.length} file(s).`);
|
|
1897
2038
|
for (const file of plan.changedFiles) {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** Game-relative paths, under the build's `release-output/native/<target>/` root. */
|
|
2
|
+
export interface NativeBuildSymbols {
|
|
3
|
+
/** Android: R8 `mapping.txt` of this exact bundle. */
|
|
4
|
+
readonly r8Mapping?: string;
|
|
5
|
+
/** iOS with Firebase Crashlytics. */
|
|
6
|
+
readonly crashlytics?: NativeBuildCrashlyticsSymbols;
|
|
7
|
+
}
|
|
8
|
+
export interface NativeBuildCrashlyticsSymbols {
|
|
9
|
+
/** Zip of the archive's `dSYMs` folder. */
|
|
10
|
+
readonly dsymArchive: string;
|
|
11
|
+
/** Each dSYM bundle in the archive and the Mach-O UUIDs of its DWARF files. */
|
|
12
|
+
readonly dsyms: readonly NativeDsymIdentity[];
|
|
13
|
+
/** Firebase's `upload-symbols` from the shell's package checkout. */
|
|
14
|
+
readonly uploadTool: string;
|
|
15
|
+
/** The `GoogleService-Info.plist` the archived app bundles. */
|
|
16
|
+
readonly googleServiceInfo: string;
|
|
17
|
+
}
|
|
18
|
+
export interface NativeDsymIdentity {
|
|
19
|
+
readonly bundle: string;
|
|
20
|
+
readonly uuids: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Validate a symbols record read from a build status file. `isPath` checks
|
|
24
|
+
* each recorded path (game-relative, inside the build's output root).
|
|
25
|
+
*/
|
|
26
|
+
export declare function assertNativeBuildSymbols(value: unknown, isPath: (file: string) => boolean): asserts value is NativeBuildSymbols;
|
|
27
|
+
/** A non-empty, sorted list of dSYM bundles with their UUIDs. */
|
|
28
|
+
export declare function isDsymIdentityList(value: unknown): value is readonly NativeDsymIdentity[];
|
|
29
|
+
/** A dSYM bundle file name: no path separator or control character, not hidden. */
|
|
30
|
+
export declare function isDsymBundleName(name: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* A game-relative path under the target's native build output root
|
|
33
|
+
* (`release-output/native/<target>/`), without empty or `..` segments.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isNativeOutputPath(file: string, target: string): boolean;
|
|
36
|
+
/** Where AGP writes the release variant's R8 mapping file. */
|
|
37
|
+
export declare const androidReleaseMappingPath = "app/build/outputs/mapping/release/mapping.txt";
|
|
38
|
+
/** File names inside the build's `symbols/` directory. */
|
|
39
|
+
export declare const nativeSymbolFileNames: Readonly<{
|
|
40
|
+
r8Mapping: "mapping.txt";
|
|
41
|
+
dsymArchive: "dSYMs.zip";
|
|
42
|
+
uploadTool: "upload-symbols";
|
|
43
|
+
googleServiceInfo: "GoogleService-Info.plist";
|
|
44
|
+
}>;
|
|
45
|
+
/**
|
|
46
|
+
* Where a Crashlytics-enabled iOS archive puts its Swift Package checkouts
|
|
47
|
+
* (`xcodebuild -clonedSourcePackagesDirPath`), so `upload-symbols` is found
|
|
48
|
+
* without searching DerivedData.
|
|
49
|
+
*/
|
|
50
|
+
export declare const iosCrashlyticsSourcePackagesDirectory = ".mpgd-source-packages";
|
|
51
|
+
/**
|
|
52
|
+
* `upload-symbols` ships next to the Crashlytics `run` script: in the Swift
|
|
53
|
+
* Package checkout (`SourcePackages/checkouts/firebase-ios-sdk/Crashlytics/`,
|
|
54
|
+
* the path Firebase's run script uses) or in CocoaPods
|
|
55
|
+
* (`Pods/FirebaseCrashlytics/`, the path Firebase's manual-upload command uses).
|
|
56
|
+
*/
|
|
57
|
+
export declare function crashlyticsUploadToolCandidates(input: {
|
|
58
|
+
readonly sourcePackages: string;
|
|
59
|
+
readonly iosProject: string;
|
|
60
|
+
}): readonly string[];
|
|
61
|
+
export interface CollectIosCrashlyticsSymbolsInput {
|
|
62
|
+
/** Absolute path of the `.xcarchive` this build produced. */
|
|
63
|
+
readonly archive: string;
|
|
64
|
+
/** Absolute path of the staged shell's `ios` directory. */
|
|
65
|
+
readonly iosProject: string;
|
|
66
|
+
/** Absolute `-clonedSourcePackagesDirPath` of the archive build. */
|
|
67
|
+
readonly sourcePackages: string;
|
|
68
|
+
/** Absolute output directory (`<artifact root>/symbols`). */
|
|
69
|
+
readonly outputDirectory: string;
|
|
70
|
+
readonly copyFile: (source: string, destination: string) => void;
|
|
71
|
+
/** Zips `source` (a directory) into `destination` keeping the folder name. */
|
|
72
|
+
readonly zipDirectory: (source: string, destination: string) => void;
|
|
73
|
+
}
|
|
74
|
+
export interface CollectedIosCrashlyticsSymbols {
|
|
75
|
+
readonly dsymArchive: string;
|
|
76
|
+
readonly dsyms: readonly NativeDsymIdentity[];
|
|
77
|
+
readonly uploadTool: string;
|
|
78
|
+
readonly googleServiceInfo: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Copy what Crashlytics needs out of a staged archive build before the stage
|
|
82
|
+
* is removed. Refused when the archive has no dSYM (Firebase: "Set Debug
|
|
83
|
+
* Information Format to DWARF with dSYM File"), when the checkout has no
|
|
84
|
+
* `upload-symbols`, or when the app does not bundle `GoogleService-Info.plist`.
|
|
85
|
+
*/
|
|
86
|
+
export declare function collectIosCrashlyticsSymbols(input: CollectIosCrashlyticsSymbolsInput): CollectedIosCrashlyticsSymbols;
|
|
87
|
+
/** Every `*.dSYM` bundle of an archive's `dSYMs` folder, sorted, with its UUIDs. */
|
|
88
|
+
export declare function readArchiveDsyms(dsymFolder: string): NativeDsymIdentity[];
|
|
89
|
+
/** dSYM UUIDs as `dwarfdump --uuid` prints them. */
|
|
90
|
+
export declare function isDsymUuid(value: unknown): value is string;
|
|
91
|
+
/**
|
|
92
|
+
* The `LC_UUID` of each architecture in a thin or universal Mach-O file,
|
|
93
|
+
* read from its headers only (DWARF files can be hundreds of megabytes).
|
|
94
|
+
*/
|
|
95
|
+
export declare function readMachOUuids(file: string): string[];
|