@bravemobile/react-native-code-push 13.1.0-beta.3 → 13.1.0-beta.5

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 (46) hide show
  1. package/CodePush.podspec +5 -0
  2. package/README.md +20 -145
  3. package/android/app/src/main/java/com/microsoft/codepush/react/ArchiveAttemptLog.java +181 -0
  4. package/android/app/src/main/java/com/microsoft/codepush/react/{BinaryPatchResult.java → ArchiveRestoreResult.java} +17 -7
  5. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +20 -20
  6. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushConstants.java +20 -1
  7. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushErrorCode.java +86 -0
  8. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushHttpException.java +29 -0
  9. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushIncompleteDownloadException.java +17 -0
  10. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +12 -7
  11. package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateManager.java +143 -43
  12. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +31 -31
  13. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushErrorCodeTest.java +90 -0
  14. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerBinaryPatchTest.java +159 -19
  15. package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +304 -15
  16. package/cli/commands/createHistoryCommand/createReleaseHistory.test.ts +133 -0
  17. package/cli/commands/createHistoryCommand/createReleaseHistory.ts +3 -11
  18. package/cli/commands/releaseCommand/addToReleaseHistory.ts +3 -11
  19. package/cli/commands/releaseCommand/release.test.ts +55 -13
  20. package/cli/commands/releaseCommand/release.ts +39 -6
  21. package/cli/commands/updateHistoryCommand/updateReleaseHistory.ts +3 -11
  22. package/cli/dist/commands/createHistoryCommand/createReleaseHistory.js +2 -8
  23. package/cli/dist/commands/createHistoryCommand/createReleaseHistory.test.js +95 -0
  24. package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +2 -8
  25. package/cli/dist/commands/releaseCommand/release.js +23 -8
  26. package/cli/dist/commands/releaseCommand/release.test.js +37 -9
  27. package/cli/dist/commands/updateHistoryCommand/updateReleaseHistory.js +2 -8
  28. package/cli/dist/functions/resolveAssetDiffBases.js +7 -2
  29. package/cli/dist/functions/resolveAssetDiffBases.test.js +26 -12
  30. package/cli/dist/functions/stageReleaseHistoryFile.js +28 -0
  31. package/cli/functions/resolveAssetDiffBases.test.ts +28 -14
  32. package/cli/functions/resolveAssetDiffBases.ts +8 -1
  33. package/cli/functions/stageReleaseHistoryFile.ts +38 -0
  34. package/ios/CodePush/CodePush.h +6 -3
  35. package/ios/CodePush/CodePush.mm +10 -10
  36. package/ios/CodePush/CodePushBinaryPatch.h +18 -8
  37. package/ios/CodePush/CodePushBinaryPatch.m +23 -22
  38. package/ios/CodePush/CodePushDownloadHandler.m +2 -1
  39. package/ios/CodePush/CodePushErrorUtils.m +55 -0
  40. package/ios/CodePush/CodePushPackage.m +242 -71
  41. package/package.json +2 -1
  42. package/src/CodePush.js +28 -18
  43. package/src/CodePush.test.js +110 -55
  44. package/src/package-mixins.js +19 -10
  45. package/typings/react-native-code-push.d.ts +152 -41
  46. package/android/app/src/main/java/com/microsoft/codepush/react/BinaryPatchAttempt.java +0 -92
