@mpgd/cli 0.67.0 → 0.68.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.
@@ -19,8 +19,12 @@ export declare const ascListTimeoutMs: number;
19
19
  export declare const ascUploadTimeoutMs: number;
20
20
  /** asc exit code for an App Store Connect 404 (`cmd/exit_codes.go`, 5.5.0). */
21
21
  export declare const ascExitNotFound = 4;
22
- /** Copy and verify the pinned binary into a private directory; `close` removes it. */
23
- export declare function openAscSession(credential: AscCredential, signal?: AbortSignal): Promise<{
22
+ /**
23
+ * Copy and verify the pinned binary into a private directory; `close` removes
24
+ * it. `extraSecretValues` are redacted from asc output like the API key (for
25
+ * example a TestFlight review demo account password).
26
+ */
27
+ export declare function openAscSession(credential: AscCredential, signal?: AbortSignal, extraSecretValues?: readonly string[]): Promise<{
24
28
  readonly run: AscCommandRunner;
25
29
  close(): void;
26
30
  }>;
@@ -9,8 +9,12 @@ export const ascUploadTimeoutMs = 10 * 60_000;
9
9
  const maxJsonBytes = 8 * 1024 * 1024;
10
10
  /** asc exit code for an App Store Connect 404 (`cmd/exit_codes.go`, 5.5.0). */
11
11
  export const ascExitNotFound = 4;
12
- /** Copy and verify the pinned binary into a private directory; `close` removes it. */
13
- export async function openAscSession(credential, signal) {
12
+ /**
13
+ * Copy and verify the pinned binary into a private directory; `close` removes
14
+ * it. `extraSecretValues` are redacted from asc output like the API key (for
15
+ * example a TestFlight review demo account password).
16
+ */
17
+ export async function openAscSession(credential, signal, extraSecretValues = []) {
14
18
  if (!path.isAbsolute(credential.ascBinary)) {
15
19
  throw new Error('asc binary must be an absolute file path.');
16
20
  }
@@ -30,7 +34,12 @@ export async function openAscSession(credential, signal) {
30
34
  timeoutMs,
31
35
  maxOutputBytes: maxJsonBytes,
32
36
  captureMachineStdout: true,
33
- secretValues: [credential.apiKeyId, credential.apiIssuerId, credential.apiPrivateKeyBase64],
37
+ secretValues: [
38
+ credential.apiKeyId,
39
+ credential.apiIssuerId,
40
+ credential.apiPrivateKeyBase64,
41
+ ...extraSecretValues.filter((value) => value !== ''),
42
+ ],
34
43
  signal,
35
44
  });
36
45
  if (result.truncated || result.machineStdout === undefined) {
package/dist/index.d.ts CHANGED
@@ -17,11 +17,16 @@ export { StorePlanStaleError } from './store-sync.js';
17
17
  export { applyStorePresence, planStorePresence, saveStorePresencePlan, type StorePresenceApplyOptions, type StorePresencePlanFile, type StorePresencePlanOptions, type StorePresencePorts, type StorePresenceStore, } from './store-presence-command.js';
18
18
  export { applyPlayPresence, formatPlayPresencePlan, planPlayPresence, PlayPresenceApplyError, type PlannedPlayImageSlot, type PlannedPlayLanguage, type PlayPresenceApplyResult, type PlayPresencePlan, type PlayPresencePlanInput, } from './store-presence-play.js';
19
19
  export { applyAppStorePresence, AppStorePresenceApplyError, formatAppStorePresencePlan, planAppStorePresence, type AppStorePresenceApplyResult, type AppStorePresencePlan, type AppStorePresencePlanInput, type PlannedAppStoreLocale, type PlannedAscScreenshotSet, } from './store-presence-app-store.js';
20
+ export { applyAppStoreReleaseCommand, changeAppStorePhasedReleaseCommand, planAppStoreReleaseCommand, publishAppStoreVersionCommand, readAppStoreReleaseStatusCommand, saveAppStoreReleasePlan, type AppStoreReleaseApplyOptions, type AppStoreReleaseCommandPorts, type AppStoreReleasePlanFile, type AppStoreReleasePlanOptions, type AppStoreReleaseVersionOptions, type ReleaseTypeOption, } from './app-store-release-command.js';
21
+ export { applyAppStoreRelease, AppStoreReleaseApplyError, changeAppStorePhasedRelease, formatAppStoreReleasePlan, planAppStoreRelease, publishAppStoreVersion, readAppStoreReleaseStatus, type AppStoreReleaseApplyResult, type AppStoreReleasePlan, type AppStoreReleasePlanInput, type AppStoreReleaseRequest, type AppStoreReleaseStatus, type PhasedReleaseAction, } from './app-store-release.js';
22
+ export type { AscReleasePort } from './app-store-release-port.js';
20
23
  export type { AscAppInfo, AscAppInfoLocalization, AscAppStoreVersion, AscScreenshot, AscScreenshotSet, AscStorePresencePort, AscVersionLocalization, AscVersionLocalizationFields, } from './app-store-presence-port.js';
21
24
  export type { PlayAppDetails, PlayImage, PlayImageType, PlayListing, PlayStoreListingPort, } from './play-listing-port.js';
22
25
  export { PlayPromoteUncertainError, promotePlayRelease, type PlayPromoteInput, type PlayPromoteResult, } from './play-release-promote.js';
23
26
  export { PlayRolloutUncertainError, changePlayRollout, type PlayRolloutAction, type PlayRolloutInput, type PlayRolloutResult, } from './play-release-rollout.js';
24
27
  export { IosSubmissionUncertainError, submitVerifiedIosBuild, type IosTestFlightStatus, type IosTestFlightSubmissionInput, type IosTestFlightSubmissionResult, } from './testflight-submission.js';
28
+ export { applyTestFlightDistribution, planTestFlightDistribution, readTestFlightBuildStatus, type TestFlightBetaReviewInput, type TestFlightBetaReviewStatus, type TestFlightBuildTarget, type TestFlightDistributionReport, type TestFlightDistributionRequest, type TestFlightStatusReport, } from './testflight-distribution.js';
29
+ export type { TestFlightDistributionPort } from './testflight-distribution-port.js';
25
30
  export { createBrowserGameplayE2EDriver, type BrowserGameplayE2EInspectInput, type BrowserGameplayE2EPage, type BrowserGameplayE2EScreenshotOptions, type BrowserGameplayE2EViewport, type CreateBrowserGameplayE2EDriverInput, } from './browser-gameplay-e2e.js';
26
31
  export { collectGameplayE2EPathEvidence, defaultGameplayE2EReportFile, maximumGameplayE2EStates, parseGameplayE2EPlan, readGameplayE2EPlan, renderGameplayE2EMarkdown, resolveGameplayE2EReportFile, runGameplayE2E, type GameplayE2EAction, type GameplayE2EActionResult, type GameplayE2EDriver, type GameplayE2EHashLimits, type GameplayE2EInputAction, type GameplayE2EObservation, type GameplayE2EPauseResumeAction, type GameplayE2EPathEvidence, type GameplayE2EPlan, type GameplayE2EReport, type GameplayE2EResultStatus, type GameplayE2EState, type GameplayE2EStateResult, type GameplayE2EStatus, type RunGameplayE2EInput, type RunGameplayE2EResult, } from './gameplay-e2e.js';
27
32
  export { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, isSupportedHostedPwaProfile, verifyHostedPwaDeployment, type CloudflarePagesDeploymentProfile, type HostedPwaDeploymentHost, type HostedPwaDeploymentVerification, type VerifyHostedPwaDeploymentInput, } from './hosted-pwa-deployment.js';
package/dist/index.js CHANGED
@@ -33,7 +33,9 @@ import { runMicrosoftStoreSubmissionPreflight } from './microsoft-store-submissi
33
33
  import { defaultOfflinePlaytestArtifactDir, defaultOfflinePlaytestMaximumBytes, defaultOfflinePlaytestOutputDir, runOfflinePlaytestPackaging, } from './offline-playtest.js';
34
34
  import { targetConfigExtensionsFileEnv, targetConfigMatrixFileEnv } from './target-config-env.js';
35
35
  import { readStoreCredential, runNativeDeployment } from './native-deploy-run.js';
36
- import { isNativeDeployApproved, planRecordedAndroidSubmission, submitRecordedNativeTarget, } from './native-deploy-submission.js';
36
+ import { isNativeDeployApproved, planRecordedAndroidSubmission, readRecordedIosBuild, submitRecordedNativeTarget, } from './native-deploy-submission.js';
37
+ import { applyTestFlightDistribution, parseTestFlightGroups, planTestFlightDistribution, readTestFlightBetaReviewFile, readTestFlightBuildStatus, readTestFlightWhatToTestFile, } from './testflight-distribution.js';
38
+ import { openAscTestFlightDistributionSession } from './testflight-distribution-asc.js';
37
39
  import { createPlayReleaseRequest, parsePlayUserFraction, readPlayReleaseNotesFile, } from './play-release-options.js';
38
40
  import { promotePlayRelease } from './play-release-promote.js';
39
41
  import { applyStoreProducts, planStoreProducts, saveStoreProductPlan, } from './store-products-command.js';
@@ -42,6 +44,8 @@ import { formatPlayProductPlan } from './store-products-play.js';
42
44
  import { applyStorePresence, planStorePresence, saveStorePresencePlan, } from './store-presence-command.js';
43
45
  import { formatPlayPresencePlan } from './store-presence-play.js';
44
46
  import { formatAppStorePresencePlan } from './store-presence-app-store.js';
47
+ import { applyAppStoreReleaseCommand, changeAppStorePhasedReleaseCommand, planAppStoreReleaseCommand, publishAppStoreVersionCommand, readAppStoreReleaseStatusCommand, saveAppStoreReleasePlan, } from './app-store-release-command.js';
48
+ import { formatAppStoreReleasePlan } from './app-store-release.js';
45
49
  import { changePlayRollout } from './play-release-rollout.js';
46
50
  import { readNativeReleaseStatus as readNativeReleaseStatusForCli } from './release-state.js';
47
51
  import { uploadRecordedCrashSymbols } from './crash-symbols-upload.js';
@@ -64,9 +68,12 @@ export { StorePlanStaleError } from './store-sync.js';
64
68
  export { applyStorePresence, planStorePresence, saveStorePresencePlan, } from './store-presence-command.js';
65
69
  export { applyPlayPresence, formatPlayPresencePlan, planPlayPresence, PlayPresenceApplyError, } from './store-presence-play.js';
66
70
  export { applyAppStorePresence, AppStorePresenceApplyError, formatAppStorePresencePlan, planAppStorePresence, } from './store-presence-app-store.js';
71
+ export { applyAppStoreReleaseCommand, changeAppStorePhasedReleaseCommand, planAppStoreReleaseCommand, publishAppStoreVersionCommand, readAppStoreReleaseStatusCommand, saveAppStoreReleasePlan, } from './app-store-release-command.js';
72
+ export { applyAppStoreRelease, AppStoreReleaseApplyError, changeAppStorePhasedRelease, formatAppStoreReleasePlan, planAppStoreRelease, publishAppStoreVersion, readAppStoreReleaseStatus, } from './app-store-release.js';
67
73
  export { PlayPromoteUncertainError, promotePlayRelease, } from './play-release-promote.js';
68
74
  export { PlayRolloutUncertainError, changePlayRollout, } from './play-release-rollout.js';
69
75
  export { IosSubmissionUncertainError, submitVerifiedIosBuild, } from './testflight-submission.js';
76
+ export { applyTestFlightDistribution, planTestFlightDistribution, readTestFlightBuildStatus, } from './testflight-distribution.js';
70
77
  export { createBrowserGameplayE2EDriver, } from './browser-gameplay-e2e.js';
71
78
  export { collectGameplayE2EPathEvidence, defaultGameplayE2EReportFile, maximumGameplayE2EStates, parseGameplayE2EPlan, readGameplayE2EPlan, renderGameplayE2EMarkdown, resolveGameplayE2EReportFile, runGameplayE2E, } from './gameplay-e2e.js';
72
79
  export { cloudflarePagesDeploymentProfiles, hostedPwaDeploymentHosts, isSupportedHostedPwaProfile, verifyHostedPwaDeployment, } from './hosted-pwa-deployment.js';
@@ -675,6 +682,92 @@ const deployCommand = defineI18n({
675
682
  console.info(JSON.stringify(result, null, 2));
676
683
  },
677
684
  }),
685
+ testflight: defineI18n({
686
+ name: 'testflight',
687
+ description: 'Distribute a submitted iOS build to more TestFlight groups and read its App Review state.',
688
+ resource: commandResource({
689
+ en: 'Distribute a submitted iOS build to more TestFlight groups and read its App Review state.',
690
+ ko: '제출한 iOS 빌드를 다른 TestFlight 그룹에 배포하고 앱 심사 상태를 조회합니다.',
691
+ }),
692
+ subCommands: {
693
+ distribute: defineI18n({
694
+ name: 'distribute',
695
+ description: 'Add the build to TestFlight groups, set What to Test and submit external groups for TestFlight App Review.',
696
+ resource: commandResource({
697
+ en: 'Add the build to TestFlight groups, set What to Test and submit external groups for TestFlight App Review.',
698
+ ko: '빌드를 TestFlight 그룹에 추가하고 테스트 내용을 설정하며, 외부 그룹이면 TestFlight 앱 심사에 제출합니다.',
699
+ }),
700
+ args: {
701
+ plan: { type: 'string', required: true, description: 'Saved deployment plan JSON (bundle ID and credential).' },
702
+ game: { type: 'string', required: false, description: 'Current game directory if the plan moved.' },
703
+ 'game-id': { type: 'string', required: true, description: 'Explicit release ledger game ID.' },
704
+ release: { type: 'string', required: true, description: 'Explicit release ID whose recorded iOS build is distributed.' },
705
+ groups: {
706
+ type: 'string', required: true,
707
+ description: 'Comma-separated TestFlight group IDs or exact names; external groups need TestFlight App Review.',
708
+ },
709
+ 'what-to-test': {
710
+ type: 'string', required: false,
711
+ description: 'What to Test file, <en-US>…</en-US> blocks or JSON (the --release-notes format); required for a first external group.',
712
+ },
713
+ 'beta-review': {
714
+ type: 'string', required: false,
715
+ description: 'TestFlight review details JSON: contact, notes and a demo account by environment variable names.',
716
+ },
717
+ approve: { type: 'boolean', required: false, description: 'Approve the group changes and the review submission.' },
718
+ 'dry-run': { type: 'boolean', required: false, description: 'Read App Store Connect and print the planned changes; write nothing.' },
719
+ },
720
+ run: async (ctx) => {
721
+ const dryRun = ctx.values['dry-run'] === true;
722
+ if (!dryRun && ctx.values.approve !== true) {
723
+ throw new Error('TestFlight distribution requires --approve (or --dry-run to preview it).');
724
+ }
725
+ const groups = parseTestFlightGroups(readRequiredCliOption(ctx.values.groups, '--groups'));
726
+ const whatToTestFile = readOptionalString(ctx.values['what-to-test']);
727
+ const betaReviewFile = readOptionalString(ctx.values['beta-review']);
728
+ const betaReview = betaReviewFile === undefined
729
+ ? undefined
730
+ : readTestFlightBetaReviewFile(betaReviewFile, process.env, { requireValues: !dryRun });
731
+ const context = await readTestFlightCliContext(ctx.values);
732
+ const request = {
733
+ ...context.target,
734
+ groups,
735
+ ...(whatToTestFile === undefined ? {} : { whatToTest: readTestFlightWhatToTestFile(whatToTestFile) }),
736
+ ...(betaReview === undefined ? {} : { betaReview }),
737
+ };
738
+ const secrets = betaReview?.demoAccount?.password === undefined ? [] : [betaReview.demoAccount.password];
739
+ const result = await withTestFlightCliPort(context.credential, secrets, (port) => dryRun
740
+ ? planTestFlightDistribution(port, request)
741
+ : applyTestFlightDistribution(port, request));
742
+ console.info(JSON.stringify(result, null, 2));
743
+ if (!dryRun && (result.betaReview.status === 'action-required'
744
+ || result.betaReview.status === 'rejected')) {
745
+ throw new Error(`TestFlight distribution needs action in App Store Connect (${result.betaReview.status}): `
746
+ + (result.detail ?? 'see App Review > TestFlight.'));
747
+ }
748
+ },
749
+ }),
750
+ status: defineI18n({
751
+ name: 'status',
752
+ description: "Read the build's TestFlight groups and App Review state; never writes.",
753
+ resource: commandResource({
754
+ en: "Read the build's TestFlight groups and App Review state; never writes.",
755
+ ko: '빌드의 TestFlight 그룹과 앱 심사 상태를 조회합니다. 쓰지 않습니다.',
756
+ }),
757
+ args: {
758
+ plan: { type: 'string', required: true, description: 'Saved deployment plan JSON (bundle ID and credential).' },
759
+ game: { type: 'string', required: false, description: 'Current game directory if the plan moved.' },
760
+ 'game-id': { type: 'string', required: true, description: 'Explicit release ledger game ID.' },
761
+ release: { type: 'string', required: true, description: 'Explicit release ID whose recorded iOS build is read.' },
762
+ },
763
+ run: async (ctx) => {
764
+ const { target, credential } = await readTestFlightCliContext(ctx.values);
765
+ const result = await withTestFlightCliPort(credential, [], (port) => readTestFlightBuildStatus(port, target));
766
+ console.info(JSON.stringify(result, null, 2));
767
+ },
768
+ }),
769
+ },
770
+ }),
678
771
  run: defineI18n({
679
772
  name: 'run',
680
773
  description: 'Reserve, build, verify and submit native internal-test releases.',
@@ -906,9 +999,167 @@ const storeCommand = defineI18n({
906
999
  }),
907
1000
  },
908
1001
  }),
1002
+ release: defineI18n({
1003
+ name: 'release',
1004
+ description: 'App Store: prepare an iOS version, submit it for review, and release it.',
1005
+ resource: commandResource({
1006
+ en: 'App Store: prepare an iOS version, submit it for review, and release it.',
1007
+ ko: 'App Store: iOS 버전을 준비해 심사에 제출하고 출시합니다.',
1008
+ }),
1009
+ subCommands: {
1010
+ plan: defineI18n({
1011
+ name: 'plan',
1012
+ description: 'Read App Store Connect and print the release plan; never writes to the store.',
1013
+ resource: commandResource({
1014
+ en: 'Read App Store Connect and print the release plan; never writes to the store.',
1015
+ ko: 'App Store Connect를 읽고 출시 계획을 출력합니다. 스토어에 쓰지 않습니다.',
1016
+ }),
1017
+ args: {
1018
+ game: { type: 'string', required: false, description: 'Game project directory.' },
1019
+ profile: { type: 'string', required: false, description: 'Deployment profile naming the app and credential; defaults to beta.' },
1020
+ presence: { type: 'string', required: false, description: 'Store presence file path, relative to the current directory; defaults to mpgd.store-presence.json in the game.' },
1021
+ 'marketing-version': { type: 'string', required: false, description: 'Marketing version to submit, for example 1.0.1.' },
1022
+ 'build-number': { type: 'string', required: false, description: 'Build number to submit; defaults to the newest build of the version.' },
1023
+ 'game-id': { type: 'string', required: false, description: 'Release ledger game ID of the build deploy run recorded (with --release).' },
1024
+ release: { type: 'string', required: false, description: 'Release ID of the build deploy run recorded (with --game-id).' },
1025
+ 'release-type': { type: 'string', required: false, description: 'manual (default), after-approval or scheduled.' },
1026
+ 'release-date': { type: 'string', required: false, description: 'Earliest release date for scheduled, ISO 8601 with time and offset.' },
1027
+ 'phased-release': { type: 'boolean', required: false, description: 'Release over 7 days (phased release); off by default.' },
1028
+ 'skip-iap': { type: 'string', required: false, description: 'Comma-separated in-app purchase product IDs to leave out of this submission.' },
1029
+ 'rotate-demo-password': { type: 'boolean', required: false, description: 'Send the demo account password again even when App Store Connect has one.' },
1030
+ out: { type: 'string', required: false, description: 'New plan JSON path for apply; existing files are not overwritten.' },
1031
+ },
1032
+ run: async (ctx) => {
1033
+ const optional = (key) => readOptionalString(ctx.values[key]);
1034
+ const skipIaps = optional('skip-iap')?.split(',').map((entry) => entry.trim()).filter((entry) => entry !== '');
1035
+ const presence = optional('presence');
1036
+ const version = optional('marketing-version');
1037
+ const buildNumber = optional('build-number');
1038
+ const gameId = optional('game-id');
1039
+ const release = optional('release');
1040
+ const releaseType = optional('release-type');
1041
+ const releaseDate = optional('release-date');
1042
+ const result = await planAppStoreReleaseCommand({
1043
+ game: optional('game') ?? '.',
1044
+ profile: optional('profile') ?? 'beta',
1045
+ ...(presence === undefined ? {} : { presenceFile: presence }),
1046
+ ...(version === undefined ? {} : { version }),
1047
+ ...(buildNumber === undefined ? {} : { buildNumber }),
1048
+ ...(gameId === undefined ? {} : { gameId }),
1049
+ ...(release === undefined ? {} : { release }),
1050
+ ...(releaseType === undefined ? {} : { releaseType: releaseType }),
1051
+ ...(releaseDate === undefined ? {} : { releaseDate }),
1052
+ ...(ctx.values['phased-release'] === true ? { phasedRelease: true } : {}),
1053
+ ...(skipIaps === undefined ? {} : { skipIaps }),
1054
+ ...(ctx.values['rotate-demo-password'] === true ? { rotateDemoPassword: true } : {}),
1055
+ });
1056
+ console.info(formatAppStoreReleasePlan(result.plan));
1057
+ const out = optional('out');
1058
+ if (out !== undefined) {
1059
+ console.info(`Wrote store release plan: ${saveAppStoreReleasePlan(out, result)}`);
1060
+ }
1061
+ },
1062
+ }),
1063
+ apply: defineI18n({
1064
+ name: 'apply',
1065
+ description: 'Perform the reviewed release plan; refuses when App Store Connect or the files changed since.',
1066
+ resource: commandResource({
1067
+ en: 'Perform the reviewed release plan; refuses when App Store Connect or the files changed since.',
1068
+ ko: '검토한 출시 계획을 수행합니다. 이후 App Store Connect나 파일이 바뀌었으면 거부합니다.',
1069
+ }),
1070
+ args: {
1071
+ plan: { type: 'string', required: true, description: 'Plan JSON written by "store release plan --out".' },
1072
+ game: { type: 'string', required: false, description: 'Current game directory if the game moved.' },
1073
+ approve: { type: 'boolean', required: false, description: 'Approve the App Store Connect writes and the review submission.' },
1074
+ 'dry-run': { type: 'boolean', required: false, description: 'Re-check the plan against App Store Connect and print the writes; write nothing.' },
1075
+ },
1076
+ run: async (ctx) => {
1077
+ const game = readOptionalString(ctx.values.game);
1078
+ const result = await applyAppStoreReleaseCommand({
1079
+ planFile: readRequiredCliOption(ctx.values.plan, '--plan'),
1080
+ ...(game === undefined ? {} : { game }),
1081
+ approved: ctx.values.approve === true,
1082
+ ...(ctx.values['dry-run'] === true ? { dryRun: true } : {}),
1083
+ });
1084
+ console.info(JSON.stringify(result, null, 2));
1085
+ },
1086
+ }),
1087
+ status: defineI18n({
1088
+ name: 'status',
1089
+ description: 'Report where the iOS version and its review submission stand; reads only.',
1090
+ resource: commandResource({
1091
+ en: 'Report where the iOS version and its review submission stand; reads only.',
1092
+ ko: 'iOS 버전과 심사 제출 상태를 보고합니다. 읽기만 합니다.',
1093
+ }),
1094
+ args: {
1095
+ game: { type: 'string', required: false, description: 'Game project directory.' },
1096
+ profile: { type: 'string', required: false, description: 'Deployment profile naming the app and credential; defaults to beta.' },
1097
+ 'marketing-version': { type: 'string', required: false, description: 'Marketing version; defaults to the newest iOS version.' },
1098
+ },
1099
+ run: async (ctx) => {
1100
+ const version = readOptionalString(ctx.values['marketing-version']);
1101
+ const result = await readAppStoreReleaseStatusCommand({
1102
+ game: readOptionalString(ctx.values.game) ?? '.',
1103
+ profile: readOptionalString(ctx.values.profile) ?? 'beta',
1104
+ ...(version === undefined ? {} : { version }),
1105
+ });
1106
+ console.info(result.summary);
1107
+ console.info(JSON.stringify(result, null, 2));
1108
+ },
1109
+ }),
1110
+ publish: defineI18n({
1111
+ name: 'publish',
1112
+ description: 'Release an approved version that waits for a manual release.',
1113
+ resource: commandResource({
1114
+ en: 'Release an approved version that waits for a manual release.',
1115
+ ko: '수동 출시를 기다리는 승인된 버전을 출시합니다.',
1116
+ }),
1117
+ args: {
1118
+ game: { type: 'string', required: false, description: 'Game project directory.' },
1119
+ profile: { type: 'string', required: false, description: 'Deployment profile naming the app and credential; defaults to beta.' },
1120
+ 'marketing-version': { type: 'string', required: true, description: 'Marketing version to release.' },
1121
+ approve: { type: 'boolean', required: false, description: 'Approve releasing the version to players.' },
1122
+ },
1123
+ run: async (ctx) => {
1124
+ const result = await publishAppStoreVersionCommand({
1125
+ game: readOptionalString(ctx.values.game) ?? '.',
1126
+ profile: readOptionalString(ctx.values.profile) ?? 'beta',
1127
+ version: readRequiredCliOption(ctx.values['marketing-version'], '--marketing-version'),
1128
+ approved: ctx.values.approve === true,
1129
+ });
1130
+ console.info(JSON.stringify(result, null, 2));
1131
+ },
1132
+ }),
1133
+ phased: defineI18n({
1134
+ name: 'phased',
1135
+ description: 'Pause, resume or complete the phased release of a released version.',
1136
+ resource: commandResource({
1137
+ en: 'Pause, resume or complete the phased release of a released version.',
1138
+ ko: '출시된 버전의 단계적 출시를 일시 중지, 재개 또는 완료합니다.',
1139
+ }),
1140
+ args: {
1141
+ game: { type: 'string', required: false, description: 'Game project directory.' },
1142
+ profile: { type: 'string', required: false, description: 'Deployment profile naming the app and credential; defaults to beta.' },
1143
+ 'marketing-version': { type: 'string', required: true, description: 'Marketing version of the phased release.' },
1144
+ action: { type: 'string', required: true, description: 'pause, resume or complete.' },
1145
+ approve: { type: 'boolean', required: false, description: 'Approve the phased release change.' },
1146
+ },
1147
+ run: async (ctx) => {
1148
+ const result = await changeAppStorePhasedReleaseCommand({
1149
+ game: readOptionalString(ctx.values.game) ?? '.',
1150
+ profile: readOptionalString(ctx.values.profile) ?? 'beta',
1151
+ version: readRequiredCliOption(ctx.values['marketing-version'], '--marketing-version'),
1152
+ action: readRequiredCliOption(ctx.values.action, '--action'),
1153
+ approved: ctx.values.approve === true,
1154
+ });
1155
+ console.info(JSON.stringify(result, null, 2));
1156
+ },
1157
+ }),
1158
+ },
1159
+ }),
909
1160
  },
