@bravemobile/react-native-code-push 13.1.0-beta.2 → 13.1.0-beta.4

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 (32) hide show
  1. package/README.md +48 -1
  2. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +22 -8
  3. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +5 -2
  4. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +37 -0
  5. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +189 -2
  6. package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
  7. package/cli/commands/releaseCommand/index.test.ts +32 -0
  8. package/cli/commands/releaseCommand/index.ts +16 -1
  9. package/cli/commands/releaseCommand/release.test.ts +259 -10
  10. package/cli/commands/releaseCommand/release.ts +163 -8
  11. package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
  12. package/cli/dist/commands/releaseCommand/index.js +12 -2
  13. package/cli/dist/commands/releaseCommand/index.test.js +25 -0
  14. package/cli/dist/commands/releaseCommand/release.js +108 -10
  15. package/cli/dist/commands/releaseCommand/release.test.js +205 -7
  16. package/cli/dist/functions/makeAssetDiffBundle.js +114 -0
  17. package/cli/dist/functions/makeAssetDiffBundle.test.js +215 -0
  18. package/cli/dist/functions/resolveAssetDiffBases.js +76 -0
  19. package/cli/dist/functions/resolveAssetDiffBases.test.js +188 -0
  20. package/cli/dist/utils/zip.js +12 -2
  21. package/cli/functions/makeAssetDiffBundle.test.ts +283 -0
  22. package/cli/functions/makeAssetDiffBundle.ts +166 -0
  23. package/cli/functions/resolveAssetDiffBases.test.ts +234 -0
  24. package/cli/functions/resolveAssetDiffBases.ts +108 -0
  25. package/cli/package.json +2 -0
  26. package/cli/utils/zip.ts +14 -3
  27. package/ios/CodePush/CodePushBinaryPatch.m +21 -8
  28. package/ios/CodePush/CodePushPackage.m +29 -2
  29. package/package.json +1 -1
  30. package/src/CodePush.js +7 -1
  31. package/src/CodePush.test.js +75 -5
  32. package/typings/react-native-code-push.d.ts +49 -0
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Picks the previously released packages a new release builds asset diffs against, and
3
+ * hands back a verified local copy of each.
4
+ *
5
+ * A diff is only installable by a client whose package is byte for byte the base it was
6
+ * built against, so the base has to be the exact archive the release history describes.
7
+ * Every download is therefore unpacked and hashed before it is accepted: a base that is
8
+ * not what the history recorded would produce a diff that sends every client holding
9
+ * that version into the full-download fallback.
10
+ *
11
+ * Bases are the most recent releases, because that is where the installed clients are.
12
+ * One base failing is never a reason to fail the release - the release still publishes
13
+ * its full and patch archives, and simply serves fewer diffs.
14
+ */
15
+
16
+ import fs from "fs";
17
+ import os from "os";
18
+ import path from "path";
19
+ import semver from "semver";
20
+ import { generatePackageHashFromDirectory } from "../utils/hash-utils.js";
21
+ import { extractCodePushBundleContents } from "./makeBinaryPatchBundle.js";
22
+ import type { CliConfigInterface, ReleaseHistoryInterface } from "../../typings/react-native-code-push.d.ts";
23
+
24
+ const TEMP_DIR_PREFIX = 'codepush-asset-diff-base-';
25
+
26
+ export type AssetDiffBase = {
27
+ basePackageHash: string;
28
+ baseBundleFilePath: string;
29
+ };
30
+
31
+ /**
32
+ * @param releaseHistory {ReleaseHistoryInterface} History of the binary version being released to
33
+ * @param diffBaseCount {number} How many of the most recent releases to build diffs against
34
+ * @param bundleDownloader Consumer-provided downloader for a released archive
35
+ * @return The bases that downloaded and verified, newest release first.
36
+ */
37
+ export async function resolveAssetDiffBases({
38
+ releaseHistory,
39
+ diffBaseCount,
40
+ bundleDownloader,
41
+ platform,
42
+ identifier,
43
+ targetBinaryVersion,
44
+ }: {
45
+ releaseHistory: ReleaseHistoryInterface;
46
+ diffBaseCount: number;
47
+ bundleDownloader: NonNullable<CliConfigInterface['bundleDownloader']>;
48
+ platform: 'ios' | 'android';
49
+ identifier?: string;
50
+ targetBinaryVersion: string;
51
+ }): Promise<AssetDiffBase[]> {
52
+ // The seeded entry for the update inside the app binary has neither a url nor a hash,
53
+ // and nothing can be diffed against a package that was never published. A key that is
54
+ // not a version cannot be ordered against the others either, and ordering is what picks
55
+ // the recent releases, so it is not eligible.
56
+ const candidates = Object.entries(releaseHistory)
57
+ .filter(([version, releaseInfo]) =>
58
+ Boolean(semver.valid(version)) && Boolean(releaseInfo.downloadUrl) && Boolean(releaseInfo.packageHash))
59
+ .sort(([left], [right]) => semver.rcompare(left, right))
60
+ .slice(0, diffBaseCount);
61
+
62
+ const bases: AssetDiffBase[] = [];
63
+ for (const [version, releaseInfo] of candidates) {
64
+ try {
65
+ const { downloadedFilePath } = await bundleDownloader({
66
+ downloadUrl: releaseInfo.downloadUrl,
67
+ targetBinaryVersion,
68
+ releaseVersion: version,
69
+ packageHash: releaseInfo.packageHash,
70
+ }, platform, identifier);
71
+
72
+ if (!(await matchesPackageHash(downloadedFilePath, releaseInfo.packageHash))) {
73
+ console.warn(
74
+ `warn: Skipping the asset diff base v${version}: the downloaded archive does not hash to the ` +
75
+ `package hash recorded for it (${releaseInfo.packageHash}).`,
76
+ );
77
+ continue;
78
+ }
79
+
80
+ bases.push({ basePackageHash: releaseInfo.packageHash, baseBundleFilePath: downloadedFilePath });
81
+ } catch (error) {
82
+ console.warn(
83
+ `warn: Skipping the asset diff base v${version}: ${error instanceof Error ? error.message : String(error)}`,
84
+ );
85
+ }
86
+ }
87
+
88
+ return bases;
89
+ }
90
+
91
+ /**
92
+ * Whether an archive holds the contents the release history recorded for it.
93
+ *
94
+ * Unpacked into a working directory of its own: the extraction reuses one directory name,
95
+ * so bases sharing a parent would have the second unpack wipe the first.
96
+ */
97
+ async function matchesPackageHash(bundleFilePath: string, packageHash: string): Promise<boolean> {
98
+ const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), TEMP_DIR_PREFIX));
99
+
100
+ try {
101
+ const { contentsPath } = await extractCodePushBundleContents(bundleFilePath, tempRoot);
102
+ // Hashed relative to the contents parent, the way the archive itself was hashed.
103
+ const actualHash = await generatePackageHashFromDirectory(contentsPath, path.join(contentsPath, '..'));
104
+ return actualHash === packageHash;
105
+ } finally {
106
+ fs.rmSync(tempRoot, { recursive: true, force: true });
107
+ }
108
+ }
package/cli/package.json CHANGED
@@ -12,6 +12,7 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "commander": "^12.1.0",
15
+ "semver": "^7.3.5",
15
16
  "shelljs": "^0.10.0",
