@mpgd/cli 0.58.0 → 0.61.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/capacitor-shell-starter.d.ts +24 -0
- package/dist/capacitor-shell-starter.js +100 -8
- package/dist/crash-reporting-starter.d.ts +99 -0
- package/dist/crash-reporting-starter.js +271 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +221 -14
- package/dist/ios-ipa-inspection.js +120 -1
- package/dist/native-build-info.json +2 -2
- package/dist/native-build-target.js +1617 -1185
- package/dist/native-deploy-run.d.ts +6 -0
- package/dist/native-deploy-run.js +19 -1
- package/dist/native-deploy-submission.d.ts +20 -1
- package/dist/native-deploy-submission.js +94 -7
- package/dist/native-shell-ios-push-files.d.ts +9 -0
- package/dist/native-shell-ios-push-files.js +65 -0
- package/dist/native-shell-store-policy.d.ts +109 -0
- package/dist/native-shell-store-policy.js +553 -7
- package/dist/play-internal-submission.d.ts +40 -4
- package/dist/play-internal-submission.js +215 -48
- package/dist/play-publisher-port.d.ts +26 -1
- package/dist/play-release-options.d.ts +97 -0
- package/dist/play-release-options.js +307 -0
- package/dist/play-release-promote.d.ts +59 -0
- package/dist/play-release-promote.js +235 -0
- package/dist/play-release-rollout.d.ts +57 -0
- package/dist/play-release-rollout.js +237 -0
- package/dist/play-sdk-adapter.js +26 -3
- package/dist/release-state.d.ts +6 -0
- package/dist/release-state.js +8 -0
- package/package.json +17 -17
- package/templates/phaser-game/vite.runtime-target-config.ts +4 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { createReadStream, existsSync, statSync } from 'node:fs';
|
|
3
3
|
import { inspectAndroidBundleSigner } from './android-bundle-signer.js';
|
|
4
|
+
import { assertPlayReleaseConfirmed, assertPlayReleaseRequest, defaultPlayReleaseRequest, hashMappingFile, isPlayDraftAppRejection, isWellKnownPlayTrack, mergeNewPlayRelease, newPlayRelease, playReleaseNeedsConfirmation, } from './play-release-options.js';
|
|
4
5
|
const sha256Pattern = /^[a-f0-9]{64}$/u;
|
|
5
6
|
const editIdPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
6
|
-
const trackName = 'internal';
|
|
7
7
|
const bundleUploadTimeoutMs = 120_000;
|
|
8
8
|
export class PlaySubmissionUncertainError extends Error {
|
|
9
9
|
stage;
|
|
@@ -24,9 +24,45 @@ export async function submitVerifiedAndroidBundle(input) {
|
|
|
24
24
|
const publisher = createServiceAccountPublisher(input.serviceAccountFile);
|
|
25
25
|
return submitVerifiedAndroidBundleWithPublisher(input, publisher);
|
|
26
26
|
}
|
|
27
|
+
/** Verify the record and artifact, then describe the edit without calling Google Play. */
|
|
28
|
+
export async function planVerifiedAndroidBundleSubmission(input) {
|
|
29
|
+
const request = input.release ?? defaultPlayReleaseRequest;
|
|
30
|
+
assertPlayReleaseRequest(request);
|
|
31
|
+
const versionCode = await preflight(input);
|
|
32
|
+
await assertMappingUnchanged(request);
|
|
33
|
+
return {
|
|
34
|
+
dryRun: true,
|
|
35
|
+
packageName: input.packageName,
|
|
36
|
+
track: request.track,
|
|
37
|
+
versionCode,
|
|
38
|
+
bundleSha256: input.record.artifactSha256,
|
|
39
|
+
release: newPlayRelease(request, input.record.releaseKey, versionCode),
|
|
40
|
+
...(request.mappingFile === undefined || request.mappingSha256 === undefined ? {} : {
|
|
41
|
+
mapping: { file: request.mappingFile, sha256: request.mappingSha256 },
|
|
42
|
+
}),
|
|
43
|
+
requiresConfirmProduction: playReleaseNeedsConfirmation(request),
|
|
44
|
+
operations: [
|
|
45
|
+
'edits.insert',
|
|
46
|
+
'edits.bundles.list',
|
|
47
|
+
`edits.tracks.get ${request.track}`,
|
|
48
|
+
'edits.bundles.upload (skipped when the edit already has this version code)',
|
|
49
|
+
...(request.mappingFile === undefined
|
|
50
|
+
? [] : ['edits.deobfuscationfiles.upload proguard']),
|
|
51
|
+
`edits.tracks.update ${request.track} (other releases kept; a ${request.status === 'draft' ? 'draft replaces an older draft'
|
|
52
|
+
: 'new release replaces an in-progress or halted rollout'})`,
|
|
53
|
+
'edits.validate',
|
|
54
|
+
'edits.commit changesInReviewBehavior=ERROR_IF_IN_REVIEW',
|
|
55
|
+
],
|
|
56
|
+
};
|
|
57
|
+
}
|
|
27
58
|
/** Internal injection point for credential-free SDK contract tests. */
|
|
28
59
|
export async function submitVerifiedAndroidBundleWithPublisher(input, publisher, requestRootUrl) {
|
|
60
|
+
const request = input.release ?? defaultPlayReleaseRequest;
|
|
61
|
+
assertPlayReleaseRequest(request);
|
|
62
|
+
assertPlayReleaseConfirmed(request, input.confirmProduction === true);
|
|
63
|
+
const trackName = request.track;
|
|
29
64
|
const versionCode = await preflight(input);
|
|
65
|
+
await assertMappingUnchanged(request);
|
|
30
66
|
const expectedSha256 = input.record.artifactSha256;
|
|
31
67
|
const packageName = input.packageName;
|
|
32
68
|
if (input.resumeEditId !== undefined && !editIdPattern.test(input.resumeEditId)) {
|
|
@@ -35,14 +71,22 @@ export async function submitVerifiedAndroidBundleWithPublisher(input, publisher,
|
|
|
35
71
|
input.signal?.throwIfAborted();
|
|
36
72
|
let editId = input.resumeEditId ?? await publisher.insertEdit(packageName, input.signal);
|
|
37
73
|
let freshEdit = input.resumeEditId === undefined;
|
|
38
|
-
|
|
74
|
+
// A new edit is checkpointed after the read-only checks below and before
|
|
75
|
+
// anything is uploaded or committed, so a refused request leaves no edit to
|
|
76
|
+
// reconcile and can be retried with corrected options.
|
|
77
|
+
let editPersisted = input.resumeEditId !== undefined;
|
|
78
|
+
const persistEdit = async () => {
|
|
79
|
+
if (editPersisted) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
39
82
|
try {
|
|
40
83
|
await input.onEditCreated?.(editId);
|
|
41
84
|
}
|
|
42
85
|
catch {
|
|
43
86
|
throw new PlaySubmissionUncertainError('checkpoint', editId);
|
|
44
87
|
}
|
|
45
|
-
|
|
88
|
+
editPersisted = true;
|
|
89
|
+
};
|
|
46
90
|
let bundles;
|
|
47
91
|
try {
|
|
48
92
|
bundles = await publisher.listBundles(packageName, editId, input.signal);
|
|
@@ -52,39 +96,73 @@ export async function submitVerifiedAndroidBundleWithPublisher(input, publisher,
|
|
|
52
96
|
throw error;
|
|
53
97
|
}
|
|
54
98
|
// A successful commit may have invalidated the old edit while its response
|
|
55
|
-
// was lost. Open a new snapshot solely to inspect the live
|
|
99
|
+
// was lost. Open a new snapshot solely to inspect the live track.
|
|
56
100
|
editId = await publisher.insertEdit(packageName, input.signal);
|
|
57
101
|
freshEdit = true;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
catch {
|
|
62
|
-
throw new PlaySubmissionUncertainError('checkpoint', editId);
|
|
63
|
-
}
|
|
102
|
+
editPersisted = false;
|
|
103
|
+
await persistEdit();
|
|
64
104
|
bundles = await publisher.listBundles(packageName, editId, input.signal);
|
|
65
105
|
}
|
|
66
106
|
let existing = findVersionBundle(bundles, versionCode, expectedSha256);
|
|
67
|
-
const currentTrack = await
|
|
107
|
+
const currentTrack = await getTrackOrEmpty(publisher, packageName, editId, trackName, input.signal, !isWellKnownPlayTrack(trackName));
|
|
108
|
+
const release = newPlayRelease(request, input.record.releaseKey, versionCode);
|
|
109
|
+
const commit = async (status) => {
|
|
110
|
+
const committed = await validateAndCommit({
|
|
111
|
+
publisher, packageName, editId, trackName, versionCode, release, status,
|
|
112
|
+
currentTrack, ...(input.signal === undefined ? {} : { signal: input.signal }),
|
|
113
|
+
...(input.onDraftAppFallback === undefined ? {} : {
|
|
114
|
+
onDraftAppFallback: input.onDraftAppFallback,
|
|
115
|
+
}),
|
|
116
|
+
});
|
|
117
|
+
return result(input, trackName, editId, versionCode, false, committed);
|
|
118
|
+
};
|
|
68
119
|
if (hasVersion(currentTrack.releases, versionCode)) {
|
|
69
120
|
if (existing === undefined) {
|
|
70
|
-
throw new Error(
|
|
121
|
+
throw new Error(`Google Play ${trackName} track has this version without matching bundle bytes.`);
|
|
122
|
+
}
|
|
123
|
+
// Our own edit (resumed), or the live track after that edit was committed
|
|
124
|
+
// and its response lost (a resume that found the edit gone), may hold this
|
|
125
|
+
// release as a draft-app fallback.
|
|
126
|
+
const fallbackDraft = request.status !== 'draft' && (!freshEdit
|
|
127
|
+
? hasVersionWithStatus(currentTrack.releases, versionCode, 'draft')
|
|
128
|
+
: input.resumeEditId !== undefined && (currentTrack.releases ?? []).some((item) => item.status === 'draft' && item.name === input.record.releaseKey
|
|
129
|
+
&& item.versionCodes?.includes(String(versionCode))));
|
|
130
|
+
if (!hasVersionWithStatus(currentTrack.releases, versionCode, request.status)
|
|
131
|
+
&& !fallbackDraft) {
|
|
132
|
+
throw new Error(`Google Play ${trackName} track version is not ${request.status}; inspect it before resubmitting.`);
|
|
71
133
|
}
|
|
72
|
-
|
|
73
|
-
|
|
134
|
+
const observedStatus = fallbackDraft
|
|
135
|
+
&& !hasVersionWithStatus(currentTrack.releases, versionCode, request.status)
|
|
136
|
+
? 'draft' : request.status;
|
|
137
|
+
const observed = (currentTrack.releases ?? []).find((item) => item.status === observedStatus
|
|
138
|
+
&& item.versionCodes?.includes(String(versionCode)));
|
|
139
|
+
if (observed !== undefined && !matchesRequestedRelease(observed, request, observedStatus)) {
|
|
140
|
+
throw new Error(`Google Play ${trackName} track already has this version with another rollout fraction `
|
|
141
|
+
+ 'or other release notes; change it with "mpgd deploy rollout" or inspect it.');
|
|
74
142
|
}
|
|
75
143
|
if (freshEdit) {
|
|
76
|
-
|
|
144
|
+
await persistEdit();
|
|
145
|
+
if (request.mappingFile !== undefined) {
|
|
146
|
+
// The release may have reached Play without this mapping: add it to
|
|
147
|
+
// the version in an edit of its own.
|
|
148
|
+
await uploadMapping(publisher, request, packageName, editId, versionCode, input.signal, requestRootUrl);
|
|
149
|
+
return { ...await commit(observedStatus), alreadyCommitted: true };
|
|
150
|
+
}
|
|
151
|
+
return result(input, trackName, editId, versionCode, true, {
|
|
152
|
+
status: observedStatus,
|
|
153
|
+
draftAppFallback: observedStatus !== request.status,
|
|
154
|
+
});
|
|
77
155
|
}
|
|
78
156
|
// A resumed edit sees its own uncommitted track. Publish it before success.
|
|
79
|
-
await publisher
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return result(input, editId, versionCode, false);
|
|
157
|
+
await uploadMapping(publisher, request, packageName, editId, versionCode, input.signal, requestRootUrl);
|
|
158
|
+
return commit(observedStatus);
|
|
159
|
+
}
|
|
160
|
+
if (playReleaseStartsRollout(request.status, currentTrack.releases)) {
|
|
161
|
+
// Play Console: "Rollout percentage options are unavailable for a first release."
|
|
162
|
+
throw new Error(`Google Play ${trackName} has no completed release yet, so a staged rollout cannot start; `
|
|
163
|
+
+ 'release it with --status completed (or draft) first.');
|
|
87
164
|
}
|
|
165
|
+
await persistEdit();
|
|
88
166
|
if (existing === undefined) {
|
|
89
167
|
try {
|
|
90
168
|
const uploaded = await publisher.uploadBundle({
|
|
@@ -113,23 +191,69 @@ export async function submitVerifiedAndroidBundleWithPublisher(input, publisher,
|
|
|
113
191
|
}
|
|
114
192
|
}
|
|
115
193
|
}
|
|
116
|
-
|
|
117
|
-
name: input.record.releaseKey,
|
|
118
|
-
status: 'completed',
|
|
119
|
-
versionCodes: [String(versionCode)],
|
|
120
|
-
};
|
|
194
|
+
await uploadMapping(publisher, request, packageName, editId, versionCode, input.signal, requestRootUrl);
|
|
121
195
|
const desired = {
|
|
122
196
|
track: trackName,
|
|
123
|
-
releases:
|
|
197
|
+
releases: mergeNewPlayRelease(currentTrack.releases, release).releases,
|
|
124
198
|
};
|
|
199
|
+
await updateTrackChecked(publisher, packageName, editId, trackName, desired, versionCode, request.status, input.signal);
|
|
200
|
+
return commit(request.status);
|
|
201
|
+
}
|
|
202
|
+
async function validateAndCommit(input) {
|
|
203
|
+
const { publisher, packageName, editId } = input;
|
|
204
|
+
try {
|
|
205
|
+
await publisher.validateEdit(packageName, editId, input.signal);
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
if (input.status !== 'draft' && isPlayDraftAppRejection(error)) {
|
|
209
|
+
return commitDraftFallback(input);
|
|
210
|
+
}
|
|
211
|
+
if (isDefinitiveHttpError(error)) {
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
throw new PlaySubmissionUncertainError('validate', editId);
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
await publisher.commitEdit(packageName, editId, input.signal);
|
|
218
|
+
return { status: input.status, draftAppFallback: input.status === 'draft'
|
|
219
|
+
&& input.release.status !== 'draft' };
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
// A definitive draft-app rejection means nothing was committed.
|
|
223
|
+
if (input.status !== 'draft' && isPlayDraftAppRejection(error)) {
|
|
224
|
+
return commitDraftFallback(input);
|
|
225
|
+
}
|
|
226
|
+
// Inserting a new edit invalidates the first edit for this API user. A
|
|
227
|
+
// failed commit response cannot safely be reconciled by creating one.
|
|
228
|
+
throw new PlaySubmissionUncertainError('commit', editId);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Play accepts only draft releases until an app's first release is rolled out
|
|
233
|
+
* from Play Console. Re-stage the same version as a draft in the same edit and
|
|
234
|
+
* commit that, so the operator can roll it out from the console.
|
|
235
|
+
*/
|
|
236
|
+
async function commitDraftFallback(input) {
|
|
237
|
+
input.onDraftAppFallback?.(input.trackName);
|
|
238
|
+
const { userFraction: _fraction, ...withoutFraction } = input.release;
|
|
239
|
+
const draft = { ...withoutFraction, status: 'draft' };
|
|
240
|
+
const others = (input.currentTrack.releases ?? []).filter((release) => !release.versionCodes?.includes(String(input.versionCode)));
|
|
241
|
+
await updateTrackChecked(input.publisher, input.packageName, input.editId, input.trackName, {
|
|
242
|
+
track: input.trackName,
|
|
243
|
+
releases: mergeNewPlayRelease(others, draft).releases,
|
|
244
|
+
}, input.versionCode, 'draft', input.signal);
|
|
245
|
+
await validateAndCommit({ ...input, status: 'draft' });
|
|
246
|
+
return { status: 'draft', draftAppFallback: true };
|
|
247
|
+
}
|
|
248
|
+
async function updateTrackChecked(publisher, packageName, editId, trackName, desired, versionCode, status, signal) {
|
|
125
249
|
try {
|
|
126
|
-
await publisher.updateTrack(packageName, editId, desired,
|
|
250
|
+
await publisher.updateTrack(packageName, editId, desired, signal);
|
|
127
251
|
}
|
|
128
252
|
catch (error) {
|
|
129
253
|
let applied;
|
|
130
254
|
try {
|
|
131
|
-
const observed = await
|
|
132
|
-
applied =
|
|
255
|
+
const observed = await getTrackOrEmpty(publisher, packageName, editId, trackName, signal);
|
|
256
|
+
applied = hasVersionWithStatus(observed.releases, versionCode, status);
|
|
133
257
|
}
|
|
134
258
|
catch {
|
|
135
259
|
throw new PlaySubmissionUncertainError('track-update', editId);
|
|
@@ -138,24 +262,61 @@ export async function submitVerifiedAndroidBundleWithPublisher(input, publisher,
|
|
|
138
262
|
throw error;
|
|
139
263
|
}
|
|
140
264
|
}
|
|
265
|
+
}
|
|
266
|
+
async function uploadMapping(publisher, request, packageName, editId, versionCode, signal, requestRootUrl) {
|
|
267
|
+
if (request.mappingFile === undefined) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// Re-uploading replaces the edit's mapping for this version code, so a lost
|
|
271
|
+
// response is safe to retry on the same edit.
|
|
141
272
|
try {
|
|
142
|
-
await publisher.
|
|
273
|
+
await publisher.uploadDeobfuscationFile({
|
|
274
|
+
packageName,
|
|
275
|
+
editId,
|
|
276
|
+
versionCode,
|
|
277
|
+
mappingFile: request.mappingFile,
|
|
278
|
+
timeoutMs: bundleUploadTimeoutMs,
|
|
279
|
+
...(signal === undefined ? {} : { signal }),
|
|
280
|
+
...(requestRootUrl === undefined ? {} : { requestRootUrl }),
|
|
281
|
+
});
|
|
143
282
|
}
|
|
144
283
|
catch (error) {
|
|
145
284
|
if (isDefinitiveHttpError(error)) {
|
|
146
285
|
throw error;
|
|
147
286
|
}
|
|
148
|
-
throw new PlaySubmissionUncertainError('
|
|
287
|
+
throw new PlaySubmissionUncertainError('deobfuscation-upload', editId);
|
|
149
288
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
289
|
+
}
|
|
290
|
+
async function assertMappingUnchanged(request) {
|
|
291
|
+
if (request.mappingFile !== undefined
|
|
292
|
+
&& await hashMappingFile(request.mappingFile) !== request.mappingSha256) {
|
|
293
|
+
throw new Error('R8 mapping file changed after the release request was created.');
|
|
153
294
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* The release already on the track matches what this request would create:
|
|
298
|
+
* the rollout fraction (a draft-app fallback has none) and, when given, the
|
|
299
|
+
* release notes in any order.
|
|
300
|
+
*/
|
|
301
|
+
export function matchesRequestedRelease(observed, request, observedStatus) {
|
|
302
|
+
const fraction = typeof observed.userFraction === 'number' ? observed.userFraction : undefined;
|
|
303
|
+
if (observedStatus === request.status && fraction !== request.userFraction) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
if (request.releaseNotes === undefined) {
|
|
307
|
+
return true;
|
|
158
308
|
}
|
|
309
|
+
const key = (language, text) => JSON.stringify([language ?? '', text ?? '']);
|
|
310
|
+
const observedNotes = (observed.releaseNotes ?? []).map((note) => key(note.language, note.text))
|
|
311
|
+
.sort();
|
|
312
|
+
const wanted = request.releaseNotes.map((note) => key(note.language, note.text)).sort();
|
|
313
|
+
return observedNotes.length === wanted.length
|
|
314
|
+
&& observedNotes.every((note, index) => note === wanted[index]);
|
|
315
|
+
}
|
|
316
|
+
/** A staged rollout needs a completed release on the track to stage over. */
|
|
317
|
+
function playReleaseStartsRollout(status, releases) {
|
|
318
|
+
return status === 'inProgress'
|
|
319
|
+
&& !(releases ?? []).some((release) => release.status === 'completed');
|
|
159
320
|
}
|
|
160
321
|
async function preflight(input) {
|
|
161
322
|
input.signal?.throwIfAborted();
|
|
@@ -195,25 +356,29 @@ function findVersionBundle(bundles, versionCode, sha256) {
|
|
|
195
356
|
}
|
|
196
357
|
return sameVersion;
|
|
197
358
|
}
|
|
198
|
-
async function
|
|
359
|
+
async function getTrackOrEmpty(publisher, packageName, editId, trackName, signal, mustExist = false) {
|
|
199
360
|
try {
|
|
200
|
-
return await publisher.getTrack(packageName, editId, signal);
|
|
361
|
+
return await publisher.getTrack(packageName, editId, trackName, signal);
|
|
201
362
|
}
|
|
202
363
|
catch (error) {
|
|
203
364
|
if (isHttpStatus(error, 404)) {
|
|
365
|
+
if (mustExist) {
|
|
366
|
+
throw new Error(`Google Play has no closed-testing track named "${trackName}"; create it in Play `
|
|
367
|
+
+ 'Console or check the name.');
|
|
368
|
+
}
|
|
204
369
|
return { track: trackName, releases: [] };
|
|
205
370
|
}
|
|
206
371
|
throw error;
|
|
207
372
|
}
|
|
208
373
|
}
|
|
209
|
-
function isHttpStatus(error, status) {
|
|
374
|
+
export function isHttpStatus(error, status) {
|
|
210
375
|
if (typeof error !== 'object' || error === null) {
|
|
211
376
|
return false;
|
|
212
377
|
}
|
|
213
378
|
const value = error;
|
|
214
379
|
return value.code === status || value.response?.status === status;
|
|
215
380
|
}
|
|
216
|
-
function isDefinitiveHttpError(error) {
|
|
381
|
+
export function isDefinitiveHttpError(error) {
|
|
217
382
|
if (typeof error !== 'object' || error === null) {
|
|
218
383
|
return false;
|
|
219
384
|
}
|
|
@@ -230,16 +395,18 @@ function isDefinitiveHttpError(error) {
|
|
|
230
395
|
function hasVersion(releases, versionCode) {
|
|
231
396
|
return releases?.some((release) => release.versionCodes?.includes(String(versionCode))) ?? false;
|
|
232
397
|
}
|
|
233
|
-
function
|
|
234
|
-
return releases?.some((release) => release.status ===
|
|
398
|
+
function hasVersionWithStatus(releases, versionCode, status) {
|
|
399
|
+
return releases?.some((release) => release.status === status
|
|
235
400
|
&& release.versionCodes?.includes(String(versionCode))) ?? false;
|
|
236
401
|
}
|
|
237
|
-
function result(input, editId, versionCode, alreadyCommitted) {
|
|
402
|
+
function result(input, trackName, editId, versionCode, alreadyCommitted, committed) {
|
|
238
403
|
return {
|
|
239
404
|
status: 'committed',
|
|
240
405
|
packageName: input.packageName,
|
|
241
406
|
editId,
|
|
242
407
|
track: trackName,
|
|
408
|
+
releaseStatus: committed.status,
|
|
409
|
+
draftAppFallback: committed.draftAppFallback,
|
|
243
410
|
versionCode,
|
|
244
411
|
bundleSha256: input.record.artifactSha256,
|
|
245
412
|
alreadyCommitted,
|
|
@@ -3,10 +3,22 @@ export interface PlayBundle {
|
|
|
3
3
|
readonly versionCode?: number | null;
|
|
4
4
|
readonly sha256?: string | null;
|
|
5
5
|
}
|
|
6
|
+
/** Android Publisher v3 `LocalizedText`: a BCP-47 language tag and its text. */
|
|
7
|
+
export interface PlayLocalizedText {
|
|
8
|
+
readonly language?: string | null | undefined;
|
|
9
|
+
readonly text?: string | null | undefined;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Android Publisher v3 `Release`. Releases read from Play keep every field Play
|
|
13
|
+
* returned (for example `countryTargeting`), so a track update that sends them
|
|
14
|
+
* back leaves them unchanged.
|
|
15
|
+
*/
|
|
6
16
|
export interface PlayTrackRelease {
|
|
7
17
|
readonly name?: string | null | undefined;
|
|
8
18
|
readonly status?: string | null | undefined;
|
|
9
19
|
readonly versionCodes?: readonly string[] | null | undefined;
|
|
20
|
+
readonly userFraction?: number | null | undefined;
|
|
21
|
+
readonly releaseNotes?: readonly PlayLocalizedText[] | null | undefined;
|
|
10
22
|
}
|
|
11
23
|
export interface PlayTrack {
|
|
12
24
|
readonly track?: string | null | undefined;
|
|
@@ -23,8 +35,21 @@ export interface PlayPublisher {
|
|
|
23
35
|
readonly requestRootUrl?: string;
|
|
24
36
|
readonly signal?: AbortSignal;
|
|
25
37
|
}): Promise<PlayBundle>;
|
|
26
|
-
|
|
38
|
+
/** `edits.deobfuscationfiles.upload` with type `proguard` (an R8/ProGuard mapping). */
|
|
39
|
+
uploadDeobfuscationFile(input: {
|
|
40
|
+
readonly packageName: string;
|
|
41
|
+
readonly editId: string;
|
|
42
|
+
readonly versionCode: number;
|
|
43
|
+
readonly mappingFile: string;
|
|
44
|
+
readonly timeoutMs: number;
|
|
45
|
+
readonly requestRootUrl?: string;
|
|
46
|
+
readonly signal?: AbortSignal;
|
|
47
|
+
}): Promise<void>;
|
|
48
|
+
getTrack(packageName: string, editId: string, track: string, signal?: AbortSignal): Promise<PlayTrack>;
|
|
49
|
+
/** Updates the track named by `track.track`. */
|
|
27
50
|
updateTrack(packageName: string, editId: string, track: PlayTrack, signal?: AbortSignal): Promise<void>;
|
|
28
51
|
validateEdit(packageName: string, editId: string, signal?: AbortSignal): Promise<void>;
|
|
29
52
|
commitEdit(packageName: string, editId: string, signal?: AbortSignal): Promise<void>;
|
|
53
|
+
/** Discards an uncommitted edit (`edits.delete`), used after a read-only dry run. */
|
|
54
|
+
deleteEdit(packageName: string, editId: string, signal?: AbortSignal): Promise<void>;
|
|
30
55
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { PlayTrackRelease } from './play-publisher-port.js';
|
|
2
|
+
/**
|
|
3
|
+
* Android Publisher v3 `Release.status` values the kit writes. `halted` is set
|
|
4
|
+
* only on an existing rollout (`mpgd deploy rollout --action halt`): a new
|
|
5
|
+
* bundle cannot start out halted.
|
|
6
|
+
* https://developers.google.com/android-publisher/api-ref/rest/v3/edits.tracks#status
|
|
7
|
+
*/
|
|
8
|
+
export type PlayReleaseStatus = 'draft' | 'completed' | 'inProgress' | 'halted';
|
|
9
|
+
export type PlayNewReleaseStatus = Exclude<PlayReleaseStatus, 'halted'>;
|
|
10
|
+
/** The kit's original destination; Play's tracks guide also lists this track as `qa`. */
|
|
11
|
+
export declare const defaultPlayTrack = "internal";
|
|
12
|
+
/** Play Console: "up to 500 Unicode characters per language". */
|
|
13
|
+
export declare const maximumPlayReleaseNoteCharacters = 500;
|
|
14
|
+
/** What one `deploy submit`/`deploy run` asks Play to do with the new bundle. */
|
|
15
|
+
export interface PlayReleaseRequest {
|
|
16
|
+
readonly track: string;
|
|
17
|
+
readonly status: PlayNewReleaseStatus;
|
|
18
|
+
/** Required for `inProgress`: 0 < fraction < 1. */
|
|
19
|
+
readonly userFraction?: number;
|
|
20
|
+
readonly releaseNotes?: readonly PlayReleaseNote[];
|
|
21
|
+
/** Absolute path of an R8/ProGuard `mapping.txt` for this exact build. */
|
|
22
|
+
readonly mappingFile?: string;
|
|
23
|
+
/** SHA-256 of `mappingFile`, fixed when the request is created. */
|
|
24
|
+
readonly mappingSha256?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface PlayReleaseNote {
|
|
27
|
+
readonly language: string;
|
|
28
|
+
readonly text: string;
|
|
29
|
+
}
|
|
30
|
+
export declare const defaultPlayReleaseRequest: PlayReleaseRequest;
|
|
31
|
+
export interface CreatePlayReleaseRequestInput {
|
|
32
|
+
readonly track?: string | undefined;
|
|
33
|
+
readonly status?: string | undefined;
|
|
34
|
+
readonly userFraction?: string | number | undefined;
|
|
35
|
+
readonly releaseNotesFile?: string | undefined;
|
|
36
|
+
readonly mappingFile?: string | undefined;
|
|
37
|
+
}
|
|
38
|
+
/** Validate CLI release options; with none it returns the internal/completed default. */
|
|
39
|
+
export declare function createPlayReleaseRequest(input: CreatePlayReleaseRequestInput): Promise<PlayReleaseRequest>;
|
|
40
|
+
/** Re-check a programmatic request; the CLI path builds it with createPlayReleaseRequest. */
|
|
41
|
+
export declare function assertPlayReleaseRequest(request: PlayReleaseRequest): void;
|
|
42
|
+
export declare function assertPlayTrackName(track: string): void;
|
|
43
|
+
/** `production` and form-factor production tracks such as `wear:production`. */
|
|
44
|
+
export declare function isProductionTrack(track: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* A default track (`internal`/`qa`, `alpha`, `beta`, `production`, optionally
|
|
47
|
+
* form-factor prefixed). Other names are closed-testing tracks, which "are
|
|
48
|
+
* created manually", so a missing one is a typo rather than an empty track.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isWellKnownPlayTrack(track: string): boolean;
|
|
51
|
+
/** Production releases and every staged rollout need an explicit confirmation flag. */
|
|
52
|
+
export declare function playReleaseNeedsConfirmation(request: Pick<PlayReleaseRequest, 'track' | 'status'>): boolean;
|
|
53
|
+
export declare function assertPlayReleaseConfirmed(request: Pick<PlayReleaseRequest, 'track' | 'status'>, confirmed: boolean): void;
|
|
54
|
+
export declare function isDefaultPlayReleaseRequest(request: PlayReleaseRequest): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Binds a submission checkpoint to the release options that started it, so a
|
|
57
|
+
* resumed edit is never committed with another track or status. The original
|
|
58
|
+
* internal/completed request has no digest, which keeps its checkpoints as before.
|
|
59
|
+
*/
|
|
60
|
+
export declare function playReleaseRequestDigest(request: PlayReleaseRequest): string | undefined;
|
|
61
|
+
export declare function parsePlayUserFraction(value: string | number): number;
|
|
62
|
+
/** Android Publisher v3 `Release.userFraction`: "0 < fraction < 1". */
|
|
63
|
+
export declare function assertPlayUserFraction(fraction: number): void;
|
|
64
|
+
export declare function readPlayReleaseNotesFile(file: string): PlayReleaseNote[];
|
|
65
|
+
/**
|
|
66
|
+
* Accepts the Play Console's own language-tag format, one block per language:
|
|
67
|
+
*
|
|
68
|
+
* <en-US>
|
|
69
|
+
* Bug fixes.
|
|
70
|
+
* </en-US>
|
|
71
|
+
*
|
|
72
|
+
* or JSON: `{ "en-US": "Bug fixes." }` or `[{ "language": "en-US", "text": "Bug fixes." }]`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function parsePlayReleaseNotes(source: string): PlayReleaseNote[];
|
|
75
|
+
export declare function assertPlayReleaseNotes(notes: readonly PlayReleaseNote[]): void;
|
|
76
|
+
export declare function hashMappingFile(mappingFile: string): Promise<string>;
|
|
77
|
+
/** Build the Release body for a new bundle; the default matches the original body exactly. */
|
|
78
|
+
export declare function newPlayRelease(request: PlayReleaseRequest, name: string, versionCode: number): PlayTrackRelease;
|
|
79
|
+
/**
|
|
80
|
+
* Add a new release to the releases Play returned for the track. Every other
|
|
81
|
+
* release is sent back unchanged, except the ones the new release supersedes:
|
|
82
|
+
* a completed or staged release replaces an in-progress or halted rollout, and
|
|
83
|
+
* a draft replaces an older draft. A track with only completed releases (the
|
|
84
|
+
* usual internal track) therefore gets exactly the original appended body.
|
|
85
|
+
*/
|
|
86
|
+
export declare function mergeNewPlayRelease(current: readonly PlayTrackRelease[] | null | undefined, release: PlayTrackRelease): {
|
|
87
|
+
readonly releases: PlayTrackRelease[];
|
|
88
|
+
readonly superseded: PlayTrackRelease[];
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* The Play Developer API answers "Only releases with status draft may be
|
|
92
|
+
* created on draft app." while an app has never been published. Google's API
|
|
93
|
+
* reference does not document it; it is reported by fastlane, the
|
|
94
|
+
* upload-google-play action and others, on edits.commit. Only a definitive 4xx
|
|
95
|
+
* with that text counts: the edit was not committed.
|
|
96
|
+
*/
|
|
97
|
+
export declare function isPlayDraftAppRejection(error: unknown): boolean;
|