hotcodepush 0.0.1 → 0.2.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.
Files changed (165) hide show
  1. package/README.md +3 -3
  2. package/dist/commands/app/create.js +49 -0
  3. package/dist/commands/app/delete.js +34 -0
  4. package/dist/commands/app/get.js +33 -0
  5. package/dist/commands/app/list.js +38 -0
  6. package/dist/commands/app/transfer.js +58 -0
  7. package/dist/commands/app/update.js +34 -0
  8. package/dist/commands/audience/get.js +69 -0
  9. package/dist/commands/audit-log/list.js +82 -0
  10. package/dist/commands/binary/create.js +191 -0
  11. package/dist/commands/binary/get.js +56 -0
  12. package/dist/commands/binary/list.js +58 -0
  13. package/dist/commands/bundle/delete.js +35 -0
  14. package/dist/commands/bundle/get.js +63 -0
  15. package/dist/commands/bundle/list.js +66 -0
  16. package/dist/commands/bundle/upload.js +177 -0
  17. package/dist/commands/channel/create.js +59 -0
  18. package/dist/commands/channel/delete.js +68 -0
  19. package/dist/commands/channel/get.js +45 -0
  20. package/dist/commands/channel/list.js +39 -0
  21. package/dist/commands/channel/pause.js +32 -0
  22. package/dist/commands/channel/resume.js +32 -0
  23. package/dist/commands/channel/update.js +40 -0
  24. package/dist/commands/checkout/create.js +25 -0
  25. package/dist/commands/completion.js +37 -0
  26. package/dist/commands/customer-portal-session/create.js +25 -0
  27. package/dist/commands/device/delete.js +29 -0
  28. package/dist/commands/device/get.js +60 -0
  29. package/dist/commands/device/list.js +97 -0
  30. package/dist/commands/device/probe.js +253 -0
  31. package/dist/commands/doctor.js +364 -0
  32. package/dist/commands/fingerprint/diff.js +134 -0
  33. package/dist/commands/fingerprint.js +42 -0
  34. package/dist/commands/init.js +493 -0
  35. package/dist/commands/invitation/accept.js +53 -0
  36. package/dist/commands/invitation/create.js +52 -0
  37. package/dist/commands/invitation/delete.js +60 -0
  38. package/dist/commands/invitation/list.js +40 -0
  39. package/dist/commands/limit/list.js +34 -0
  40. package/dist/commands/login.js +36 -0
  41. package/dist/commands/logout.js +81 -0
  42. package/dist/commands/member/delete.js +35 -0
  43. package/dist/commands/member/get.js +37 -0
  44. package/dist/commands/member/list.js +41 -0
  45. package/dist/commands/member/update.js +53 -0
  46. package/dist/commands/open.js +37 -0
  47. package/dist/commands/organization/create.js +30 -0
  48. package/dist/commands/organization/delete.js +39 -0
  49. package/dist/commands/organization/get.js +38 -0
  50. package/dist/commands/organization/list.js +41 -0
  51. package/dist/commands/organization/update.js +64 -0
  52. package/dist/commands/release/create.js +237 -0
  53. package/dist/commands/release/get.js +81 -0
  54. package/dist/commands/release/list.js +69 -0
  55. package/dist/commands/release/pause.js +36 -0
  56. package/dist/commands/release/resume.js +36 -0
  57. package/dist/commands/release/revoke.js +254 -0
  58. package/dist/commands/release/rollback.js +73 -0
  59. package/dist/commands/release/rollout.js +63 -0
  60. package/dist/commands/release/update.js +82 -0
  61. package/dist/commands/resource-file/write.js +78 -0
  62. package/dist/commands/signing-key/add.js +45 -0
  63. package/dist/commands/signing-key/create.js +84 -0
  64. package/dist/commands/signing-key/delete.js +67 -0
  65. package/dist/commands/signing-key/list.js +38 -0
  66. package/dist/commands/sso-provider/delete.js +32 -0
  67. package/dist/commands/sso-provider/get.js +63 -0
  68. package/dist/commands/sso-provider/set.js +161 -0
  69. package/dist/commands/sso-provider/verify.js +31 -0
  70. package/dist/commands/statistics/get.js +189 -0
  71. package/dist/commands/usage/get.js +31 -0
  72. package/dist/commands/usage/list.js +41 -0
  73. package/dist/commands/webhook/create.js +72 -0
  74. package/dist/commands/webhook/delete.js +32 -0
  75. package/dist/commands/webhook/get.js +34 -0
  76. package/dist/commands/webhook/list.js +40 -0
  77. package/dist/commands/webhook/test.js +32 -0
  78. package/dist/commands/webhook/update.js +56 -0
  79. package/dist/commands/webhook-delivery/list.js +63 -0
  80. package/dist/commands/webhook-delivery/redeliver.js +48 -0
  81. package/dist/commands/whoami.js +24 -0
  82. package/dist/completion/hotcodepush.bash +536 -0
  83. package/dist/completion/hotcodepush.fish +979 -0
  84. package/dist/completion/hotcodepush.zsh +1372 -0
  85. package/dist/config/consts.js +32 -0
  86. package/dist/index.js +5 -0
  87. package/dist/registry.js +83 -0
  88. package/dist/utils/api-client.js +17 -0
  89. package/dist/utils/audience.js +9 -0
  90. package/dist/utils/auth-client.js +35 -0
  91. package/dist/utils/authentication.js +60 -0
  92. package/dist/utils/boolean-flag.js +3 -0
  93. package/dist/utils/browser.js +6 -0
  94. package/dist/utils/build-step.js +104 -0
  95. package/dist/utils/bundle-files.js +80 -0
  96. package/dist/utils/bundle-resolution.js +46 -0
  97. package/dist/utils/channel-fields.js +130 -0
  98. package/dist/utils/cli.js +80 -0
  99. package/dist/utils/command-registry.js +79 -0
  100. package/dist/utils/command.js +4 -0
  101. package/dist/utils/completion/bash.js +48 -0
  102. package/dist/utils/completion/command-tree.js +72 -0
  103. package/dist/utils/completion/fish.js +44 -0
  104. package/dist/utils/completion/posix-case-arms.js +9 -0
  105. package/dist/utils/completion/posix-quote.js +4 -0
  106. package/dist/utils/completion/script-files.js +26 -0
  107. package/dist/utils/completion/zsh.js +72 -0
  108. package/dist/utils/compressed-files.js +31 -0
  109. package/dist/utils/credential.js +15 -0
  110. package/dist/utils/device-resolution.js +17 -0
  111. package/dist/utils/diagnostics.js +20 -0
  112. package/dist/utils/duration.js +16 -0
  113. package/dist/utils/environment.js +22 -0
  114. package/dist/utils/error-mapping.js +75 -0
  115. package/dist/utils/errors.js +266 -0
  116. package/dist/utils/fingerprint.js +76 -0
  117. package/dist/utils/flag-name.js +4 -0
  118. package/dist/utils/fleet.js +9 -0
  119. package/dist/utils/framework.js +74 -0
  120. package/dist/utils/frameworks/binary-create-step.js +70 -0
  121. package/dist/utils/frameworks/capacitor.js +150 -0
  122. package/dist/utils/frameworks/cordova.js +110 -0
  123. package/dist/utils/frameworks/expo.js +152 -0
  124. package/dist/utils/frameworks/index.js +13 -0
  125. package/dist/utils/frameworks/native-glue.js +10 -0
  126. package/dist/utils/frameworks/native-projects.js +27 -0
  127. package/dist/utils/frameworks/react-native-build.js +182 -0
  128. package/dist/utils/frameworks/react-native.js +127 -0
  129. package/dist/utils/frameworks/sdk-package.js +63 -0
  130. package/dist/utils/git-provenance.js +71 -0
  131. package/dist/utils/global-options.js +34 -0
  132. package/dist/utils/hosts.js +53 -0
  133. package/dist/utils/init-steps.js +51 -0
  134. package/dist/utils/json-file.js +28 -0
  135. package/dist/utils/login.js +153 -0
  136. package/dist/utils/member-resolution.js +49 -0
  137. package/dist/utils/native-project-edit.js +41 -0
  138. package/dist/utils/nullable-field.js +10 -0
  139. package/dist/utils/outcome.js +24 -0
  140. package/dist/utils/output.js +38 -0
  141. package/dist/utils/pack.js +20 -0
  142. package/dist/utils/package-json.js +5 -0
  143. package/dist/utils/package-manager.js +50 -0
  144. package/dist/utils/pagination.js +29 -0
  145. package/dist/utils/progress.js +23 -0
  146. package/dist/utils/progression.js +119 -0
  147. package/dist/utils/project-config.js +61 -0
  148. package/dist/utils/prompts.js +53 -0
  149. package/dist/utils/react-native-project.js +119 -0
  150. package/dist/utils/release-conditions.js +79 -0
  151. package/dist/utils/release-output.js +13 -0
  152. package/dist/utils/release-resolution.js +83 -0
  153. package/dist/utils/resource-file.js +81 -0
  154. package/dist/utils/resource-resolution.js +107 -0
  155. package/dist/utils/signing-private-key.js +105 -0
  156. package/dist/utils/spending-cap.js +16 -0
  157. package/dist/utils/telemetry.js +172 -0
  158. package/dist/utils/token-store.js +63 -0
  159. package/dist/utils/update-notice.js +68 -0
  160. package/dist/utils/upload.js +148 -0
  161. package/dist/utils/usage-month.js +14 -0
  162. package/dist/utils/user-config.js +54 -0
  163. package/dist/utils/webhook-resolution.js +47 -0
  164. package/dist/utils/xcode-project.js +181 -0
  165. package/package.json +11 -9