16
17
  "xcode": "^3.0.1",
17
18
  "yazl": "^3.3.1",
@@ -29,6 +30,7 @@
29
30
  "node": ">=18"
30
31
  },
31
32
  "devDependencies": {
33
+ "@types/semver": "^7.5.8",
32
34
  "@types/yazl": "^3.3.0",
33
35
  "@types/yauzl": "^2.10.3"
34
36
  }
package/cli/utils/zip.ts CHANGED
@@ -11,6 +11,20 @@ import { walk } from "./promisfied-fs.js";
11
11
  type ReleaseFile = { sourceLocation: string, targetLocation: string };
12
12
 
13
13
  export function zip(updateContentsPath: string): Promise<string> {
14
+ // For legacy reasons, put the root directory in the zip
15
+ return zipRelativeTo(updateContentsPath, path.join(updateContentsPath, '..'));
16
+ }
17
+
18
+ /**
19
+ * Zips the contents of `rootPath` with entries relative to the root itself, so a file
20
+ * placed directly under the root lands at the archive root rather than inside a wrapper
21
+ * directory.
22
+ */
23
+ export function zipDirectoryContents(rootPath: string): Promise<string> {
24
+ return zipRelativeTo(rootPath, rootPath);
25
+ }
26
+
27
+ function zipRelativeTo(updateContentsPath: string, baseDirectoryPath: string): Promise<string> {
14
28
 
15
29
  // eslint-disable-next-line no-async-promise-executor
16
30
  return new Promise(async (resolve, reject) => {
@@ -30,9 +44,6 @@ export function zip(updateContentsPath: string): Promise<string> {
30
44
  reject(error);
31
45
  }
32
46
 
33
- const directoryPath = updateContentsPath;
34
- const baseDirectoryPath = path.join(directoryPath, '..'); // For legacy reasons, put the root directory in the zip
35
-
36
47
  const files: string[] = await walk(updateContentsPath);
37
48
 
38
49
  files.forEach((filePath) => {
@@ -40,6 +40,10 @@ static NSString *const BinaryPatchTargetBundleHashKey = @"targetBundleHash";
40
40
  static NSString *const BinaryPatchTargetBundleSizeKey = @"targetBundleSize";
41
41
  static const NSInteger BinaryPatchFormatVersion = 1;
42
42
 
43
+ /* The manifest of the assets to delete that a diff archive carries at its root. Paired
44
+ * with CodePushPackage.m's DiffManifestFileName, which is file-local there as well. */
45
+ static NSString *const AssetDiffManifestFileName = @"hotcodepush.json";
46
+
43
47
  /*
44
48
  * The largest bundle a patch is allowed to promise. A manifest is untrusted input, and
45
49
  * the size in it decides how much disk the restore asks for before a single byte of the
@@ -373,21 +377,30 @@ static CodePushBinaryPatchApplyResult CodePushApplyBinaryPatch(const unsigned ch
373
377
  * An archive wraps its files in a single directory, and the manifest's paths are
374
378
  * relative to that directory rather than to the archive. An archive whose files are at
375
379
  * the top level is its own contents root, which is how the tooling that unpacks one
376
- * reads it too.
380
+ * reads it too. A diff archive carries the manifest of the assets to delete beside that
381
+ * wrapper directory, at the root, so that file does not count against the shape.
377
382
  */
378
383
  + (NSString *)contentsFolderInUnzippedFolder:(NSString *)unzippedFolderPath
379
384
  {
380
- NSArray *entries = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:unzippedFolderPath
381
- error:nil];
382
- if (entries.count == 1) {
383
- NSString *entryPath = [unzippedFolderPath stringByAppendingPathComponent:entries.firstObject];
385
+ NSFileManager *fileManager = [NSFileManager defaultManager];
386
+ NSArray *entries = [fileManager contentsOfDirectoryAtPath:unzippedFolderPath error:nil];
387
+ NSString *wrapperPath = nil;
388
+ for (NSString *entry in entries) {
389
+ NSString *entryPath = [unzippedFolderPath stringByAppendingPathComponent:entry];
384
390
  BOOL isDirectory = NO;
385
- if ([[NSFileManager defaultManager] fileExistsAtPath:entryPath isDirectory:&isDirectory] && isDirectory) {
386
- return entryPath;
391
+ BOOL exists = [fileManager fileExistsAtPath:entryPath isDirectory:&isDirectory];
392
+ if (exists && !isDirectory && [entry isEqualToString:AssetDiffManifestFileName]) {
393
+ continue;
387
394
  }
395
+ if (wrapperPath == nil && exists && isDirectory) {
396
+ wrapperPath = entryPath;
397
+ continue;
398
+ }
399
+
400
+ return unzippedFolderPath;
388
401
  }
389
402
 
390
- return unzippedFolderPath;
403
+ return wrapperPath != nil ? wrapperPath : unzippedFolderPath;
391
404
  }
392
405
 
393
406
  /**
@@ -329,8 +329,12 @@ static NSString *const UnzippedFolderName = @"unzipped";
329
329
  error:&error];
330
330
  NSArray *deletedFiles = manifestJSON[@"deletedFiles"];
331
331
  for (NSString *deletedFileName in deletedFiles) {
332
- NSString *absoluteDeletedFilePath = [newUpdateFolderPath stringByAppendingPathComponent:deletedFileName];
333
- if ([[NSFileManager defaultManager] fileExistsAtPath:absoluteDeletedFilePath]) {
332
+ // The manifest is untrusted input, so an entry that reaches outside the
333
+ // package folder is ignored rather than followed. Whether skipping it
334
+ // left the update whole is the folder hash check's answer to give.
335
+ NSString *absoluteDeletedFilePath = [self pathInsideFolder:newUpdateFolderPath
336
+ relativePath:deletedFileName];
337
+ if (absoluteDeletedFilePath && [[NSFileManager defaultManager] fileExistsAtPath:absoluteDeletedFilePath]) {
334
338
  [[NSFileManager defaultManager] removeItemAtPath:absoluteDeletedFilePath
335
339
  error:&error];
336
340
  if (error) {
@@ -468,6 +472,29 @@ static NSString *const UnzippedFolderName = @"unzipped";
468
472
  return [[self getCodePushPath] stringByAppendingPathComponent:BinaryPatchFolderName];
469
473
  }
470
474
 
475
+ /*
476
+ * Resolves a path a manifest points at, refusing anything that would reach outside the
477
+ * folder it is relative to - an archive is untrusted input, and its manifest is no more
478
+ * trusted than its entries.
479
+ *
480
+ * @return the resolved path, or nil when the path is unusable
481
+ */
482
+ + (NSString *)pathInsideFolder:(NSString *)folderPath
483
+ relativePath:(NSString *)relativePath
484
+ {
485
+ if (relativePath.length == 0 || [relativePath isAbsolutePath]) {
486
+ return nil;
487
+ }
488
+
489
+ NSString *resolvedFolderPath = [[folderPath stringByStandardizingPath] stringByAppendingString:@"/"];
490
+ NSString *resolvedPath = [[folderPath stringByAppendingPathComponent:relativePath] stringByStandardizingPath];
491
+ if (![resolvedPath hasPrefix:resolvedFolderPath]) {
492
+ return nil;
493
+ }
494
+
495
+ return resolvedPath;
496
+ }
497
+
471
498
  + (NSString *)getCodePushPath
472
499
  {
473
500
  NSString* codePushPath = [[CodePush getApplicationSupportDirectory] stringByAppendingPathComponent:@"CodePush"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bravemobile/react-native-code-push",
3
- "version": "13.1.0-beta.2",
3
+ "version": "13.1.0-beta.4",
4
4
  "description": "React Native plugin for the CodePush service",
5
5
  "main": "src/CodePush.js",
6
6
  "react-native": "src/CodePush.js",
package/src/CodePush.js CHANGED
@@ -174,6 +174,12 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
174
174
  const [latestVersion, latestReleaseInfo] = versioning.findLatestRelease();
175
175
  const isMandatory = versioning.checkIsMandatory(runtimeVersion);
176
176
 
177
+ // A diff archive is a patch archive computed against one specific earlier release,
178
+ // so it only applies when that exact release is the one installed right now.
179
+ const diffPackageDownloadUrl = localPackage && localPackage.packageHash
180
+ ? latestReleaseInfo.diffPackages?.[localPackage.packageHash]
181
+ : undefined;
182
+
177
183
  /**
178
184
  * Convert the update information decided from `ReleaseHistoryInterface` to be passed to the library core (original CodePush library).
179
185
  *
@@ -185,7 +191,7 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
185
191
  * Only released updates that were published with a binary patch carry this.
186
192
  * When it is missing, the update is downloaded in full from `download_url`.
187
193
  */
188
- binary_patch_download_url: latestReleaseInfo.binaryPatchDownloadUrl,
194
+ binary_patch_download_url: diffPackageDownloadUrl || latestReleaseInfo.binaryPatchDownloadUrl,
189
195
  // (`enabled` will always be true in the release information obtained from the previous process.)
190
196
  is_available: latestReleaseInfo.enabled,
191
197
  package_hash: latestReleaseInfo.packageHash,
@@ -27,6 +27,8 @@ const LABEL = '1.0.1';
27
27
  const PACKAGE_HASH = 'a'.repeat(64);
28
28
  const DOWNLOAD_URL = 'https://cdn.example.com/full.zip';
29
29
  const BINARY_PATCH_DOWNLOAD_URL = 'https://cdn.example.com/full.zip-patch.zip';
30
+ const DIFF_URL = 'https://cdn.example.com/diff-from-base.zip';
31
+ const INSTALLED_HASH = 'base-package-hash';
30
32
 
31
33
  /** The release the CLI writes when only the full bundle was published. */
32
34
  function fullOnlyRelease() {
@@ -50,19 +52,27 @@ function patchedRelease() {
50
52
  };
51
53
  }
52
54
 
55
+ /** The release the CLI writes when the update also has a diff archive against an earlier one. */
56
+ function diffRelease() {
57
+ const history = patchedRelease();
58
+ history[LABEL].diffPackages = { [INSTALLED_HASH]: DIFF_URL };
59
+ return history;
60
+ }
61
+
53
62
  /**
54
63
  * @param binaryPatchResult what the native side reports the patch attempt ended in, which
55
64
  * only a download that had a patch to try comes back with.
65
+ * @param installedPackage the CodePush update the app is running, if any.
56
66
  */
57
- function createNativeBridge({ binaryPatchResult } = {}) {
67
+ function createNativeBridge({ binaryPatchResult, installedPackage } = {}) {
58
68
  return {
59
69
  addDownloadProgressListener: jest.fn(() => ({ remove: jest.fn() })),
60
70
  downloadUpdate: jest.fn(async (updatePackage) => ({
61
71
  ...updatePackage,
62
72
  ...(binaryPatchResult ? { binaryPatchResult } : {}),
63
73
  })),
64
- // No CodePush update is installed, so the app runs the bundle of its binary.
65
- getUpdateMetadata: jest.fn(async () => null),
74
+ // Without an installed CodePush update, the app runs the bundle of its binary.
75
+ getUpdateMetadata: jest.fn(async () => installedPackage ?? null),
66
76
  isFailedUpdate: jest.fn(async () => false),
67
77
  isFirstRun: jest.fn(async () => false),
68
78
  // The rest of what a full `sync()` reaches for.
@@ -82,10 +92,10 @@ function createNativeBridge({ binaryPatchResult } = {}) {
82
92
  * @param onBinaryPatchResult a callback the app registers on the decorator, as opposed to
83
93
  * one it passes to a single `sync()` call.
84
94
  */
85
- function loadCodePush({ releaseHistory = {}, updateChecker, binaryPatchResult, onBinaryPatchResult } = {}) {
95
+ function loadCodePush({ releaseHistory = {}, updateChecker, binaryPatchResult, onBinaryPatchResult, installedPackage } = {}) {
86
96
  jest.resetModules();
87
97
  const CodePush = require('./CodePush');
88
- const nativeBridge = createNativeBridge({ binaryPatchResult });
98
+ const nativeBridge = createNativeBridge({ binaryPatchResult, installedPackage });
89
99
 
90
100
  CodePush.setUpTestDependencies(
91
101
  null,
@@ -200,6 +210,66 @@ describe('checkForUpdate without a binary patch in the release history', () => {
200
210
  });
201
211
  });
202
212
 
213
+ /**
214
+ * A diff archive only applies to the one release it was computed against, so which of the
215
+ * two patch urls of a release is downloaded depends on what the app is running right now.
216
+ */
217
+ describe('checkForUpdate with asset diff packages in the release history', () => {
218
+ it('downloads the diff archive when the installed update matches a diff base', async () => {
219
+ const { CodePush, nativeBridge } = loadCodePush({
220
+ releaseHistory: diffRelease(),
221
+ installedPackage: { packageHash: INSTALLED_HASH },
222
+ });
223
+
224
+ const remotePackage = await CodePush.checkForUpdate();
225
+ await remotePackage.download();
226
+
227
+ expect(downloadedPackageMetadata(nativeBridge)).toMatchObject({
228
+ downloadUrl: DOWNLOAD_URL,
229
+ binaryPatchDownloadUrl: DIFF_URL,
230
+ });
231
+ });
232
+
233
+ it('downloads the plain patch archive when the installed update matches no diff base', async () => {
234
+ const { CodePush, nativeBridge } = loadCodePush({
235
+ releaseHistory: diffRelease(),
236
+ installedPackage: { packageHash: 'other-hash' },
237
+ });
238
+
239
+ const remotePackage = await CodePush.checkForUpdate();
240
+ await remotePackage.download();
241
+
242
+ expect(downloadedPackageMetadata(nativeBridge)).toMatchObject({
243
+ binaryPatchDownloadUrl: BINARY_PATCH_DOWNLOAD_URL,
244
+ });
245
+ });
246
+
247
+ it('downloads the plain patch archive when no update is installed', async () => {
248
+ const { CodePush, nativeBridge } = loadCodePush({ releaseHistory: diffRelease() });
249
+
250
+ const remotePackage = await CodePush.checkForUpdate();
251
+ await remotePackage.download();
252
+
253
+ expect(downloadedPackageMetadata(nativeBridge)).toMatchObject({
254
+ binaryPatchDownloadUrl: BINARY_PATCH_DOWNLOAD_URL,
255
+ });
256
+ });
257
+
258
+ it('does not send the diff package map across the bridge', async () => {
259
+ const { CodePush, nativeBridge } = loadCodePush({
260
+ releaseHistory: diffRelease(),
261
+ installedPackage: { packageHash: INSTALLED_HASH },
262
+ });
263
+
264
+ const remotePackage = await CodePush.checkForUpdate();
265
+ expect(remotePackage).not.toHaveProperty('diffPackages');
266
+
267
+ await remotePackage.download();
268
+
269
+ expect(downloadedPackageMetadata(nativeBridge)).not.toHaveProperty('diffPackages');
270
+ });
271
+ });
272
+
203
273
  describe('checkForUpdate through the deprecated updateChecker', () => {
204
274
  it('carries the patch url of the update check response', async () => {
205
275
  const { CodePush } = loadCodePush({
@@ -34,6 +34,13 @@ export interface ReleaseInfo {
34
34
  * that cannot use it downloads the full update from `downloadUrl` instead.
35
35
  */
36
36
  binaryPatchDownloadUrl?: string;
37
+ /**
38
+ * URLs of patch archives that also carry an asset diff, keyed by the packageHash of the
39
+ * release each archive was diffed against. A client whose installed update matches one of
40
+ * these keys downloads that archive instead of `binaryPatchDownloadUrl`; every other client
41
+ * ignores this field. Only present when the release was published with asset diff archives.
42
+ */
43
+ diffPackages?: Record<string, string>;
37
44
  packageHash: string;
38
45
  rollout?: number;
39
46
  }
@@ -626,6 +633,31 @@ declare namespace CodePush {
626
633
 
627
634
  export default CodePush;
628
635
 
636
+ /**
637
+ * Identifies the archive passed to `bundleUploader` during a release.
638
+ *
639
+ * `targetBinaryVersion` is the value of the release command's `--binary-version` option.
640
+ * A full bundle's `packageHash` identifies its contents across target binary versions, while
641
+ * binary patch and asset diff archives need the target binary version to distinguish their contents.
642
+ * Asset diff archives also identify the released package they were built against.
643
+ */
644
+ export type BundleUploadArtifact = {
645
+ targetBinaryVersion: string;
646
+ packageHash: string;
647
+ } & (
648
+ | { type: 'full-bundle' }
649
+ | { type: 'binary-patch' }
650
+ | { type: 'asset-diff'; basePackageHash: string }
651
+ );
652
+
653
+ /** Identifies the previously released full archive passed to `bundleDownloader`. */
654
+ export interface BundleDownloadInfo {
655
+ downloadUrl: string;
656
+ targetBinaryVersion: string;
657
+ releaseVersion: ReleaseVersion;
658
+ packageHash: string;
659
+ }
660
+
629
661
  /**
630
662
  * Interface for the config file required for `npx code-push` CLI operation.
631
663
  *
@@ -640,12 +672,14 @@ export interface CliConfigInterface {
640
672
  *
641
673
  * @param source The relative path of the generated bundle file. (e.g. build/bundleOutput/1087bc338fc45a961c...)
642
674
  * @param platform The target platform of the bundle file. This is the string passed when executing the CLI command. ('ios'/'android')
675
+ * @param artifact Information that identifies the full bundle, binary patch, or asset diff archive. The release command always supplies it; it remains optional so existing callers and three-parameter implementations stay compatible.
643
676
  * @return {Promise<{downloadUrl: string}>} An object containing the `downloadUrl` property, which is the URL from which the uploaded bundle file can be downloaded. This URL will be recorded in the release history.
644
677
  */
645
678
  bundleUploader: (
646
679
  source: string,
647
680
  platform: "ios" | "android",
648
681
  identifier?: string,
682
+ artifact?: BundleUploadArtifact,
649
683
  ) => Promise<{downloadUrl: string}>;
650
684
 
651
685
  /**
@@ -683,4 +717,19 @@ export interface CliConfigInterface {
683
717
  platform: "ios" | "android",
684
718
  identifier?: string,
685
719
  ) => Promise<void>;
720
+
721
+ /**
722
+ * Downloads a previously released update archive so the release command can compute
723
+ * asset diffs against it. Receives the `downloadUrl` recorded in the release history and
724
+ * must resolve with the local path of the downloaded file.
725
+ * Optional: when absent, releases are published without asset diff archives.
726
+ *
727
+ * @param archive Information that identifies the released full archive. The release command
728
+ * supplies its download URL, target binary version, release version, and package hash together.
729
+ */
730
+ bundleDownloader?: (
731
+ archive: BundleDownloadInfo,
732
+ platform: "ios" | "android",
733
+ identifier?: string,
734
+ ) => Promise<{ downloadedFilePath: string }>;
686
735
  }