@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.
- package/README.md +48 -1
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushBinaryPatch.java +22 -8
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushUpdateUtils.java +5 -2
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushBinaryPatchTest.java +37 -0
- package/android/app/src/test/java/com/microsoft/codepush/react/CodePushUpdateManagerDownloadTest.java +189 -2
- package/cli/commands/releaseCommand/addToReleaseHistory.ts +7 -0
- package/cli/commands/releaseCommand/index.test.ts +32 -0
- package/cli/commands/releaseCommand/index.ts +16 -1
- package/cli/commands/releaseCommand/release.test.ts +259 -10
- package/cli/commands/releaseCommand/release.ts +163 -8
- package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +6 -1
- package/cli/dist/commands/releaseCommand/index.js +12 -2
- package/cli/dist/commands/releaseCommand/index.test.js +25 -0
- package/cli/dist/commands/releaseCommand/release.js +108 -10
- package/cli/dist/commands/releaseCommand/release.test.js +205 -7
- package/cli/dist/functions/makeAssetDiffBundle.js +114 -0
- package/cli/dist/functions/makeAssetDiffBundle.test.js +215 -0
- package/cli/dist/functions/resolveAssetDiffBases.js +76 -0
- package/cli/dist/functions/resolveAssetDiffBases.test.js +188 -0
- package/cli/dist/utils/zip.js +12 -2
- package/cli/functions/makeAssetDiffBundle.test.ts +283 -0
- package/cli/functions/makeAssetDiffBundle.ts +166 -0
- package/cli/functions/resolveAssetDiffBases.test.ts +234 -0
- package/cli/functions/resolveAssetDiffBases.ts +108 -0
- package/cli/package.json +2 -0
- package/cli/utils/zip.ts +14 -3
- package/ios/CodePush/CodePushBinaryPatch.m +21 -8
- package/ios/CodePush/CodePushPackage.m +29 -2
- package/package.json +1 -1
- package/src/CodePush.js +7 -1
- package/src/CodePush.test.js +75 -5
- package/typings/react-native-code-push.d.ts +49 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
1
2
|
import fs from "fs";
|
|
2
3
|
import os from "os";
|
|
3
4
|
import path from "path";
|
|
4
5
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
5
6
|
import { release } from "./release.js";
|
|
6
7
|
import { makeCodePushBundle } from "../../functions/makeCodePushBundle.js";
|
|
8
|
+
import { ASSET_DIFF_ARCHIVE_INFIX, assetDiffArchiveName } from "../../functions/makeAssetDiffBundle.js";
|
|
7
9
|
import {
|
|
8
10
|
BINARY_PATCH_ARCHIVE_SUFFIX,
|
|
9
11
|
BINARY_PATCH_BASE_RECORD_NAME,
|
|
@@ -15,7 +17,7 @@ import {
|
|
|
15
17
|
import { applyPatch } from "../../utils/binaryPatch.js";
|
|
16
18
|
import { generatePackageHashFromDirectory } from "../../utils/hash-utils.js";
|
|
17
19
|
import { unzip } from "../../utils/unzip.js";
|
|
18
|
-
import type { ReleaseHistoryInterface } from "../../../typings/react-native-code-push.d.ts";
|
|
20
|
+
import type { BundleUploadArtifact, CliConfigInterface, ReleaseHistoryInterface } from "../../../typings/react-native-code-push.d.ts";
|
|
19
21
|
|
|
20
22
|
/**
|
|
21
23
|
* Covers the release flow around the binary patch option with `--skip-bundle`, which is
|
|
@@ -90,29 +92,38 @@ class ProcessExitError extends Error {
|
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
type Uploads = {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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) => {
|
|
97
107
|
if (failOn?.(filePath)) {
|
|
98
108
|
throw new Error(`upload rejected: ${path.basename(filePath)}`);
|
|
99
109
|
}
|
|
100
110
|
const downloadUrl = `https://cdn.example.com/${path.basename(filePath)}`;
|
|
101
|
-
uploads.push({ filePath, downloadUrl, logCountBeforeUpload: logs.length });
|
|
111
|
+
uploads.push({ artifact, filePath, downloadUrl, logCountBeforeUpload: logs.length });
|
|
102
112
|
return { downloadUrl };
|
|
103
113
|
};
|
|
104
114
|
}
|
|
105
115
|
|
|
106
|
-
function historyStore() {
|
|
116
|
+
function historyStore(existingHistory: ReleaseHistoryInterface = {}, onSave?: () => void) {
|
|
107
117
|
const saved: ReleaseHistoryInterface[] = [];
|
|
108
118
|
return {
|
|
109
119
|
saved,
|
|
110
|
-
getReleaseHistory: async () =>
|
|
120
|
+
getReleaseHistory: async () => existingHistory,
|
|
111
121
|
setReleaseHistory: async (
|
|
112
122
|
_binaryVersion: string,
|
|
113
123
|
_jsonFilePath: string,
|
|
114
124
|
releaseInfo: ReleaseHistoryInterface,
|
|
115
125
|
) => {
|
|
126
|
+
onSave?.();
|
|
116
127
|
saved.push(releaseInfo);
|
|
117
128
|
},
|
|
118
129
|
};
|
|
@@ -127,11 +138,18 @@ type ReleaseOverrides = {
|
|
|
127
138
|
onOversizedPatch?: OversizedPatchPolicy;
|
|
128
139
|
/** Passed in when the case has to read the uploads of a release that threw. */
|
|
129
140
|
uploads?: Uploads;
|
|
141
|
+
/** What the consumer already has released, which the asset diff bases are picked from. */
|
|
142
|
+
releaseHistory?: ReleaseHistoryInterface;
|
|
143
|
+
bundleDownloader?: CliConfigInterface['bundleDownloader'];
|
|
144
|
+
diffBaseCount?: number;
|
|
130
145
|
};
|
|
131
146
|
|
|
132
147
|
async function runRelease(staged: StagedBundle, overrides: ReleaseOverrides = {}) {
|
|
133
148
|
const uploads: Uploads = overrides.uploads ?? [];
|
|
134
|
-
const
|
|
149
|
+
const uploadCountsWhenHistorySaved: number[] = [];
|
|
150
|
+
const history = historyStore(overrides.releaseHistory, () => {
|
|
151
|
+
uploadCountsWhenHistorySaved.push(uploads.length);
|
|
152
|
+
});
|
|
135
153
|
|
|
136
154
|
await release(
|
|
137
155
|
recordingUploader(uploads, overrides.uploadFailsFor),
|
|
@@ -156,9 +174,11 @@ async function runRelease(staged: StagedBundle, overrides: ReleaseOverrides = {}
|
|
|
156
174
|
undefined,
|
|
157
175
|
overrides.binaryBundlePath,
|
|
158
176
|
overrides.onOversizedPatch,
|
|
177
|
+
overrides.bundleDownloader,
|
|
178
|
+
overrides.diffBaseCount,
|
|
159
179
|
);
|
|
160
180
|
|
|
161
|
-
return { uploads, releaseHistories: history.saved };
|
|
181
|
+
return { uploads, releaseHistories: history.saved, uploadCountsWhenHistorySaved };
|
|
162
182
|
}
|
|
163
183
|
|
|
164
184
|
let logs: string[];
|
|
@@ -192,6 +212,17 @@ afterEach(() => {
|
|
|
192
212
|
});
|
|
193
213
|
|
|
194
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
|
+
|
|
195
226
|
it("uploads the full bundle only, exactly as before binary patches existed", async () => {
|
|
196
227
|
const staged = await stageBundleOutput("full-only");
|
|
197
228
|
|
|
@@ -199,6 +230,11 @@ describe("release without --binary-bundle-path", () => {
|
|
|
199
230
|
|
|
200
231
|
expect(uploads).toHaveLength(1);
|
|
201
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
|
+
});
|
|
202
238
|
expect(fs.readdirSync(staged.bundleDirectory)).toEqual([staged.bundleFileName]);
|
|
203
239
|
expect(releaseHistories[0][APP_VERSION]).toEqual({
|
|
204
240
|
enabled: true,
|
|
@@ -580,3 +616,216 @@ describe("release --on-oversized-patch", () => {
|
|
|
580
616
|
expect(uploads).toHaveLength(2);
|
|
581
617
|
});
|
|
582
618
|
});
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* A release can also ship a diff per recently released package: the patch archive without
|
|
622
|
+
* the files that package already holds. The bases come out of the release history and are
|
|
623
|
+
* downloaded through the consumer's `bundleDownloader`, so a release only builds diffs
|
|
624
|
+
* where the consumer supplied one.
|
|
625
|
+
*/
|
|
626
|
+
describe("release with asset diff bases", () => {
|
|
627
|
+
/**
|
|
628
|
+
* The asset both packages hold. Sized like a real asset because a diff is only shipped
|
|
629
|
+
* when it comes out smaller than the patch archive it was derived from.
|
|
630
|
+
*/
|
|
631
|
+
const sharedAsset = crypto.randomBytes(4096);
|
|
632
|
+
|
|
633
|
+
const PREVIOUS_APP_VERSION = '9.9.9';
|
|
634
|
+
|
|
635
|
+
function updateFiles(): Record<string, Buffer | string> {
|
|
636
|
+
return { 'main.jsbundle': fs.readFileSync(targetFixture), 'assets/keep.png': sharedAsset };
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
type StagedBaseRelease = { downloadUrl: string, packageHash: string, archivePath: string };
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* A package released earlier, as the history describes it and as the CDN serves it.
|
|
643
|
+
*
|
|
644
|
+
* @param ownAsset An asset only this release holds, which is what makes two staged base releases two packages rather than one.
|
|
645
|
+
*/
|
|
646
|
+
async function stageBaseRelease(caseName: string, ownAsset?: string): Promise<StagedBaseRelease> {
|
|
647
|
+
const caseDir = fs.mkdtempSync(path.join(workDir, `${caseName}-base-`));
|
|
648
|
+
const contentsPath = path.join(caseDir, CONTENTS_DIR_NAME);
|
|
649
|
+
const files: Record<string, Buffer | string> = {
|
|
650
|
+
'main.jsbundle': fs.readFileSync(baseFixture),
|
|
651
|
+
'assets/keep.png': sharedAsset,
|
|
652
|
+
...(ownAsset === undefined ? {} : { 'assets/dropped.png': ownAsset }),
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
656
|
+
const filePath = path.join(contentsPath, relativePath);
|
|
657
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
658
|
+
fs.writeFileSync(filePath, content);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const archiveDir = path.join(caseDir, 'archive');
|
|
662
|
+
const { bundleFileName } = await makeCodePushBundle(contentsPath, archiveDir);
|
|
663
|
+
|
|
664
|
+
return {
|
|
665
|
+
downloadUrl: `https://cdn.example.com/released/${bundleFileName}`,
|
|
666
|
+
packageHash: bundleFileName,
|
|
667
|
+
archivePath: path.join(archiveDir, bundleFileName),
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function releaseHistoryOf(base: StagedBaseRelease, versions = [PREVIOUS_APP_VERSION]): ReleaseHistoryInterface {
|
|
672
|
+
return Object.fromEntries(versions.map((version) => [
|
|
673
|
+
version,
|
|
674
|
+
{
|
|
675
|
+
enabled: true,
|
|
676
|
+
mandatory: false,
|
|
677
|
+
downloadUrl: base.downloadUrl,
|
|
678
|
+
packageHash: base.packageHash,
|
|
679
|
+
},
|
|
680
|
+
]));
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** Serves the staged base archives, and records which releases were asked for. */
|
|
684
|
+
function recordingDownloader(bases: StagedBaseRelease[], downloads: string[]): CliConfigInterface['bundleDownloader'] {
|
|
685
|
+
return async (archive) => {
|
|
686
|
+
downloads.push(archive.downloadUrl);
|
|
687
|
+
const base = bases.find((candidate) => candidate.downloadUrl === archive.downloadUrl);
|
|
688
|
+
if (!base) {
|
|
689
|
+
throw new Error(`the downloader was called with an unexpected url: ${archive.downloadUrl}`);
|
|
690
|
+
}
|
|
691
|
+
return { downloadedFilePath: base.archivePath };
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
it("publishes asset diff archives against recent releases and records them in the history", async () => {
|
|
696
|
+
const staged = await stageBundleOutput("asset-diff", updateFiles());
|
|
697
|
+
const base = await stageBaseRelease("asset-diff");
|
|
698
|
+
const downloads: string[] = [];
|
|
699
|
+
|
|
700
|
+
const { uploads, releaseHistories, uploadCountsWhenHistorySaved } = await runRelease(staged, {
|
|
701
|
+
binaryBundlePath: baseFixture,
|
|
702
|
+
releaseHistory: releaseHistoryOf(base),
|
|
703
|
+
bundleDownloader: recordingDownloader([base], downloads),
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
const diffFileName = `${staged.bundleFileName}${ASSET_DIFF_ARCHIVE_INFIX}${base.packageHash}.zip`;
|
|
707
|
+
// The diff is the last artifact: it is an optimisation on top of the patch, which
|
|
708
|
+
// is itself an optimisation on top of the full archive.
|
|
709
|
+
expect(uploads.map(({ filePath }) => path.basename(filePath))).toEqual([
|
|
710
|
+
staged.bundleFileName,
|
|
711
|
+
`${staged.bundleFileName}${BINARY_PATCH_ARCHIVE_SUFFIX}`,
|
|
712
|
+
diffFileName,
|
|
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
|
+
]);
|
|
732
|
+
expect(downloads).toEqual([base.downloadUrl]);
|
|
733
|
+
// Every artifact is uploaded before the history points at any of them.
|
|
734
|
+
expect(uploadCountsWhenHistorySaved).toEqual([3]);
|
|
735
|
+
expect(releaseHistories[0][APP_VERSION].diffPackages).toEqual({
|
|
736
|
+
[base.packageHash]: uploads[2].downloadUrl,
|
|
737
|
+
});
|
|
738
|
+
expect(fs.existsSync(path.join(staged.bundleDirectory, diffFileName))).toBe(true);
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it("publishes without diff archives when the config has no bundle downloader", async () => {
|
|
742
|
+
const staged = await stageBundleOutput("no-downloader", updateFiles());
|
|
743
|
+
const base = await stageBaseRelease("no-downloader");
|
|
744
|
+
|
|
745
|
+
const { uploads, releaseHistories } = await runRelease(staged, {
|
|
746
|
+
binaryBundlePath: baseFixture,
|
|
747
|
+
releaseHistory: releaseHistoryOf(base),
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
expect(uploads).toHaveLength(2);
|
|
751
|
+
// A release without diffs says nothing about them, so a client reading this history
|
|
752
|
+
// behaves exactly as it did before asset diffs existed.
|
|
753
|
+
expect(releaseHistories[0][APP_VERSION]).not.toHaveProperty('diffPackages');
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
it("publishes without diff archives when --diff-base-count is zero", async () => {
|
|
757
|
+
const staged = await stageBundleOutput("zero-bases", updateFiles());
|
|
758
|
+
const base = await stageBaseRelease("zero-bases");
|
|
759
|
+
const downloads: string[] = [];
|
|
760
|
+
|
|
761
|
+
const { uploads, releaseHistories } = await runRelease(staged, {
|
|
762
|
+
binaryBundlePath: baseFixture,
|
|
763
|
+
releaseHistory: releaseHistoryOf(base),
|
|
764
|
+
bundleDownloader: recordingDownloader([base], downloads),
|
|
765
|
+
diffBaseCount: 0,
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
expect(uploads).toHaveLength(2);
|
|
769
|
+
expect(downloads).toEqual([]);
|
|
770
|
+
expect(releaseHistories[0][APP_VERSION]).not.toHaveProperty('diffPackages');
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
it("builds one diff archive when two releases hold the same package", async () => {
|
|
774
|
+
const staged = await stageBundleOutput("repeated-base", updateFiles());
|
|
775
|
+
const base = await stageBaseRelease("repeated-base");
|
|
776
|
+
const downloads: string[] = [];
|
|
777
|
+
|
|
778
|
+
const { uploads, releaseHistories } = await runRelease(staged, {
|
|
779
|
+
binaryBundlePath: baseFixture,
|
|
780
|
+
releaseHistory: releaseHistoryOf(base, ['9.9.8', PREVIOUS_APP_VERSION]),
|
|
781
|
+
bundleDownloader: recordingDownloader([base], downloads),
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
expect(downloads).toHaveLength(2);
|
|
785
|
+
expect(uploads.filter(({ filePath }) => filePath.includes(ASSET_DIFF_ARCHIVE_INFIX))).toHaveLength(1);
|
|
786
|
+
expect(releaseHistories[0][APP_VERSION].diffPackages).toEqual({
|
|
787
|
+
[base.packageHash]: uploads[2].downloadUrl,
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
it("releases the diffs that uploaded when one of them is rejected", async () => {
|
|
792
|
+
const staged = await stageBundleOutput("diff-upload-failure", updateFiles());
|
|
793
|
+
// Two packages to diff against, one of whose diffs the storage backend refuses.
|
|
794
|
+
const rejected = await stageBaseRelease("diff-upload-failure-rejected");
|
|
795
|
+
const kept = await stageBaseRelease("diff-upload-failure-kept", 'an asset of the older release');
|
|
796
|
+
const downloads: string[] = [];
|
|
797
|
+
|
|
798
|
+
const { uploads, releaseHistories } = await runRelease(staged, {
|
|
799
|
+
binaryBundlePath: baseFixture,
|
|
800
|
+
releaseHistory: {
|
|
801
|
+
...releaseHistoryOf(kept, ['9.9.8']),
|
|
802
|
+
...releaseHistoryOf(rejected, [PREVIOUS_APP_VERSION]),
|
|
803
|
+
},
|
|
804
|
+
bundleDownloader: recordingDownloader([kept, rejected], downloads),
|
|
805
|
+
uploadFailsFor: (filePath) => filePath.includes(rejected.packageHash),
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
expect(uploads.map(({ filePath }) => path.basename(filePath))).toEqual([
|
|
809
|
+
staged.bundleFileName,
|
|
810
|
+
`${staged.bundleFileName}${BINARY_PATCH_ARCHIVE_SUFFIX}`,
|
|
811
|
+
`${staged.bundleFileName}${ASSET_DIFF_ARCHIVE_INFIX}${kept.packageHash}.zip`,
|
|
812
|
+
]);
|
|
813
|
+
// The release goes out with the two artifacts it needs, and describes only the diff
|
|
814
|
+
// a client can actually download.
|
|
815
|
+
expect(releaseHistories[0][APP_VERSION].diffPackages).toEqual({
|
|
816
|
+
[kept.packageHash]: uploads[2].downloadUrl,
|
|
817
|
+
});
|
|
818
|
+
expect(logs.filter((line) => line.startsWith('warn:')).join('\n')).toMatch(
|
|
819
|
+
new RegExp(`Skipping the asset diff archive against ${rejected.packageHash}`),
|
|
820
|
+
);
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
it("still picks the full bundle when diff archives sit in the output directory", async () => {
|
|
824
|
+
const staged = await stageBundleOutput("leftover-diff");
|
|
825
|
+
fs.writeFileSync(path.join(staged.bundleDirectory, assetDiffArchiveName('stale', 'stale-base')), 'stale');
|
|
826
|
+
|
|
827
|
+
const { uploads } = await runRelease(staged, { binaryBundlePath: baseFixture });
|
|
828
|
+
|
|
829
|
+
expect(path.basename(uploads[0].filePath)).toBe(staged.bundleFileName);
|
|
830
|
+
});
|
|
831
|
+
});
|
|
@@ -2,9 +2,11 @@ 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 } 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
|
+
import { ASSET_DIFF_ARCHIVE_INFIX, makeAssetDiffBundle } from "../../functions/makeAssetDiffBundle.js";
|
|
9
|
+
import { resolveAssetDiffBases } from "../../functions/resolveAssetDiffBases.js";
|
|
8
10
|
import {
|
|
9
11
|
BINARY_PATCH_ARCHIVE_SUFFIX,
|
|
10
12
|
BINARY_PATCH_BASE_RECORD_NAME,
|
|
@@ -20,6 +22,9 @@ import {
|
|
|
20
22
|
type OversizedPatchPolicy,
|
|
21
23
|
} from "../../functions/makeBinaryPatchBundle.js";
|
|
22
24
|
|
|
25
|
+
/** How many of the most recent releases a release builds asset diff archives against. */
|
|
26
|
+
export const DEFAULT_DIFF_BASE_COUNT = 3;
|
|
27
|
+
|
|
23
28
|
export async function release(
|
|
24
29
|
bundleUploader: CliConfigInterface['bundleUploader'],
|
|
25
30
|
getReleaseHistory: CliConfigInterface['getReleaseHistory'],
|
|
@@ -42,6 +47,8 @@ export async function release(
|
|
|
42
47
|
hashCalc?: boolean,
|
|
43
48
|
baseBundlePath?: string,
|
|
44
49
|
onOversizedPatch: OversizedPatchPolicy = DEFAULT_OVERSIZED_PATCH_POLICY,
|
|
50
|
+
bundleDownloader?: CliConfigInterface['bundleDownloader'],
|
|
51
|
+
diffBaseCount: number = DEFAULT_DIFF_BASE_COUNT,
|
|
45
52
|
): Promise<void> {
|
|
46
53
|
if (baseBundlePath) {
|
|
47
54
|
// Checked before the bundler runs, so the wrong base bundle costs a second rather
|
|
@@ -77,14 +84,74 @@ export async function release(
|
|
|
77
84
|
})
|
|
78
85
|
: null;
|
|
79
86
|
|
|
87
|
+
// A diff is the patch archive with what a base package already holds taken out, so it
|
|
88
|
+
// only exists where a patch does - and only a consumer that can hand back a released
|
|
89
|
+
// archive can supply the bases to diff against.
|
|
90
|
+
const assetDiffArtifacts = binaryPatch && bundleDownloader && diffBaseCount > 0
|
|
91
|
+
? await makeAssetDiffArtifacts({
|
|
92
|
+
patchBundleFilePath: binaryPatch.patchBundleFilePath,
|
|
93
|
+
releaseHistory: await getReleaseHistory(binaryVersion, platform, identifier),
|
|
94
|
+
diffBaseCount,
|
|
95
|
+
bundleDownloader,
|
|
96
|
+
bundleDirectory,
|
|
97
|
+
packageHash,
|
|
98
|
+
targetBinaryVersion: binaryVersion,
|
|
99
|
+
platform,
|
|
100
|
+
identifier,
|
|
101
|
+
})
|
|
102
|
+
: [];
|
|
103
|
+
|
|
80
104
|
// Every artifact is uploaded before the release history is touched, so a failed
|
|
81
105
|
// upload leaves the history describing only updates that can actually be downloaded.
|
|
82
|
-
const downloadUrl = await uploadArtifact(
|
|
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
|
+
);
|
|
83
118
|
let patchDownloadUrl: string | undefined;
|
|
84
119
|
if (binaryPatch) {
|
|
85
|
-
patchDownloadUrl = await uploadArtifact(
|
|
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
|
+
);
|
|
86
132
|
console.log(`log: Binary patch archive uploaded (download url: ${patchDownloadUrl})`);
|
|
87
133
|
}
|
|
134
|
+
const diffPackages: Record<string, string> = {};
|
|
135
|
+
for (const { basePackageHash, filePath } of assetDiffArtifacts) {
|
|
136
|
+
// A diff is an optimisation on top of the patch archive, which is uploaded by now:
|
|
137
|
+
// one that cannot be uploaded is left out of the history instead of failing the
|
|
138
|
+
// release the other two artifacts are ready for.
|
|
139
|
+
try {
|
|
140
|
+
const diffDownloadUrl = await uploadFile(bundleUploader, filePath, platform, identifier, {
|
|
141
|
+
type: 'asset-diff',
|
|
142
|
+
targetBinaryVersion: binaryVersion,
|
|
143
|
+
packageHash,
|
|
144
|
+
basePackageHash,
|
|
145
|
+
});
|
|
146
|
+
diffPackages[basePackageHash] = diffDownloadUrl;
|
|
147
|
+
console.log(`log: Asset diff archive against ${basePackageHash} uploaded (download url: ${diffDownloadUrl})`);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.warn(
|
|
150
|
+
`warn: Skipping the asset diff archive against ${basePackageHash}: it could not be uploaded ` +
|
|
151
|
+
`(${error instanceof Error ? error.message : String(error)}).`,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
88
155
|
|
|
89
156
|
await addToReleaseHistory(
|
|
90
157
|
appVersion,
|
|
@@ -99,6 +166,7 @@ export async function release(
|
|
|
99
166
|
mandatory,
|
|
100
167
|
enable,
|
|
101
168
|
rollout,
|
|
169
|
+
Object.keys(diffPackages).length > 0 ? diffPackages : undefined,
|
|
102
170
|
)
|
|
103
171
|
|
|
104
172
|
if (!skipCleanup) {
|
|
@@ -111,9 +179,11 @@ function cleanUpOutputs(dir: string) {
|
|
|
111
179
|
}
|
|
112
180
|
|
|
113
181
|
function readBundleFileNameFrom(bundleDirectory: string): string {
|
|
114
|
-
// A previous release of the same bundle may have left its patch
|
|
115
|
-
//
|
|
116
|
-
const files = fs.readdirSync(bundleDirectory).filter(
|
|
182
|
+
// A previous release of the same bundle may have left its patch and diff archives
|
|
183
|
+
// here, and those are derived from the bundle file rather than candidates for release.
|
|
184
|
+
const files = fs.readdirSync(bundleDirectory).filter(
|
|
185
|
+
(file) => !file.endsWith(BINARY_PATCH_ARCHIVE_SUFFIX) && !file.includes(ASSET_DIFF_ARCHIVE_INFIX),
|
|
186
|
+
);
|
|
117
187
|
if (files.length !== 1) {
|
|
118
188
|
console.error('The bundlePath must contain only one file.');
|
|
119
189
|
process.exit(1);
|
|
@@ -229,6 +299,79 @@ async function makeBinaryPatchArtifact({
|
|
|
229
299
|
}
|
|
230
300
|
}
|
|
231
301
|
|
|
302
|
+
type AssetDiffArtifact = { basePackageHash: string, filePath: string };
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Builds a diff artifact per recently released package, for the clients holding one.
|
|
306
|
+
*
|
|
307
|
+
* A base that cannot be downloaded or verified is left out by the resolver, and a diff
|
|
308
|
+
* that comes out no smaller than the patch archive is not published. Either way the
|
|
309
|
+
* release goes out with its full and patch archives, serving fewer diffs.
|
|
310
|
+
*
|
|
311
|
+
* @return {Promise<AssetDiffArtifact[]>} The diff archives to upload, each naming the package it was built against.
|
|
312
|
+
*/
|
|
313
|
+
async function makeAssetDiffArtifacts({
|
|
314
|
+
patchBundleFilePath,
|
|
315
|
+
releaseHistory,
|
|
316
|
+
diffBaseCount,
|
|
317
|
+
bundleDownloader,
|
|
318
|
+
bundleDirectory,
|
|
319
|
+
packageHash,
|
|
320
|
+
targetBinaryVersion,
|
|
321
|
+
platform,
|
|
322
|
+
identifier,
|
|
323
|
+
}: {
|
|
324
|
+
patchBundleFilePath: string;
|
|
325
|
+
releaseHistory: ReleaseHistoryInterface;
|
|
326
|
+
diffBaseCount: number;
|
|
327
|
+
bundleDownloader: NonNullable<CliConfigInterface['bundleDownloader']>;
|
|
328
|
+
bundleDirectory: string;
|
|
329
|
+
packageHash: string;
|
|
330
|
+
targetBinaryVersion: string;
|
|
331
|
+
platform: 'ios' | 'android';
|
|
332
|
+
identifier: string | undefined;
|
|
333
|
+
}): Promise<AssetDiffArtifact[]> {
|
|
334
|
+
const bases = await resolveAssetDiffBases({
|
|
335
|
+
releaseHistory,
|
|
336
|
+
diffBaseCount,
|
|
337
|
+
bundleDownloader,
|
|
338
|
+
platform,
|
|
339
|
+
identifier,
|
|
340
|
+
targetBinaryVersion,
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const artifacts: AssetDiffArtifact[] = [];
|
|
344
|
+
const builtAgainst = new Set<string>();
|
|
345
|
+
for (const base of bases) {
|
|
346
|
+
// Releasing identical contents twice leaves two history entries describing one
|
|
347
|
+
// package, and their diff is the same archive built - and named - twice.
|
|
348
|
+
if (builtAgainst.has(base.basePackageHash)) {
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
builtAgainst.add(base.basePackageHash);
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const assetDiff = await makeAssetDiffBundle({
|
|
355
|
+
patchBundleFilePath,
|
|
356
|
+
baseBundleFilePath: base.baseBundleFilePath,
|
|
357
|
+
bundleDirectory,
|
|
358
|
+
packageHash,
|
|
359
|
+
basePackageHash: base.basePackageHash,
|
|
360
|
+
});
|
|
361
|
+
if (assetDiff) {
|
|
362
|
+
artifacts.push({ basePackageHash: base.basePackageHash, filePath: assetDiff.diffBundleFilePath });
|
|
363
|
+
}
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.warn(
|
|
366
|
+
`warn: Skipping the asset diff against ${base.basePackageHash}: it could not be built ` +
|
|
367
|
+
`(${error instanceof Error ? error.message : String(error)}).`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return artifacts;
|
|
373
|
+
}
|
|
374
|
+
|
|
232
375
|
/**
|
|
233
376
|
* Cross-checks the base bundle against the record a build hook exported next to it.
|
|
234
377
|
*
|
|
@@ -303,18 +446,30 @@ function warnOnBaseBundleMismatch(outputPath: string, baseBundlePath: string): v
|
|
|
303
446
|
}
|
|
304
447
|
}
|
|
305
448
|
|
|
449
|
+
/** Uploads an artifact the release cannot go out without, so a failure ends the program. */
|
|
306
450
|
async function uploadArtifact(
|
|
307
451
|
bundleUploader: CliConfigInterface['bundleUploader'],
|
|
308
452
|
filePath: string,
|
|
309
453
|
platform: 'ios' | 'android',
|
|
310
454
|
identifier: string | undefined,
|
|
455
|
+
artifact: BundleUploadArtifact,
|
|
311
456
|
artifactName: string,
|
|
312
457
|
): Promise<string> {
|
|
313
458
|
try {
|
|
314
|
-
|
|
315
|
-
return downloadUrl
|
|
459
|
+
return await uploadFile(bundleUploader, filePath, platform, identifier, artifact);
|
|
316
460
|
} catch (error) {
|
|
317
461
|
console.error(`Failed to upload the ${artifactName} file. Exiting the program.\n`, error)
|
|
318
462
|
process.exit(1)
|
|
319
463
|
}
|
|
320
464
|
}
|
|
465
|
+
|
|
466
|
+
async function uploadFile(
|
|
467
|
+
bundleUploader: CliConfigInterface['bundleUploader'],
|
|
468
|
+
filePath: string,
|
|
469
|
+
platform: 'ios' | 'android',
|
|
470
|
+
identifier: string | undefined,
|
|
471
|
+
artifact: BundleUploadArtifact,
|
|
472
|
+
): Promise<string> {
|
|
473
|
+
const { downloadUrl } = await bundleUploader(filePath, platform, identifier, artifact);
|
|
474
|
+
return downloadUrl
|
|
475
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
|
-
export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownloadUrl, binaryPatchDownloadUrl, packageHash, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout) {
|
|
3
|
+
export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownloadUrl, binaryPatchDownloadUrl, packageHash, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout, diffPackages) {
|
|
4
4
|
const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
|
|
5
5
|
const updateInfo = releaseHistory[appVersion];
|
|
6
6
|
if (updateInfo) {
|
|
@@ -19,6 +19,11 @@ export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownl
|
|
|
19
19
|
if (binaryPatchDownloadUrl) {
|
|
20
20
|
newReleaseHistory[appVersion].binaryPatchDownloadUrl = binaryPatchDownloadUrl;
|
|
21
21
|
}
|
|
22
|
+
// Same for the asset diffs: an entry only names the bases it actually published a
|
|
23
|
+
// diff archive for.
|
|
24
|
+
if (diffPackages && Object.keys(diffPackages).length > 0) {
|
|
25
|
+
newReleaseHistory[appVersion].diffPackages = diffPackages;
|
|
26
|
+
}
|
|
22
27
|
if (typeof rollout === 'number') {
|
|
23
28
|
newReleaseHistory[appVersion].rollout = rollout;
|
|
24
29
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { program, Option } from "commander";
|
|
2
2
|
import { findAndReadConfigFile } from "../../utils/fsUtils.js";
|
|
3
|
-
import { release } from "./release.js";
|
|
3
|
+
import { DEFAULT_DIFF_BASE_COUNT, release } from "./release.js";
|
|
4
4
|
import { DEFAULT_OVERSIZED_PATCH_POLICY, OVERSIZED_PATCH_POLICIES, resolveBinaryBundlePathOption, } from "../../functions/makeBinaryPatchBundle.js";
|
|
5
5
|
import { OUTPUT_BUNDLE_DIR, CONFIG_FILE_NAME, ROOT_OUTPUT_DIR, ENTRY_FILE } from "../../constant.js";
|
|
6
6
|
program.command('release')
|
|
@@ -26,21 +26,31 @@ program.command('release')
|
|
|
26
26
|
.addOption(new Option('--on-oversized-patch <policy>', 'what to do when the binary patch bundle is not smaller than the full bundle: "skip" releases the full bundle only, "fail" stops the release before any upload')
|
|
27
27
|
.choices(OVERSIZED_PATCH_POLICIES)
|
|
28
28
|
.default(DEFAULT_OVERSIZED_PATCH_POLICY))
|
|
29
|
+
.option('--diff-base-count <number>', 'how many recent releases to build asset diff archives against (0 disables). Requires `bundleDownloader` in the config file.', parseDecimalInt, DEFAULT_DIFF_BASE_COUNT)
|
|
29
30
|
.action(async (options) => {
|
|
30
31
|
const config = findAndReadConfigFile(process.cwd(), options.config);
|
|
31
32
|
if (typeof options.rollout === 'number' && (options.rollout < 0 || options.rollout > 100)) {
|
|
32
33
|
console.error('Rollout percentage number must be between 0 and 100 (inclusive).');
|
|
33
34
|
process.exit(1);
|
|
34
35
|
}
|
|
36
|
+
if (!Number.isInteger(options.diffBaseCount) || options.diffBaseCount < 0) {
|
|
37
|
+
console.error('--diff-base-count must be a whole number of releases, 0 or greater.');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
35
40
|
if (options.hashCalc && !options.skipBundle) {
|
|
36
41
|
console.error('--hash-calc option can be used only when --skip-bundle is set to true.');
|
|
37
42
|
process.exit(1);
|
|
38
43
|
}
|
|
39
44
|
// Resolved before bundling so a wrong path fails now rather than after a build.
|
|
40
45
|
const baseBundlePath = resolveBinaryBundlePathOption(options.binaryBundlePath);
|
|
41
|
-
await release(config.bundleUploader, config.getReleaseHistory, config.setReleaseHistory, options.binaryVersion, options.appVersion, options.framework, options.platform, options.identifier, options.outputPath, options.entryFile, options.jsBundleName, options.mandatory, options.enable, options.rollout, options.skipBundle, options.skipCleanup, `${options.outputPath}/${options.outputBundleDir}`, options.outputMetroDir, options.hashCalc, baseBundlePath, options.onOversizedPatch);
|
|
46
|
+
await release(config.bundleUploader, config.getReleaseHistory, config.setReleaseHistory, options.binaryVersion, options.appVersion, options.framework, options.platform, options.identifier, options.outputPath, options.entryFile, options.jsBundleName, options.mandatory, options.enable, options.rollout, options.skipBundle, options.skipCleanup, `${options.outputPath}/${options.outputBundleDir}`, options.outputMetroDir, options.hashCalc, baseBundlePath, options.onOversizedPatch, config.bundleDownloader, options.diffBaseCount);
|
|
42
47
|
console.log('🚀 Release completed.');
|
|
43
48
|
});
|
|
49
|
+
// Not `parseInt` itself: commander hands a coercion function the current value as its
|
|
50
|
+
// second argument, which `parseInt` reads as the radix.
|
|
51
|
+
function parseDecimalInt(value) {
|
|
52
|
+
return parseInt(value, 10);
|
|
53
|
+
}
|
|
44
54
|
function parseBoolean(value) {
|
|
45
55
|
if (value === 'true')
|
|
46
56
|
return true;
|