@@ -0,0 +1,254 @@
1
+ import { z } from 'zod';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { fetchBundle } from '../../utils/bundle-resolution.js';
4
+ import { defineCommand } from '../../utils/command.js';
5
+ import { printWarning } from '../../utils/diagnostics.js';
6
+ import { InvalidParameterError } from '../../utils/errors.js';
7
+ import { resolveReleaseDeviceCount } from '../../utils/fleet.js';
8
+ import { defineCommandOptions } from '../../utils/global-options.js';
9
+ import { printJson, resolveQuantityText } from '../../utils/output.js';
10
+ import { fetchAllPages } from '../../utils/pagination.js';
11
+ import { readProjectConfig } from '../../utils/project-config.js';
12
+ import { confirmConsequence } from '../../utils/prompts.js';
13
+ import { fetchReleaseInChannel, fetchReleaseLog, releaseOptionShape, } from '../../utils/release-resolution.js';
14
+ import { channelOptionShape, fetchAppId, fetchChannel, } from '../../utils/resource-resolution.js';
15
+ // The limit of the bulk route.
16
+ const MAX_RELEASE_IDS_PER_REVOCATION = 100;
17
+ export default defineCommand({
18
+ description: 'Revoke releases for good: devices on them move to the newest older release they qualify for or the embedded bundle.',
19
+ examples: [
20
+ 'hotcodepush release revoke --release 43',
21
+ 'hotcodepush release revoke --channel staging --release-from 40 --yes --json',
22
+ ],
23
+ options: defineCommandOptions({
24
+ ...channelOptionShape,
25
+ ...releaseOptionShape,
26
+ all: z
27
+ .boolean()
28
+ .optional()
29
+ .describe('Every release of the channel. Its devices return to the embedded bundle.'),
30
+ bundle: z
31
+ .string()
32
+ .optional()
33
+ .describe("Every release of this bundle, by number or id, across the app's channels. Takes no --channel."),
34
+ releaseFrom: z.coerce
35
+ .number()
36
+ .int()
37
+ .min(1)
38
+ .optional()
39
+ .describe('The first release revoked, by number, inclusive: that one and every newer one of the channel.'),
40
+ }),
41
+ action: async (options) => {
42
+ const hotCodePush = createApiClient();
43
+ const revocations = await fetchRevocations(hotCodePush, options);
44
+ const [firstRevocation] = revocations;
45
+ if (firstRevocation === undefined ||
46
+ revocations.every(({ revokedReleases }) => revokedReleases.length === 0)) {
47
+ printNothingToRevoke(options.json);
48
+ return;
49
+ }
50
+ const fleet = await hotCodePush.apps.statistics.fleet.get({
51
+ appId: firstRevocation.channel.appId,
52
+ });
53
+ for (const pausedText of resolvePausedLandingTexts(revocations, fleet)) {
54
+ printWarning(pausedText);
55
+ }
56
+ const isConfirmed = await confirmConsequence(resolveRevocationConsequence(revocations, fleet), options);
57
+ if (!isConfirmed) {
58
+ return;
59
+ }
60
+ const answers = [];
61
+ for (const { channel, request } of revocations) {
62
+ answers.push({
63
+ channel,
64
+ releases: await fetchRevocation(hotCodePush, channel, request),
65
+ });
66
+ }
67
+ if (options.json) {
68
+ printJson(answers.flatMap(({ releases }) => releases));
69
+ return;
70
+ }
71
+ for (const { channel, releases } of answers) {
72
+ console.log(`Revoked ${resolveReleasesText(releases)} of ${channel.name}${releases.length === 1 ? ` (${releases[0]?.id})` : ''}.`);
73
+ }
74
+ },
75
+ });
76
+ async function fetchRevocation(hotCodePush, channel, request) {
77
+ if ('releaseId' in request) {
78
+ return [
79
+ await hotCodePush.apps.releases.revoke({
80
+ appId: channel.appId,
81
+ releaseId: request.releaseId,
82
+ }),
83
+ ];
84
+ }
85
+ const revokedReleases = [];
86
+ for (const releaseIds of resolveReleaseIdPages(request.releaseIds)) {
87
+ revokedReleases.push(...(await hotCodePush.apps.channels.releases.revoke({
88
+ appId: channel.appId,
89
+ channelId: channel.id,
90
+ releaseIds,
91
+ })));
92
+ }
93
+ return revokedReleases.sort((left, right) => right.number - left.number);
94
+ }
95
+ async function fetchRevocations(hotCodePush, options) {
96
+ const formFlags = [
97
+ options.all ? '--all' : undefined,
98
+ options.bundle === undefined ? undefined : '--bundle',
99
+ options.release === undefined ? undefined : '--release',
100
+ options.releaseFrom === undefined ? undefined : '--release-from',
101
+ ].filter(flag => flag !== undefined);
102
+ if (formFlags.length > 1) {
103
+ throw new InvalidParameterError(`The flags ${formFlags.join(' and ')} were passed together.`, 'Pass one of them.');
104
+ }
105
+ if (options.bundle !== undefined) {
106
+ if (options.channel !== undefined) {
107
+ throw new InvalidParameterError("With --bundle, the bundle's releases are revoked in every channel, so --channel does not apply.", 'Pass --bundle alone.');
108
+ }
109
+ return fetchBundleRevocations(hotCodePush, options);
110
+ }
111
+ if (options.all || options.releaseFrom !== undefined) {
112
+ const fromNumber = options.releaseFrom ?? 1;
113
+ const channel = await fetchChannel(hotCodePush, options);
114
+ const releaseLog = await fetchReleaseLog(hotCodePush, channel);
115
+ const revokedReleases = releaseLog.filter(({ number, status }) => number >= fromNumber && status !== 'revoked');
116
+ return [
117
+ {
118
+ channel,
119
+ releaseLog,
120
+ request: { releaseIds: revokedReleases.map(({ id }) => id) },
121
+ revokedReleases,
122
+ },
123
+ ];
124
+ }
125
+ const { channel, release } = await fetchReleaseInChannel(hotCodePush, options);
126
+ return [
127
+ {
128
+ channel,
129
+ releaseLog: await fetchReleaseLog(hotCodePush, channel),
130
+ request: { releaseId: release.id },
131
+ revokedReleases: [release],
132
+ },
133
+ ];
134
+ }
135
+ async function fetchBundleRevocations(hotCodePush, options) {
136
+ const appId = await fetchAppId(hotCodePush, options, readProjectConfig(options.config));
137
+ const bundle = await fetchBundle(hotCodePush, appId, options);
138
+ const pendingReleases = (await fetchAllPages(page => hotCodePush.apps.releases.list({
139
+ appId,
140
+ bundleId: bundle.id,
141
+ relations: ['bundle', 'channel'],
142
+ ...page,
143
+ }))).filter(({ status }) => status !== 'revoked');
144
+ const channels = new Map();
145
+ for (const { channel } of pendingReleases) {
146
+ if (channel !== undefined) {
147
+ channels.set(channel.id, channel);
148
+ }
149
+ }
150
+ return Promise.all([...channels.values()].map(async (channel) => {
151
+ const revokedReleases = pendingReleases.filter(({ channelId }) => channelId === channel.id);
152
+ return {
153
+ channel,
154
+ releaseLog: await fetchReleaseLog(hotCodePush, channel),
155
+ request: { releaseIds: revokedReleases.map(({ id }) => id) },
156
+ revokedReleases,
157
+ };
158
+ }));
159
+ }
160
+ function printNothingToRevoke(isJson) {
161
+ if (isJson) {
162
+ printJson([]);
163
+ return;
164
+ }
165
+ console.log('Nothing to revoke: no release named is active or paused.');
166
+ }
167
+ // Mirrors the API: devices move to the newest active older release not revoked with it, or to the embedded bundle.
168
+ function resolveDeviceMoves(revocation, fleet) {
169
+ const moves = new Map();
170
+ for (const revokedRelease of revocation.revokedReleases) {
171
+ const deviceCount = resolveReleaseDeviceCount(fleet, revokedRelease.id);
172
+ if (deviceCount === 0) {
173
+ continue;
174
+ }
175
+ const targetRelease = resolveOlderReleases(revocation, revokedRelease).find(({ status }) => status === 'active');
176
+ const key = targetRelease?.id ?? 'embedded';
177
+ const move = moves.get(key) ?? { deviceCount: 0, targetRelease };
178
+ move.deviceCount += deviceCount;
179
+ moves.set(key, move);
180
+ }
181
+ return [...moves.values()];
182
+ }
183
+ function resolveMovesText(revocation, fleet, isChannelNamed) {
184
+ const moves = resolveDeviceMoves(revocation, fleet);
185
+ if (moves.length === 0) {
186
+ return undefined;
187
+ }
188
+ const channelText = isChannelNamed ? ` in ${revocation.channel.name}` : '';
189
+ return moves
190
+ .map(({ deviceCount, targetRelease }) => {
191
+ const devicesText = resolveQuantityText(deviceCount, 'device');
192
+ return targetRelease === undefined
193
+ ? `${devicesText} return to the embedded bundle${channelText}`
194
+ : `${devicesText} move to release #${targetRelease.number}${channelText}`;
195
+ })
196
+ .join(' and ');
197
+ }
198
+ function resolveOlderReleases(revocation, revokedRelease) {
199
+ const revokedReleaseIds = new Set(revocation.revokedReleases.map(({ id }) => id));
200
+ return revocation.releaseLog
201
+ .filter(({ id, number, status }) => number < revokedRelease.number &&
202
+ status !== 'revoked' &&
203
+ !revokedReleaseIds.has(id))
204
+ .sort((left, right) => right.number - left.number);
205
+ }
206
+ // A paused release takes no device, so its devices land one release further back.
207
+ function resolvePausedLandingTexts(revocations, fleet) {
208
+ const texts = new Set();
209
+ for (const revocation of revocations) {
210
+ for (const revokedRelease of revocation.revokedReleases) {
211
+ const [landingRelease] = resolveOlderReleases(revocation, revokedRelease);
212
+ if (resolveReleaseDeviceCount(fleet, revokedRelease.id) > 0 &&
213
+ landingRelease?.status === 'paused') {
214
+ texts.add(`release #${landingRelease.number} of ${revocation.channel.name} is paused, so the devices that would land on it land one further back`);
215
+ }
216
+ }
217
+ }
218
+ return [...texts];
219
+ }
220
+ function resolveRevocationConsequence(revocations, fleet) {
221
+ const isChannelNamed = revocations.length > 1;
222
+ const releasesText = revocations
223
+ .map(({ channel, revokedReleases }) => `${resolveReleasesText(revokedReleases)} of ${channel.name}`)
224
+ .join(' and ');
225
+ const movesText = revocations
226
+ .map(revocation => resolveMovesText(revocation, fleet, isChannelNamed))
227
+ .filter(text => text !== undefined)
228
+ .join(', ') || 'no device runs them';
229
+ const bundleNumbers = new Set(revocations.flatMap(({ revokedReleases }) => revokedReleases.map(({ bundle }) => bundle?.number)));
230
+ const [bundleNumber] = bundleNumbers;
231
+ const reReleaseText = bundleNumbers.size === 1 && bundleNumber !== undefined
232
+ ? `"release create --bundle ${bundleNumber}" releases its bundle again`
233
+ : '"release create --bundle <number>" releases a bundle again';
234
+ return `revokes ${releasesText} for good: ${movesText}. Revoked is final, and ${reReleaseText}`;
235
+ }
236
+ // The oldest page goes first, so devices never land on a newer release that a later page revokes.
237
+ function resolveReleaseIdPages(newestFirstReleaseIds) {
238
+ const oldestFirstReleaseIds = newestFirstReleaseIds.toReversed();
239
+ const pages = [];
240
+ for (let start = 0; start < oldestFirstReleaseIds.length; start += MAX_RELEASE_IDS_PER_REVOCATION) {
241
+ pages.push(oldestFirstReleaseIds.slice(start, start + MAX_RELEASE_IDS_PER_REVOCATION));
242
+ }
243
+ return pages;
244
+ }
245
+ function resolveReleasesText(releases) {
246
+ const numberTexts = releases.map(({ number }) => `#${number}`);
247
+ if (numberTexts.length === 1) {
248
+ return `release ${numberTexts[0]}`;
249
+ }
250
+ if (numberTexts.length > 3) {
251
+ return resolveQuantityText(numberTexts.length, 'release');
252
+ }
253
+ return `releases ${numberTexts.slice(0, -1).join(', ')} and ${numberTexts.at(-1)}`;
254
+ }
@@ -0,0 +1,73 @@
1
+ import { z } from 'zod';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { booleanFlagSchema } from '../../utils/boolean-flag.js';
4
+ import { defineCommand } from '../../utils/command.js';
5
+ import { InvalidParameterError } from '../../utils/errors.js';
6
+ import { defineCommandOptions } from '../../utils/global-options.js';
7
+ import { printJson } from '../../utils/output.js';
8
+ import { confirmConsequence } from '../../utils/prompts.js';
9
+ import { printReleasedLine } from '../../utils/release-output.js';
10
+ import { fetchReleaseLog, resolveReleaseBundleLabel, resolveReleaseInLog, waitUntilLive, } from '../../utils/release-resolution.js';
11
+ import { channelOptionShape, fetchChannel, } from '../../utils/resource-resolution.js';
12
+ export default defineCommand({
13
+ description: "Release the channel's previous bundle again, or the one of --to-release, as a new release.",
14
+ examples: [
15
+ 'hotcodepush release rollback',
16
+ 'hotcodepush release rollback --channel staging --to-release 41 --yes',
17
+ ],
18
+ options: defineCommandOptions({
19
+ ...channelOptionShape,
20
+ mandatory: booleanFlagSchema
21
+ .optional()
22
+ .describe('Devices apply the rollback at once and restart, the default for an incident fix. Pass false to let them wait.'),
23
+ toRelease: z
24
+ .string()
25
+ .optional()
26
+ .describe("The release whose bundle comes back, by number or id. Defaults to the channel's previous one."),
27
+ }),
28
+ action: async (options) => {
29
+ const hotCodePush = createApiClient();
30
+ const fetchedChannel = await fetchChannel(hotCodePush, options);
31
+ const releases = await fetchReleaseLog(hotCodePush, fetchedChannel);
32
+ const targetRelease = options.toRelease === undefined
33
+ ? resolvePreviousRelease(releases)
34
+ : resolveReleaseInLog(releases, options.toRelease, '--to-release');
35
+ const bundleLabel = resolveReleaseBundleLabel(targetRelease);
36
+ const isMandatory = options.mandatory ?? true;
37
+ const { deviceCount } = await hotCodePush.apps.statistics.fleet.get({
38
+ appId: fetchedChannel.appId,
39
+ channelId: fetchedChannel.id,
40
+ });
41
+ const isConfirmed = await confirmConsequence(`rolls channel ${fetchedChannel.name} back to bundle ${bundleLabel} of release #${targetRelease.number} as a new ${isMandatory ? 'mandatory ' : ''}release, reaching its ${deviceCount} active devices`, options);
42
+ if (!isConfirmed) {
43
+ return;
44
+ }
45
+ const createdRelease = await hotCodePush.apps.channels.rollbacks.create({
46
+ appId: fetchedChannel.appId,
47
+ channelId: fetchedChannel.id,
48
+ isMandatory,
49
+ toReleaseId: targetRelease.id,
50
+ });
51
+ const liveRelease = await waitUntilLive(hotCodePush, fetchedChannel.appId, createdRelease.id);
52
+ if (options.json) {
53
+ printJson(liveRelease);
54
+ }
55
+ else {
56
+ printReleasedLine(liveRelease, fetchedChannel.name, bundleLabel);
57
+ }
58
+ },
59
+ });
60
+ // Mirrors the API rule for a rollback without a target.
61
+ function resolvePreviousRelease(releases) {
62
+ const activeReleases = releases
63
+ .filter(({ status }) => status === 'active')
64
+ .sort((left, right) => right.number - left.number);
65
+ const currentRelease = activeReleases[0];
66
+ const previousRelease = activeReleases.find(release => currentRelease !== undefined &&
67
+ release.number < currentRelease.number &&
68
+ release.bundleId !== currentRelease.bundleId);
69
+ if (previousRelease === undefined) {
70
+ throw new InvalidParameterError('The channel has no earlier release to roll back to.', 'Name one with --to-release.');
71
+ }
72
+ return previousRelease;
73
+ }
@@ -0,0 +1,63 @@
1
+ import { z } from 'zod';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { defineCommand } from '../../utils/command.js';
4
+ import { defineCommandOptions } from '../../utils/global-options.js';
5
+ import { printJson } from '../../utils/output.js';
6
+ import { progressionShape, resolveProgression, resolveProgressionText, } from '../../utils/progression.js';
7
+ import { confirmConsequence, promptText } from '../../utils/prompts.js';
8
+ import { resolveProgressingText } from '../../utils/release-output.js';
9
+ import { fetchReleaseInChannel, releaseOptionShape, } from '../../utils/release-resolution.js';
10
+ import { channelOptionShape } from '../../utils/resource-resolution.js';
11
+ const rolloutPercentSchema = z.coerce.number().int().min(0).max(100);
12
+ export default defineCommand({
13
+ description: "Set a release's rollout percentage in either direction, or widen it step by step with --progression. A device that has the release keeps it.",
14
+ examples: [
15
+ 'hotcodepush release rollout --release 43 --rollout-percent 50',
16
+ 'hotcodepush release rollout --channel staging --release 43 --rollout-percent 10 --progression --yes',
17
+ ],
18
+ options: defineCommandOptions({
19
+ ...channelOptionShape,
20
+ ...progressionShape,
21
+ ...releaseOptionShape,
22
+ rolloutPercent: rolloutPercentSchema
23
+ .optional()
24
+ .describe("The share of devices the release reaches from now on, 0 to 100. With --progression it is where the schedule starts. Defaults to the release's own percentage."),
25
+ }),
26
+ action: async (options) => {
27
+ const hotCodePush = createApiClient();
28
+ const progression = resolveProgression(options);
29
+ const rolloutPercent = options.rolloutPercent ??
30
+ (progression === null ? await promptRolloutPercent(options) : undefined);
31
+ const { channel, release } = await fetchReleaseInChannel(hotCodePush, options);
32
+ const isConfirmed = await confirmConsequence(resolveRolloutConsequence(release, channel.name, rolloutPercent ?? release.rolloutPercent, progression), options);
33
+ if (!isConfirmed) {
34
+ return;
35
+ }
36
+ const updatedRelease = await hotCodePush.apps.releases.update({
37
+ appId: channel.appId,
38
+ progression: progression ?? undefined,
39
+ releaseId: release.id,
40
+ rolloutPercent,
41
+ });
42
+ if (options.json) {
43
+ printJson(updatedRelease);
44
+ }
45
+ else {
46
+ console.log(`Set release #${updatedRelease.number} of ${channel.name} to ${updatedRelease.rolloutPercent} percent${resolveProgressingText(updatedRelease)}.`);
47
+ }
48
+ },
49
+ });
50
+ async function promptRolloutPercent(options) {
51
+ const answer = await promptText('--rollout-percent', 'Which share of devices should the release reach, 0 to 100?', options);
52
+ return z
53
+ .object({ rolloutPercent: rolloutPercentSchema })
54
+ .parse({ rolloutPercent: answer }).rolloutPercent;
55
+ }
56
+ function resolveRolloutConsequence(release, channelName, rolloutPercent, progression) {
57
+ const releaseText = `release #${release.number} of ${channelName}`;
58
+ if (progression !== null) {
59
+ return `progresses ${releaseText} from ${rolloutPercent} percent through ${resolveProgressionText(progression)}: devices already on it keep it`;
60
+ }
61
+ const progressionEndText = release.progression === null ? '' : ', and its progression ends';
62
+ return `sets ${releaseText} to ${rolloutPercent} percent: devices already on it keep it, and no new device above ${rolloutPercent} percent gets it${progressionEndText}`;
63
+ }
@@ -0,0 +1,82 @@
1
+ import { z } from 'zod';
2
+ import { createApiClient } from '../../utils/api-client.js';
3
+ import { booleanFlagSchema } from '../../utils/boolean-flag.js';
4
+ import { failurePolicyShape, releaseFailurePolicyClearShape, resolveFailurePolicy, } from '../../utils/channel-fields.js';
5
+ import { defineCommand } from '../../utils/command.js';
6
+ import { MissingParameterError } from '../../utils/errors.js';
7
+ import { defineCommandOptions } from '../../utils/global-options.js';
8
+ import { resolveNullableField } from '../../utils/nullable-field.js';
9
+ import { printJson } from '../../utils/output.js';
10
+ import { confirmConsequence } from '../../utils/prompts.js';
11
+ import { fetchReleaseInChannel, releaseOptionShape, } from '../../utils/release-resolution.js';
12
+ import { channelOptionShape } from '../../utils/resource-resolution.js';
13
+ const CLEARED_TEXT = 'cleared';
14
+ export default defineCommand({
15
+ description: 'Edit a published release in place: flip the mandatory flag, rewrite or clear the notes, change or clear an auto-pause override.',
16
+ examples: [
17
+ 'hotcodepush release update --release 43 --mandatory',
18
+ 'hotcodepush release update --channel staging --release 43 --mandatory false --notes "cart fix" --yes',
19
+ ],
20
+ options: defineCommandOptions({
21
+ ...channelOptionShape,
22
+ ...failurePolicyShape,
23
+ ...releaseFailurePolicyClearShape,
24
+ ...releaseOptionShape,
25
+ clearNotes: z.boolean().optional().describe('Sets the notes back to null.'),
26
+ mandatory: booleanFlagSchema
27
+ .optional()
28
+ .describe('Whether devices apply the release at once and restart. Pass false to turn it off.'),
29
+ notes: z.string().optional().describe('The new release notes.'),
30
+ }),
31
+ action: async (options) => {
32
+ const hotCodePush = createApiClient();
33
+ const changes = resolveReleaseChanges(options);
34
+ const changeTexts = resolveChangeTexts(changes);
35
+ if (changeTexts.length === 0) {
36
+ throw new MissingParameterError('--mandatory, --notes, a --failure-* flag or a --clear-* flag', 'Pass at least one of them.');
37
+ }
38
+ const { channel, release } = await fetchReleaseInChannel(hotCodePush, options);
39
+ const isConfirmed = await confirmConsequence(`changes release #${release.number} of ${channel.name}, ${changeTexts.join(' and ')}: devices see the change on their next check`, options);
40
+ if (!isConfirmed) {
41
+ return;
42
+ }
43
+ const updatedRelease = await hotCodePush.apps.releases.update({
44
+ ...changes,
45
+ appId: channel.appId,
46
+ releaseId: release.id,
47
+ });
48
+ if (options.json) {
49
+ printJson(updatedRelease);
50
+ }
51
+ else {
52
+ console.log(`Updated release #${updatedRelease.number} of ${channel.name} (${updatedRelease.id}).`);
53
+ }
54
+ },
55
+ });
56
+ // Uses the field order of `release get`.
57
+ function resolveChangeTexts({ failureAction, failureMinimumUpdateCount, failureThresholdPercent, isMandatory, notes, }) {
58
+ const changeTexts = [];
59
+ if (failureThresholdPercent !== undefined) {
60
+ changeTexts.push(`failure threshold ${failureThresholdPercent === null ? CLEARED_TEXT : `${failureThresholdPercent}%`}`);
61
+ }
62
+ if (failureMinimumUpdateCount !== undefined) {
63
+ changeTexts.push(`failure minimum update count ${failureMinimumUpdateCount ?? CLEARED_TEXT}`);
64
+ }
65
+ if (failureAction !== undefined) {
66
+ changeTexts.push(`failure action ${failureAction ?? CLEARED_TEXT}`);
67
+ }
68
+ if (isMandatory !== undefined) {
69
+ changeTexts.push(`mandatory ${isMandatory ? 'on' : 'off'}`);
70
+ }
71
+ if (notes !== undefined) {
72
+ changeTexts.push(notes === null ? `notes ${CLEARED_TEXT}` : `notes "${notes}"`);
73
+ }
74
+ return changeTexts;
75
+ }
76
+ function resolveReleaseChanges(options) {
77
+ return {
78
+ ...resolveFailurePolicy(options),
79
+ isMandatory: options.mandatory,
80
+ notes: resolveNullableField(options.notes, options.clearNotes, 'notes'),
81
+ };
82
+ }
@@ -0,0 +1,78 @@
1
+ import { createApiClient } from '../../utils/api-client.js';
2
+ import { buildStepShape, fetchChannelId, readBuildStep, resolveChannelIdByShape, resolveFailureText, resolveOfflineCause, writeBuildResourceFile, } from '../../utils/build-step.js';
3
+ import { defineCommand } from '../../utils/command.js';
4
+ import { printNotice, printWarning } from '../../utils/diagnostics.js';
5
+ import { CliError } from '../../utils/errors.js';
6
+ import { defineCommandOptions } from '../../utils/global-options.js';
7
+ import { printJson } from '../../utils/output.js';
8
+ // Only `binary create` gives an embedded bundle a version, the version of the binary.
9
+ const UNVERSIONED_BUNDLE_VERSION = '';
10
+ export default defineCommand({
11
+ description: 'The build step of every build that is no store build: writes the resource file the SDK reads, listing the embedded bundle, and creates no binary. Without a token, offline or with an API that cannot be reached, the channel name stays unresolved and one warning says so, in CI too.',
12
+ authentication: 'optional',
13
+ examples: [
14
+ 'hotcodepush resource-file write --platform ios --embedded-bundle-path build/App.app/public --resource-file-path build/App.app/hotcodepush.json',
15
+ 'hotcodepush resource-file write --platform android --embedded-bundle-path build/assets/public --resource-file-path build/assets/hotcodepush.json --json',
16
+ ],
17
+ options: defineCommandOptions(buildStepShape),
18
+ action: async (options) => {
19
+ const buildStep = await readBuildStep(options);
20
+ const { embeddedFiles, platform, resourceFilePath } = buildStep;
21
+ if (embeddedFiles === undefined) {
22
+ writeBuildResourceFile(buildStep, resolveChannelIdByShape(buildStep.channelReference), null);
23
+ printNotice(`The ${platform} build bundled no JavaScript, as a debug build served by the development server does. Wrote ${resourceFilePath} without an embedded bundle: over-the-air (OTA) updates are off in this build.`);
24
+ if (options.json) {
25
+ printJson({ resourceFilePath });
26
+ }
27
+ return;
28
+ }
29
+ const { channelId, skippedReason } = await resolveChannel(buildStep);
30
+ writeBuildResourceFile(buildStep, channelId, {
31
+ bundleVersion: UNVERSIONED_BUNDLE_VERSION,
32
+ files: embeddedFiles,
33
+ id: null,
34
+ });
35
+ if (skippedReason !== null) {
36
+ printWarning(skippedReason);
37
+ }
38
+ if (options.json) {
39
+ printJson({ resourceFilePath });
40
+ return;
41
+ }
42
+ console.log(`Wrote ${resourceFilePath} for ${platform}.`);
43
+ },
44
+ });
45
+ // Unlike `binary create`, this never fails offline or without a token, in a pipeline too.
46
+ async function resolveChannel(buildStep) {
47
+ const channelIdByShape = resolveChannelIdByShape(buildStep.channelReference);
48
+ if (channelIdByShape !== null) {
49
+ return { channelId: channelIdByShape, skippedReason: null };
50
+ }
51
+ const offlineCause = resolveOfflineCause();
52
+ if (offlineCause !== undefined) {
53
+ return {
54
+ channelId: null,
55
+ skippedReason: resolveOfflineText(offlineCause),
56
+ };
57
+ }
58
+ try {
59
+ return {
60
+ channelId: await fetchChannelId(createApiClient(), buildStep),
61
+ skippedReason: null,
62
+ };
63
+ }
64
+ catch (error) {
65
+ if (error instanceof CliError) {
66
+ throw error;
67
+ }
68
+ return {
69
+ channelId: null,
70
+ skippedReason: `The channel could not be resolved, so the build names none and takes no OTA updates: ${resolveFailureText(error)}`,
71
+ };
72
+ }
73
+ }
74
+ function resolveOfflineText(offlineCause) {
75
+ return offlineCause === 'offline-switch'
76
+ ? 'HOTCODEPUSH_OFFLINE is set, so the channel name was not resolved. The build names no channel and takes no OTA updates.'
77
+ : 'Not logged in, so the channel name was not resolved. The build names no channel and takes no OTA updates. Run "hotcodepush login" or set HOTCODEPUSH_TOKEN.';
78
+ }
@@ -0,0 +1,45 @@
1
+ import { resolve } from 'node:path';
2
+ import { z } from 'zod';
3
+ import { createApiClient } from '../../utils/api-client.js';
4
+ import { defineCommand } from '../../utils/command.js';
5
+ import { defineCommandOptions } from '../../utils/global-options.js';
6
+ import { printJson } from '../../utils/output.js';
7
+ import { locateProjectConfig } from '../../utils/project-config.js';
8
+ import { promptText } from '../../utils/prompts.js';
9
+ import { fetchAppId } from '../../utils/resource-resolution.js';
10
+ import { readSigningKeyPairFile } from '../../utils/signing-private-key.js';
11
+ import { addPublicKeyToProjectConfig, resolvePublicKeyListingText, UPLOAD_SIGNING_TEXT, } from './create.js';
12
+ export default defineCommand({
13
+ description: 'Add the public key of a key pair you already have, from its private key file, which stays where it is: from then on the app releases only signed bundles.',
14
+ examples: [
15
+ 'hotcodepush signing-key add --private-key-path my-app-private-key.pem',
16
+ 'hotcodepush signing-key add --app "My App" --private-key-path my-app-private-key.pem --json',
17
+ ],
18
+ options: defineCommandOptions({
19
+ privateKeyPath: z
20
+ .string()
21
+ .optional()
22
+ .describe('The private key file of the pair, a PEM of PKCS #8 or PKCS #1, read on this machine. Its public key is derived from it.'),
23
+ }),
24
+ action: async (options) => {
25
+ const privateKeyPath = resolve(options.privateKeyPath ??
26
+ (await promptText('--private-key-path', 'Which private key file?', options)));
27
+ // Read before any request, so a key that is too small or not RSA is refused first.
28
+ const { publicKey } = await readSigningKeyPairFile(privateKeyPath);
29
+ const hotCodePush = createApiClient();
30
+ const projectConfigLocation = locateProjectConfig(options.config);
31
+ const appId = await fetchAppId(hotCodePush, options, projectConfigLocation.projectConfig);
32
+ const createdSigningKey = await hotCodePush.apps.signingKeys.create({
33
+ appId,
34
+ publicKey,
35
+ });
36
+ const isConfigured = addPublicKeyToProjectConfig(projectConfigLocation, appId, publicKey);
37
+ if (options.json) {
38
+ printJson(createdSigningKey);
39
+ return;
40
+ }
41
+ console.log(`Added signing key ${createdSigningKey.keyId} (${createdSigningKey.id}).`);
42
+ console.log(resolvePublicKeyListingText(isConfigured, publicKey));
43
+ console.log(UPLOAD_SIGNING_TEXT);
44
+ },
45
+ });