@@ -17,7 +17,7 @@ import {
17
17
  import { applyPatch } from "../../utils/binaryPatch.js";
18
18
  import { generatePackageHashFromDirectory } from "../../utils/hash-utils.js";
19
19
  import { unzip } from "../../utils/unzip.js";
20
- import type { CliConfigInterface, ReleaseHistoryInterface } from "../../../typings/react-native-code-push.d.ts";
20
+ import type { BundleUploadArtifact, CliConfigInterface, ReleaseHistoryInterface } from "../../../typings/react-native-code-push.d.ts";
21
21
 
22
22
  /**
23
23
  * Covers the release flow around the binary patch option with `--skip-bundle`, which is
@@ -92,15 +92,23 @@ class ProcessExitError extends Error {
92
92
  }
93
93
  }
94
94
 
95
- type Uploads = { filePath: string, downloadUrl: string, logCountBeforeUpload: number }[];
96
-
97
- function recordingUploader(uploads: Uploads, failOn?: (filePath: string) => boolean) {
98
- return async (filePath: string) => {
95
+ type Uploads = {
96
+ artifact: BundleUploadArtifact | undefined;
97
+ downloadUrl: string;
98
+ filePath: string;
99
+ logCountBeforeUpload: number;
100
+ }[];
101
+
102
+ function recordingUploader(
103
+ uploads: Uploads,
104
+ failOn?: (filePath: string) => boolean,
105
+ ): CliConfigInterface['bundleUploader'] {
106
+ return async (filePath, _platform, _identifier, artifact) => {
99
107
  if (failOn?.(filePath)) {
100
108
  throw new Error(`upload rejected: ${path.basename(filePath)}`);
101
109
  }
102
110
  const downloadUrl = `https://cdn.example.com/${path.basename(filePath)}`;
103
- uploads.push({ filePath, downloadUrl, logCountBeforeUpload: logs.length });
111
+ uploads.push({ artifact, filePath, downloadUrl, logCountBeforeUpload: logs.length });
104
112
  return { downloadUrl };
105
113
  };
106
114
  }
@@ -198,12 +206,23 @@ beforeEach(() => {
198
206
 
199
207
  afterEach(() => {
200
208
  jest.restoreAllMocks();
201
- // addToReleaseHistory writes its JSON next to the invocation, and only removes it
202
- // when the history was stored successfully.
203
- fs.rmSync(path.resolve(process.cwd(), `${BINARY_VERSION}.json`), { force: true });
209
+ // addToReleaseHistory writes its JSON under the directory it was invoked in, and only
210
+ // removes it when the history was stored successfully.
211
+ fs.rmSync(path.resolve(process.cwd(), "codepush-release-history"), { recursive: true, force: true });
204
212
  });
205
213
 
206
214
  describe("release without --binary-bundle-path", () => {
215
+ it("keeps existing three-argument uploaders callable", async () => {
216
+ const legacyUploader = async (
217
+ source: string,
218
+ _platform: 'ios' | 'android',
219
+ _identifier?: string,
220
+ ) => ({ downloadUrl: source });
221
+ const uploader: CliConfigInterface['bundleUploader'] = legacyUploader;
222
+
223
+ await expect(uploader('bundle.zip', 'ios')).resolves.toEqual({ downloadUrl: 'bundle.zip' });
224
+ });
225
+
207
226
  it("uploads the full bundle only, exactly as before binary patches existed", async () => {
208
227
  const staged = await stageBundleOutput("full-only");
209
228
 
@@ -211,6 +230,11 @@ describe("release without --binary-bundle-path", () => {
211
230
 
212
231
  expect(uploads).toHaveLength(1);
213
232
  expect(uploads[0].filePath).toBe(`${staged.bundleDirectory}/${staged.bundleFileName}`);
233
+ expect(uploads[0].artifact).toEqual({
234
+ type: 'full-bundle',
235
+ targetBinaryVersion: BINARY_VERSION,
236
+ packageHash: staged.bundleFileName,
237
+ });
214
238
  expect(fs.readdirSync(staged.bundleDirectory)).toEqual([staged.bundleFileName]);
215
239
  expect(releaseHistories[0][APP_VERSION]).toEqual({
216
240
  enabled: true,
@@ -658,11 +682,11 @@ describe("release with asset diff bases", () => {
658
682
 
659
683
  /** Serves the staged base archives, and records which releases were asked for. */
660
684
  function recordingDownloader(bases: StagedBaseRelease[], downloads: string[]): CliConfigInterface['bundleDownloader'] {
661
- return async (downloadUrl: string) => {
662
- downloads.push(downloadUrl);
663
- const base = bases.find((candidate) => candidate.downloadUrl === downloadUrl);
685
+ return async (archive) => {
686
+ downloads.push(archive.downloadUrl);
687
+ const base = bases.find((candidate) => candidate.downloadUrl === archive.downloadUrl);
664
688
  if (!base) {
665
- throw new Error(`the downloader was called with an unexpected url: ${downloadUrl}`);
689
+ throw new Error(`the downloader was called with an unexpected url: ${archive.downloadUrl}`);
666
690
  }
667
691
  return { downloadedFilePath: base.archivePath };
668
692
  };
@@ -687,6 +711,24 @@ describe("release with asset diff bases", () => {
687
711
  `${staged.bundleFileName}${BINARY_PATCH_ARCHIVE_SUFFIX}`,
688
712
  diffFileName,
689
713
  ]);
714
+ expect(uploads.map(({ artifact }) => artifact)).toEqual([
715
+ {
716
+ type: 'full-bundle',
717
+ targetBinaryVersion: BINARY_VERSION,
718
+ packageHash: staged.bundleFileName,
719
+ },
720
+ {
721
+ type: 'binary-patch',
722
+ targetBinaryVersion: BINARY_VERSION,
723
+ packageHash: staged.bundleFileName,
724
+ },
725
+ {
726
+ type: 'asset-diff',
727
+ targetBinaryVersion: BINARY_VERSION,
728
+ packageHash: staged.bundleFileName,
729
+ basePackageHash: base.packageHash,
730
+ },
731
+ ]);
690
732
  expect(downloads).toEqual([base.downloadUrl]);
691
733
  // Every artifact is uploaded before the history points at any of them.
692
734
  expect(uploadCountsWhenHistorySaved).toEqual([3]);
@@ -2,7 +2,7 @@ import fs from "fs";
2
2
  import path from "path";
3
3
  import { bundleCodePush, resolveJsBundleName } from "../bundleCommand/bundleCodePush.js";
4
4
  import { addToReleaseHistory } from "./addToReleaseHistory.js";
5
- import type { CliConfigInterface, ReleaseHistoryInterface } from "../../../typings/react-native-code-push.d.ts";
5
+ import type { BundleUploadArtifact, CliConfigInterface, ReleaseHistoryInterface } from "../../../typings/react-native-code-push.d.ts";
6
6
  import { generatePackageHashFromDirectory } from "../../utils/hash-utils.js";
7
7
  import { unzip } from "../../utils/unzip.js";
8
8
  import { ASSET_DIFF_ARCHIVE_INFIX, makeAssetDiffBundle } from "../../functions/makeAssetDiffBundle.js";
@@ -95,6 +95,7 @@ export async function release(
95
95
  bundleDownloader,
96
96
  bundleDirectory,
97
97
  packageHash,
98
+ targetBinaryVersion: binaryVersion,
98
99
  platform,
99
100
  identifier,
100
101
  })
@@ -102,10 +103,32 @@ export async function release(
102
103
 
103
104
  // Every artifact is uploaded before the release history is touched, so a failed
104
105
  // upload leaves the history describing only updates that can actually be downloaded.
105
- const downloadUrl = await uploadArtifact(bundleUploader, bundleFilePath, platform, identifier, 'bundle');
106
+ const downloadUrl = await uploadArtifact(
107
+ bundleUploader,
108
+ bundleFilePath,
109
+ platform,
110
+ identifier,
111
+ {
112
+ type: 'full-bundle',
113
+ targetBinaryVersion: binaryVersion,
114
+ packageHash,
115
+ },
116
+ 'bundle',
117
+ );
106
118
  let patchDownloadUrl: string | undefined;
107
119
  if (binaryPatch) {
108
- patchDownloadUrl = await uploadArtifact(bundleUploader, binaryPatch.patchBundleFilePath, platform, identifier, 'binary patch bundle');
120
+ patchDownloadUrl = await uploadArtifact(
121
+ bundleUploader,
122
+ binaryPatch.patchBundleFilePath,
123
+ platform,
124
+ identifier,
125
+ {
126
+ type: 'binary-patch',
127
+ targetBinaryVersion: binaryVersion,
128
+ packageHash,
129
+ },
130
+ 'binary patch bundle',
131
+ );
109
132
  console.log(`log: Binary patch archive uploaded (download url: ${patchDownloadUrl})`);
110
133
  }
111
134
  const diffPackages: Record<string, string> = {};
@@ -114,7 +137,12 @@ export async function release(
114
137
  // one that cannot be uploaded is left out of the history instead of failing the
115
138
  // release the other two artifacts are ready for.
116
139
  try {
117
- const diffDownloadUrl = await uploadFile(bundleUploader, filePath, platform, identifier);
140
+ const diffDownloadUrl = await uploadFile(bundleUploader, filePath, platform, identifier, {
141
+ type: 'asset-diff',
142
+ targetBinaryVersion: binaryVersion,
143
+ packageHash,
144
+ basePackageHash,
145
+ });
118
146
  diffPackages[basePackageHash] = diffDownloadUrl;
119
147
  console.log(`log: Asset diff archive against ${basePackageHash} uploaded (download url: ${diffDownloadUrl})`);
120
148
  } catch (error) {
@@ -289,6 +317,7 @@ async function makeAssetDiffArtifacts({
289
317
  bundleDownloader,
290
318
  bundleDirectory,
291
319
  packageHash,
320
+ targetBinaryVersion,
292
321
  platform,
293
322
  identifier,
294
323
  }: {
@@ -298,6 +327,7 @@ async function makeAssetDiffArtifacts({
298
327
  bundleDownloader: NonNullable<CliConfigInterface['bundleDownloader']>;
299
328
  bundleDirectory: string;
300
329
  packageHash: string;
330
+ targetBinaryVersion: string;
301
331
  platform: 'ios' | 'android';
302
332
  identifier: string | undefined;
303
333
  }): Promise<AssetDiffArtifact[]> {
@@ -307,6 +337,7 @@ async function makeAssetDiffArtifacts({
307
337
  bundleDownloader,
308
338
  platform,
309
339
  identifier,
340
+ targetBinaryVersion,
310
341
  });
311
342
 
312
343
  const artifacts: AssetDiffArtifact[] = [];
@@ -421,10 +452,11 @@ async function uploadArtifact(
421
452
  filePath: string,
422
453
  platform: 'ios' | 'android',
423
454
  identifier: string | undefined,
455
+ artifact: BundleUploadArtifact,
424
456
  artifactName: string,
425
457
  ): Promise<string> {
426
458
  try {
427
- return await uploadFile(bundleUploader, filePath, platform, identifier);
459
+ return await uploadFile(bundleUploader, filePath, platform, identifier, artifact);
428
460
  } catch (error) {
429
461
  console.error(`Failed to upload the ${artifactName} file. Exiting the program.\n`, error)
430
462
  process.exit(1)
@@ -436,7 +468,8 @@ async function uploadFile(
436
468
  filePath: string,
437
469
  platform: 'ios' | 'android',
438
470
  identifier: string | undefined,
471
+ artifact: BundleUploadArtifact,
439
472
  ): Promise<string> {
440
- const { downloadUrl } = await bundleUploader(filePath, platform, identifier);
473
+ const { downloadUrl } = await bundleUploader(filePath, platform, identifier, artifact);
441
474
  return downloadUrl
442
475
  }
@@ -1,6 +1,5 @@
1
- import fs from "fs";
2
- import path from "path";
3
1
  import type { CliConfigInterface } from "../../../typings/react-native-code-push.d.ts";
2
+ import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
4
3
 
5
4
  export async function updateReleaseHistory(
6
5
  appVersion: string,
@@ -23,15 +22,8 @@ export async function updateReleaseHistory(
23
22
  if (typeof rollout === "number") updateInfo.rollout = rollout;
24
23
 
25
24
  try {
26
- const JSON_FILE_NAME = `${binaryVersion}.json`;
27
- const JSON_FILE_PATH = path.resolve(process.cwd(), JSON_FILE_NAME);
28
-
29
- console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(releaseHistory, null, 2));
30
- fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(releaseHistory));
31
-
32
- await setReleaseHistory(binaryVersion, JSON_FILE_PATH, releaseHistory, platform, identifier)
33
-
34
- fs.unlinkSync(JSON_FILE_PATH);
25
+ await stageReleaseHistoryFile(binaryVersion, releaseHistory, platform, (jsonFilePath) =>
26
+ setReleaseHistory(binaryVersion, jsonFilePath, releaseHistory, platform, identifier));
35
27
  } catch (error) {
36
28
  console.error('Error occurred while updating history:', error);
37
29
  process.exit(1)
@@ -1,5 +1,4 @@
1
- import fs from "fs";
2
- import path from "path";
1
+ import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
3
2
  export async function createReleaseHistory(targetVersion, setReleaseHistory, platform, identifier) {
4
3
  const BINARY_RELEASE = {
5
4
  enabled: true,
@@ -11,12 +10,7 @@ export async function createReleaseHistory(targetVersion, setReleaseHistory, pla
11
10
  [targetVersion]: BINARY_RELEASE
12
11
  };
13
12
  try {
14
- const JSON_FILE_NAME = `${targetVersion}.json`;
15
- const JSON_FILE_PATH = path.resolve(process.cwd(), JSON_FILE_NAME);
16
- console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(INITIAL_HISTORY, null, 2));
17
- fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(INITIAL_HISTORY));
18
- await setReleaseHistory(targetVersion, JSON_FILE_PATH, INITIAL_HISTORY, platform, identifier);
19
- fs.unlinkSync(JSON_FILE_PATH);
13
+ await stageReleaseHistoryFile(targetVersion, INITIAL_HISTORY, platform, (jsonFilePath) => setReleaseHistory(targetVersion, jsonFilePath, INITIAL_HISTORY, platform, identifier));
20
14
  }
21
15
  catch (error) {
22
16
  console.error('Error occurred while creating new history:', error);
@@ -0,0 +1,95 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
5
+ import { createReleaseHistory } from "./createReleaseHistory.js";
6
+ import { updateReleaseHistory } from "../updateHistoryCommand/updateReleaseHistory.js";
7
+ /**
8
+ * Covers where the release history is staged before the config is handed it.
9
+ *
10
+ * The config receives a path rather than the history itself, so the command has to put the
11
+ * file somewhere first. Two releases of one app for different platforms are the same binary
12
+ * version, so a staging path derived from that version alone is the same path for both, and
13
+ * they overwrite each other's contents before either has read its own.
14
+ */
15
+ const BINARY_VERSION = "1.0.0";
16
+ let workingDir;
17
+ let previousCwd;
18
+ beforeEach(() => {
19
+ previousCwd = process.cwd();
20
+ workingDir = fs.mkdtempSync(path.join(os.tmpdir(), "codepush-history-staging-"));
21
+ process.chdir(workingDir);
22
+ });
23
+ afterEach(() => {
24
+ process.chdir(previousCwd);
25
+ fs.rmSync(workingDir, { recursive: true, force: true });
26
+ jest.restoreAllMocks();
27
+ });
28
+ function stagedPath(platform) {
29
+ return path.join(workingDir, "codepush-release-history", platform, `${BINARY_VERSION}.json`);
30
+ }
31
+ /**
32
+ * Turns the command's own exit into a throw, so a staging failure fails the test instead
33
+ * of taking the worker down with it.
34
+ */
35
+ function failOnExit() {
36
+ jest.spyOn(process, "exit").mockImplementation((code) => {
37
+ throw new Error(`the command exited with code ${String(code)}`);
38
+ });
39
+ jest.spyOn(console, "error").mockImplementation(() => undefined);
40
+ jest.spyOn(console, "log").mockImplementation(() => undefined);
41
+ }
42
+ describe("staging the release history a config is handed", () => {
43
+ it("gives each platform writing at the same time a file of its own", async () => {
44
+ failOnExit();
45
+ const staged = {};
46
+ const setReleaseHistory = async (_binaryVersion, jsonFilePath, _releaseInfo, platform) => {
47
+ // Yields once, which is all it takes for the other platform to reach its own
48
+ // write and its own clean-up while this one still holds only a path.
49
+ await Promise.resolve();
50
+ staged[platform] = { jsonFilePath, contents: fs.readFileSync(jsonFilePath, "utf8") };
51
+ };
52
+ await Promise.all([
53
+ createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840"),
54
+ createReleaseHistory(BINARY_VERSION, setReleaseHistory, "android", "RN0840"),
55
+ ]);
56
+ expect(staged.ios.jsonFilePath).not.toBe(staged.android.jsonFilePath);
57
+ });
58
+ it("hands each platform the history that platform released", async () => {
59
+ failOnExit();
60
+ const released = {
61
+ ios: JSON.stringify({ [BINARY_VERSION]: { enabled: true, mandatory: false, downloadUrl: "", packageHash: "" }, "1.0.1": { enabled: true, mandatory: true, downloadUrl: "ios-url", packageHash: "ios-hash" } }),
62
+ android: JSON.stringify({ [BINARY_VERSION]: { enabled: true, mandatory: false, downloadUrl: "", packageHash: "" }, "1.0.1": { enabled: true, mandatory: true, downloadUrl: "android-url", packageHash: "android-hash" } }),
63
+ };
64
+ const staged = {};
65
+ const getReleaseHistory = async (_binaryVersion, platform) => JSON.parse(released[platform]);
66
+ const setReleaseHistory = async (_binaryVersion, jsonFilePath, _releaseInfo, platform) => {
67
+ await Promise.resolve();
68
+ staged[platform] = fs.readFileSync(jsonFilePath, "utf8");
69
+ };
70
+ await Promise.all([
71
+ updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "ios", "RN0840", undefined, false, undefined),
72
+ updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "android", "RN0840", undefined, false, undefined),
73
+ ]);
74
+ expect(staged.ios).toContain("ios-url");
75
+ expect(staged.ios).not.toContain("android-url");
76
+ expect(staged.android).toContain("android-url");
77
+ expect(staged.android).not.toContain("ios-url");
78
+ });
79
+ it("takes the file away once the config has stored the history", async () => {
80
+ failOnExit();
81
+ const setReleaseHistory = async () => undefined;
82
+ await createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840");
83
+ expect(fs.existsSync(stagedPath("ios"))).toBe(false);
84
+ });
85
+ it("leaves the file behind when the config could not store the history", async () => {
86
+ failOnExit();
87
+ const setReleaseHistory = async () => {
88
+ throw new Error("the storage backend rejected the history");
89
+ };
90
+ await expect(createReleaseHistory(BINARY_VERSION, setReleaseHistory, "ios", "RN0840")).rejects.toThrow("the command exited with code 1");
91
+ // What the release tried to store is worth reading back, so a failed release keeps
92
+ // its history at a path that is the same on every run.
93
+ expect(Object.keys(JSON.parse(fs.readFileSync(stagedPath("ios"), "utf8")))).toEqual([BINARY_VERSION]);
94
+ });
95
+ });
@@ -1,5 +1,4 @@
1
- import path from "path";
2
- import fs from "fs";
1
+ import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
3
2
  export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownloadUrl, binaryPatchDownloadUrl, packageHash, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout, diffPackages) {
4
3
  const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
5
4
  const updateInfo = releaseHistory[appVersion];
@@ -28,12 +27,7 @@ export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownl
28
27
  newReleaseHistory[appVersion].rollout = rollout;
29
28
  }
30
29
  try {
31
- const JSON_FILE_NAME = `${binaryVersion}.json`;
32
- const JSON_FILE_PATH = path.resolve(process.cwd(), JSON_FILE_NAME);
33
- console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(newReleaseHistory, null, 2));
34
- fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(newReleaseHistory));
35
- await setReleaseHistory(binaryVersion, JSON_FILE_PATH, newReleaseHistory, platform, identifier);
36
- fs.unlinkSync(JSON_FILE_PATH);
30
+ await stageReleaseHistoryFile(binaryVersion, newReleaseHistory, platform, (jsonFilePath) => setReleaseHistory(binaryVersion, jsonFilePath, newReleaseHistory, platform, identifier));
37
31
  }
38
32
  catch (error) {
39
33
  console.error('Error occurred while updating history:', error);
@@ -51,16 +51,25 @@ export async function release(bundleUploader, getReleaseHistory, setReleaseHisto
51
51
  bundleDownloader,
52
52
  bundleDirectory,
53
53
  packageHash,
54
+ targetBinaryVersion: binaryVersion,
54
55
  platform,
55
56
  identifier,
56
57
  })
57
58
  : [];
58
59
  // Every artifact is uploaded before the release history is touched, so a failed
59
60
  // upload leaves the history describing only updates that can actually be downloaded.
60
- const downloadUrl = await uploadArtifact(bundleUploader, bundleFilePath, platform, identifier, 'bundle');
61
+ const downloadUrl = await uploadArtifact(bundleUploader, bundleFilePath, platform, identifier, {
62
+ type: 'full-bundle',
63
+ targetBinaryVersion: binaryVersion,
64
+ packageHash,
65
+ }, 'bundle');
61
66
  let patchDownloadUrl;
62
67
  if (binaryPatch) {
63
- patchDownloadUrl = await uploadArtifact(bundleUploader, binaryPatch.patchBundleFilePath, platform, identifier, 'binary patch bundle');
68
+ patchDownloadUrl = await uploadArtifact(bundleUploader, binaryPatch.patchBundleFilePath, platform, identifier, {
69
+ type: 'binary-patch',
70
+ targetBinaryVersion: binaryVersion,
71
+ packageHash,
72
+ }, 'binary patch bundle');
64
73
  console.log(`log: Binary patch archive uploaded (download url: ${patchDownloadUrl})`);
65
74
  }
66
75
  const diffPackages = {};
@@ -69,7 +78,12 @@ export async function release(bundleUploader, getReleaseHistory, setReleaseHisto
69
78
  // one that cannot be uploaded is left out of the history instead of failing the
70
79
  // release the other two artifacts are ready for.
71
80
  try {
72
- const diffDownloadUrl = await uploadFile(bundleUploader, filePath, platform, identifier);
81
+ const diffDownloadUrl = await uploadFile(bundleUploader, filePath, platform, identifier, {
82
+ type: 'asset-diff',
83
+ targetBinaryVersion: binaryVersion,
84
+ packageHash,
85
+ basePackageHash,
86
+ });
73
87
  diffPackages[basePackageHash] = diffDownloadUrl;
74
88
  console.log(`log: Asset diff archive against ${basePackageHash} uploaded (download url: ${diffDownloadUrl})`);
75
89
  }
@@ -180,13 +194,14 @@ async function makeBinaryPatchArtifact({ baseBundlePath, contentsPath, bundleFil
180
194
  *
181
195
  * @return {Promise<AssetDiffArtifact[]>} The diff archives to upload, each naming the package it was built against.
182
196
  */
183
- async function makeAssetDiffArtifacts({ patchBundleFilePath, releaseHistory, diffBaseCount, bundleDownloader, bundleDirectory, packageHash, platform, identifier, }) {
197
+ async function makeAssetDiffArtifacts({ patchBundleFilePath, releaseHistory, diffBaseCount, bundleDownloader, bundleDirectory, packageHash, targetBinaryVersion, platform, identifier, }) {
184
198
  const bases = await resolveAssetDiffBases({
185
199
  releaseHistory,
186
200
  diffBaseCount,
187
201
  bundleDownloader,
188
202
  platform,
189
203
  identifier,
204
+ targetBinaryVersion,
190
205
  });
191
206
  const artifacts = [];
192
207
  const builtAgainst = new Set();
@@ -278,16 +293,16 @@ function warnOnBaseBundleMismatch(outputPath, baseBundlePath) {
278
293
  }
279
294
  }
280
295
  /** Uploads an artifact the release cannot go out without, so a failure ends the program. */
281
- async function uploadArtifact(bundleUploader, filePath, platform, identifier, artifactName) {
296
+ async function uploadArtifact(bundleUploader, filePath, platform, identifier, artifact, artifactName) {
282
297
  try {
283
- return await uploadFile(bundleUploader, filePath, platform, identifier);
298
+ return await uploadFile(bundleUploader, filePath, platform, identifier, artifact);
284
299
  }
285
300
  catch (error) {
286
301
  console.error(`Failed to upload the ${artifactName} file. Exiting the program.\n`, error);
287
302
  process.exit(1);
288
303
  }
289
304
  }
290
- async function uploadFile(bundleUploader, filePath, platform, identifier) {
291
- const { downloadUrl } = await bundleUploader(filePath, platform, identifier);
305
+ async function uploadFile(bundleUploader, filePath, platform, identifier, artifact) {
306
+ const { downloadUrl } = await bundleUploader(filePath, platform, identifier, artifact);
292
307
  return downloadUrl;
293
308
  }
@@ -66,12 +66,12 @@ class ProcessExitError extends Error {
66
66
  }
67
67
  }
68
68
  function recordingUploader(uploads, failOn) {
69
- return async (filePath) => {
69
+ return async (filePath, _platform, _identifier, artifact) => {
70
70
  if (failOn?.(filePath)) {
71
71
  throw new Error(`upload rejected: ${path.basename(filePath)}`);
72
72
  }
73
73
  const downloadUrl = `https://cdn.example.com/${path.basename(filePath)}`;
74
- uploads.push({ filePath, downloadUrl, logCountBeforeUpload: logs.length });
74
+ uploads.push({ artifact, filePath, downloadUrl, logCountBeforeUpload: logs.length });
75
75
  return { downloadUrl };
76
76
  };
77
77
  }
@@ -119,16 +119,26 @@ beforeEach(() => {
119
119
  });
120
120
  afterEach(() => {
121
121
  jest.restoreAllMocks();
122
- // addToReleaseHistory writes its JSON next to the invocation, and only removes it
123
- // when the history was stored successfully.
124
- fs.rmSync(path.resolve(process.cwd(), `${BINARY_VERSION}.json`), { force: true });
122
+ // addToReleaseHistory writes its JSON under the directory it was invoked in, and only
123
+ // removes it when the history was stored successfully.
124
+ fs.rmSync(path.resolve(process.cwd(), "codepush-release-history"), { recursive: true, force: true });
125
125
  });
126
126
  describe("release without --binary-bundle-path", () => {
127
+ it("keeps existing three-argument uploaders callable", async () => {
128
+ const legacyUploader = async (source, _platform, _identifier) => ({ downloadUrl: source });
129
+ const uploader = legacyUploader;
130
+ await expect(uploader('bundle.zip', 'ios')).resolves.toEqual({ downloadUrl: 'bundle.zip' });
131
+ });
127
132
  it("uploads the full bundle only, exactly as before binary patches existed", async () => {
128
133
  const staged = await stageBundleOutput("full-only");
129
134
  const { uploads, releaseHistories } = await runRelease(staged);
130
135
  expect(uploads).toHaveLength(1);
131
136
  expect(uploads[0].filePath).toBe(`${staged.bundleDirectory}/${staged.bundleFileName}`);
137
+ expect(uploads[0].artifact).toEqual({
138
+ type: 'full-bundle',
139
+ targetBinaryVersion: BINARY_VERSION,
140
+ packageHash: staged.bundleFileName,
141
+ });
132
142
  expect(fs.readdirSync(staged.bundleDirectory)).toEqual([staged.bundleFileName]);
133
143
  expect(releaseHistories[0][APP_VERSION]).toEqual({
134
144
  enabled: true,
@@ -426,11 +436,11 @@ describe("release with asset diff bases", () => {
426
436
  }
427
437
  /** Serves the staged base archives, and records which releases were asked for. */
428
438
  function recordingDownloader(bases, downloads) {
429
- return async (downloadUrl) => {
430
- downloads.push(downloadUrl);
431
- const base = bases.find((candidate) => candidate.downloadUrl === downloadUrl);
439
+ return async (archive) => {
440
+ downloads.push(archive.downloadUrl);
441
+ const base = bases.find((candidate) => candidate.downloadUrl === archive.downloadUrl);
432
442
  if (!base) {
433
- throw new Error(`the downloader was called with an unexpected url: ${downloadUrl}`);
443
+ throw new Error(`the downloader was called with an unexpected url: ${archive.downloadUrl}`);
434
444
  }
435
445
  return { downloadedFilePath: base.archivePath };
436
446
  };
@@ -452,6 +462,24 @@ describe("release with asset diff bases", () => {
452
462
  `${staged.bundleFileName}${BINARY_PATCH_ARCHIVE_SUFFIX}`,
453
463
  diffFileName,
454
464
  ]);
465
+ expect(uploads.map(({ artifact }) => artifact)).toEqual([
466
+ {
467
+ type: 'full-bundle',
468
+ targetBinaryVersion: BINARY_VERSION,
469
+ packageHash: staged.bundleFileName,
470
+ },
471
+ {
472
+ type: 'binary-patch',
473
+ targetBinaryVersion: BINARY_VERSION,
474
+ packageHash: staged.bundleFileName,
475
+ },
476
+ {
477
+ type: 'asset-diff',
478
+ targetBinaryVersion: BINARY_VERSION,
479
+ packageHash: staged.bundleFileName,
480
+ basePackageHash: base.packageHash,
481
+ },
482
+ ]);
455
483
  expect(downloads).toEqual([base.downloadUrl]);
456
484
  // Every artifact is uploaded before the history points at any of them.
457
485
  expect(uploadCountsWhenHistorySaved).toEqual([3]);
@@ -1,5 +1,4 @@
1
- import fs from "fs";
2
- import path from "path";
1
+ import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
3
2
  export async function updateReleaseHistory(appVersion, binaryVersion, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout) {
4
3
  const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
5
4
  const updateInfo = releaseHistory[appVersion];
@@ -12,12 +11,7 @@ export async function updateReleaseHistory(appVersion, binaryVersion, getRelease
12
11
  if (typeof rollout === "number")
13
12
  updateInfo.rollout = rollout;
14
13
  try {
15
- const JSON_FILE_NAME = `${binaryVersion}.json`;
16
- const JSON_FILE_PATH = path.resolve(process.cwd(), JSON_FILE_NAME);
17
- console.log(`log: creating JSON file... ("${JSON_FILE_NAME}")\n`, JSON.stringify(releaseHistory, null, 2));
18
- fs.writeFileSync(JSON_FILE_PATH, JSON.stringify(releaseHistory));
19
- await setReleaseHistory(binaryVersion, JSON_FILE_PATH, releaseHistory, platform, identifier);
20
- fs.unlinkSync(JSON_FILE_PATH);
14
+ await stageReleaseHistoryFile(binaryVersion, releaseHistory, platform, (jsonFilePath) => setReleaseHistory(binaryVersion, jsonFilePath, releaseHistory, platform, identifier));
21
15
  }
22
16
  catch (error) {
23
17
  console.error('Error occurred while updating history:', error);
@@ -25,7 +25,7 @@ const TEMP_DIR_PREFIX = 'codepush-asset-diff-base-';
25
25
  * @param bundleDownloader Consumer-provided downloader for a released archive
26
26
  * @return The bases that downloaded and verified, newest release first.
27
27
  */
28
- export async function resolveAssetDiffBases({ releaseHistory, diffBaseCount, bundleDownloader, platform, identifier, }) {
28
+ export async function resolveAssetDiffBases({ releaseHistory, diffBaseCount, bundleDownloader, platform, identifier, targetBinaryVersion, }) {
29
29
  // The seeded entry for the update inside the app binary has neither a url nor a hash,
30
30
  // and nothing can be diffed against a package that was never published. A key that is
31
31
  // not a version cannot be ordered against the others either, and ordering is what picks
@@ -37,7 +37,12 @@ export async function resolveAssetDiffBases({ releaseHistory, diffBaseCount, bun
37
37
  const bases = [];
38
38
  for (const [version, releaseInfo] of candidates) {
39
39
  try {
40
- const { downloadedFilePath } = await bundleDownloader(releaseInfo.downloadUrl, platform, identifier);
40
+ const { downloadedFilePath } = await bundleDownloader({
41
+ downloadUrl: releaseInfo.downloadUrl,
42
+ targetBinaryVersion,
43
+ releaseVersion: version,
44
+ packageHash: releaseInfo.packageHash,
45
+ }, platform, identifier);
41
46
  if (!(await matchesPackageHash(downloadedFilePath, releaseInfo.packageHash))) {
42
47
  console.warn(`warn: Skipping the asset diff base v${version}: the downloaded archive does not hash to the ` +
43
48
  `package hash recorded for it (${releaseInfo.packageHash}).`);