910
1161
  run: () => {
911
- console.info('Use "mpgd store products plan|apply" or "mpgd store presence plan|apply".');
1162
+ console.info('Use "mpgd store products plan|apply", "mpgd store presence plan|apply" or "mpgd store release plan|apply|status".');
912
1163
  },
913
1164
  });
914
1165
  const gameCommand = defineI18n({
@@ -4522,6 +4773,42 @@ async function readPlayReleaseOptions(values, android) {
4522
4773
  }
4523
4774
  return createPlayReleaseRequest({ track, status, userFraction, releaseNotesFile, mappingFile });
4524
4775
  }
4776
+ /**
4777
+ * The release's recorded iOS build, the App Store Connect app it belongs to,
4778
+ * and the iOS credential, read once from the saved plan.
4779
+ */
4780
+ async function readTestFlightCliContext(values) {
4781
+ const game = readOptionalString(values.game);
4782
+ const plan = readNativeDeploymentPlan(readRequiredCliOption(values.plan, '--plan'), game === undefined ? undefined : path.resolve(game));
4783
+ const credential = readStoreCredential(plan, 'ios', process.env);
4784
+ if (credential.target !== 'ios') {
4785
+ throw new Error('TestFlight distribution needs the iOS submission credential.');
4786
+ }
4787
+ const recorded = await readRecordedIosBuild({
4788
+ plan,
4789
+ gameId: readRequiredCliOption(values['game-id'], '--game-id'),
4790
+ releaseKey: readRequiredCliOption(values.release, '--release'),
4791
+ });
4792
+ return {
4793
+ credential,
4794
+ target: {
4795
+ appStoreAppId: credential.appStoreAppId,
4796
+ bundleId: recorded.bundleId,
4797
+ marketingVersion: recorded.marketingVersion,
4798
+ buildNumber: recorded.buildNumber,
4799
+ ...(recorded.buildId === undefined ? {} : { expectedBuildId: recorded.buildId }),
4800
+ },
4801
+ };
4802
+ }
4803
+ async function withTestFlightCliPort(credential, secretValues, action) {
4804
+ const session = await openAscTestFlightDistributionSession(credential, { secretValues });
4805
+ try {
4806
+ return await action(session.port);
4807
+ }
4808
+ finally {
4809
+ session.close();
4810
+ }
4811
+ }
4525
4812
  function reportPlayDraftAppFallback(track) {
4526
4813
  console.warn(`Google Play accepts only draft releases until this app's first release is rolled out, `
4527
4814
  + `so the ${track} release is committed as a draft. Open that draft release in Play Console `
@@ -1,5 +1,5 @@
1
1
  {
2
- "kitGitSha": "1be07c42f7d67faaae8b908ee1a56d766b4b5120",
2
+ "kitGitSha": "3263fb99465db390c2e0027a3cf7b597cdee45b6",
3
3
  "kitDirty": false,
4
- "packageVersion": "0.67.0"
4
+ "packageVersion": "0.68.0"
5
5
  }
@@ -3335,7 +3335,7 @@ function inferProfileName(targetName2, target2) {
3335
3335
 
3336
3336
  // node_modules/.cache/mpgd-native-build/tools/icons/types.js
3337
3337
  var iconManifestSchemaVersion = 2;
3338
- var iconGeneratorVersion = "1.1.0";
3338
+ var iconGeneratorVersion = "1.1.1";
3339
3339
  var generatedIconCacheDirectory = ".mpgd/generated/icons";
3340
3340
 
3341
3341
  // node_modules/.cache/mpgd-native-build/tools/icons/generator.js
@@ -3609,12 +3609,13 @@ function createReadiness(input, outputs, externalUrl) {
3609
3609
  externalUrl
3610
3610
  };
3611
3611
  }
3612
- if (input.enforceExternalReadiness === true) {
3612
+ const consoleManagedBranding = input.target.metadata?.sdkMajor === 3;
3613
+ if (input.enforceExternalReadiness === true && !consoleManagedBranding) {
3613
3614
  throw new Error(`${input.targetName}.icon.externalUrl is required for a production Apps in Toss package. Upload ${requireOutput(outputs, "console-icon").path} in the Apps in Toss console first.`);
3614
3615
  }
3615
3616
  return {
3616
3617
  state: "console-upload-required",
3617
- message: `Upload ${requireOutput(outputs, "console-icon").path} in the Apps in Toss console and configure icon.externalUrl before production packaging.`
3618
+ message: consoleManagedBranding ? `SDK 3 packaging does not require an external icon URL. Upload ${requireOutput(outputs, "console-icon").path} in the Apps in Toss console before requesting review; console registration is not verified by this manifest.` : `Upload ${requireOutput(outputs, "console-icon").path} in the Apps in Toss console and configure icon.externalUrl before production packaging.`
3618
3619
  };
3619
3620
  }
3620
3621
  function requireOutput(outputs, purpose) {
@@ -8045,7 +8046,9 @@ var appStorePresenceLimits = Object.freeze({
8045
8046
  keywordBytes: 100,
8046
8047
  promotionalText: 170,
8047
8048
  whatsNew: 4e3,
8048
- screenshots: 10
8049
+ screenshots: 10,
8050
+ /** App Review notes. */
8051
+ reviewNotes: 4e3
8049
8052
  });
8050
8053
  var appStorePresenceScreenshotSizes = Object.freeze({
8051
8054
  iphone69: [
@@ -60,6 +60,17 @@ export declare function withRecordedR8Mapping(request: PlayReleaseRequest | unde
60
60
  export declare function submitRecordedNativeTarget(input: SubmitRecordedNativeTargetInput): Promise<NativeSubmissionCheckpoint>;
61
61
  /** Verify one recorded Android build and print its Play edit without any store call. */
62
62
  export declare function planRecordedAndroidSubmission(input: Omit<SubmitRecordedNativeTargetInput, 'credential' | 'approved'>, readStatus?: typeof readNativeReleaseStatus): Promise<PlaySubmissionPlan>;
63
+ /**
64
+ * The recorded iOS build of a release as App Store Connect knows it, for
65
+ * `mpgd deploy testflight`: bundle ID, marketing version, build number and,
66
+ * once the submission found it, the build ID. The IPA itself is not needed.
67
+ */
68
+ export declare function readRecordedIosBuild(input: Pick<SubmitRecordedNativeTargetInput, 'plan' | 'gameId' | 'releaseKey' | 'environment' | 'signal'>, readStatus?: typeof readNativeReleaseStatus): Promise<{
69
+ readonly bundleId: string;
70
+ readonly marketingVersion: string;
71
+ readonly buildNumber: string;
72
+ readonly buildId?: string;
73
+ }>;
63
74
  export declare function submitRecordedNativeTargetWithPorts(input: SubmitRecordedNativeTargetInput, ports: NativeSubmissionPorts): Promise<NativeSubmissionCheckpoint>;
64
75
  /** Validate the workflow-restored manifest against the immutable release-state hash. */
65
76
  export declare function verifyRecordedReleaseManifest(gameRoot: string, record: Pick<ImmutableNativeBuildRecord, 'artifactLocation' | 'releaseManifestSha256' | 'target'>): void;
@@ -82,6 +82,31 @@ export async function planRecordedAndroidSubmission(input, readStatus = readNati
82
82
  });
83
83
  }
84
84
  async function readRecordedSubmission(input, target, readStatus) {
85
+ const recorded = await readRecordedBuild(input, target, readStatus);
86
+ const artifactFile = resolveRecordedArtifact(input.plan.gameRoot, recorded.record.artifactLocation);
87
+ return { ...recorded, artifactFile };
88
+ }
89
+ /**
90
+ * The recorded iOS build of a release as App Store Connect knows it, for
91
+ * `mpgd deploy testflight`: bundle ID, marketing version, build number and,
92
+ * once the submission found it, the build ID. The IPA itself is not needed.
93
+ */
94
+ export async function readRecordedIosBuild(input, readStatus = readNativeReleaseStatus) {
95
+ const { planned, record, status } = await readRecordedBuild(input, 'ios', readStatus);
96
+ const { marketingVersion, buildNumber } = record.platformVersion;
97
+ if (typeof marketingVersion !== 'string' || !/^\d+(?:\.\d+){0,2}$/u.test(marketingVersion)
98
+ || !/^[1-9]\d*$/u.test(String(buildNumber))) {
99
+ throw new Error('The recorded iOS build has no valid marketing version and build number.');
100
+ }
101
+ const buildId = status.submissions.ios?.remoteBuildId;
102
+ return {
103
+ bundleId: planned.appId,
104
+ marketingVersion,
105
+ buildNumber: String(buildNumber),
106
+ ...(buildId === undefined ? {} : { buildId }),
107
+ };
108
+ }
109
+ async function readRecordedBuild(input, target, readStatus) {
85
110
  const planned = input.plan.targets.find((entry) => entry.target === target);
86
111
  if (planned === undefined) {
87
112
  throw new Error('Native store submission requires a planned target and explicit approval.');
@@ -107,8 +132,7 @@ async function readRecordedSubmission(input, target, readStatus) {
107
132
  || record.internalTestGroupId !== planned.testGroup) {
108
133
  throw new Error('Store submission requires a matching immutable native build record.');
109
134
  }
110
- const artifactFile = resolveRecordedArtifact(input.plan.gameRoot, record.artifactLocation);
111
- return { planned, record, artifactFile, status };
135
+ return { planned, record, status };
112
136
  }
113
137
  export async function submitRecordedNativeTargetWithPorts(input, ports) {
114
138
  const target = input.credential.target;
@@ -63,6 +63,13 @@ export declare function parsePlayUserFraction(value: string | number): number;
63
63
  export declare function assertPlayUserFraction(fraction: number): void;
64
64
  export declare function readPlayReleaseNotesFile(file: string): PlayReleaseNote[];
65
65
  /**
66
+ * A per-language notes file in the release-notes format below, without a
67
+ * store's length limit; `label` names the notes in errors. TestFlight's
68
+ * "What to Test" (`mpgd deploy testflight distribute --what-to-test`) reads
69
+ * the same file format as Google Play release notes.
70
+ */
71
+ export declare function readLocalizedNotesFile(file: string, label?: string): PlayReleaseNote[];
72
+ /**
66
73
  * Accepts the Play Console's own language-tag format, one block per language:
67
74
  *
68
75
  * <en-US>
@@ -72,7 +79,11 @@ export declare function readPlayReleaseNotesFile(file: string): PlayReleaseNote[
72
79
  * or JSON: `{ "en-US": "Bug fixes." }` or `[{ "language": "en-US", "text": "Bug fixes." }]`.
73
80
  */
74
81
  export declare function parsePlayReleaseNotes(source: string): PlayReleaseNote[];
82
+ /** `parsePlayReleaseNotes` without Google Play's length limit. */
83
+ export declare function parseLocalizedNotes(source: string, label?: string): PlayReleaseNote[];
75
84
  export declare function assertPlayReleaseNotes(notes: readonly PlayReleaseNote[]): void;
85
+ /** One non-empty text per BCP-47 language, each language once. */
86
+ export declare function assertLocalizedNotes(notes: readonly PlayReleaseNote[], label: string): void;
76
87
  export declare function hashMappingFile(mappingFile: string): Promise<string>;
77
88
  /** Build the Release body for a new bundle; the default matches the original body exactly. */
78
89
  export declare function newPlayRelease(request: PlayReleaseRequest, name: string, versionCode: number): PlayTrackRelease;