@bravemobile/react-native-code-push 13.1.0-beta.2 → 13.1.0-beta.3
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 +43 -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 +212 -5
- package/cli/commands/releaseCommand/release.ts +128 -6
- 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 +90 -7
- package/cli/dist/commands/releaseCommand/release.test.js +175 -5
- package/cli/dist/functions/makeAssetDiffBundle.js +114 -0
- package/cli/dist/functions/makeAssetDiffBundle.test.js +215 -0
- package/cli/dist/functions/resolveAssetDiffBases.js +71 -0
- package/cli/dist/functions/resolveAssetDiffBases.test.js +174 -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 +220 -0
- package/cli/functions/resolveAssetDiffBases.ts +101 -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 +19 -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 { 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
|
|
@@ -103,16 +105,17 @@ function recordingUploader(uploads: Uploads, failOn?: (filePath: string) => bool
|
|
|
103
105
|
};
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
function historyStore() {
|
|
108
|
+
function historyStore(existingHistory: ReleaseHistoryInterface = {}, onSave?: () => void) {
|
|
107
109
|
const saved: ReleaseHistoryInterface[] = [];
|
|
108
110
|
return {
|
|
109
111
|
saved,
|
|
110
|
-
getReleaseHistory: async () =>
|
|
112
|
+
getReleaseHistory: async () => existingHistory,
|
|
111
113
|
setReleaseHistory: async (
|
|
112
114
|
_binaryVersion: string,
|
|
113
115
|
_jsonFilePath: string,
|
|
114
116
|
releaseInfo: ReleaseHistoryInterface,
|
|
115
117
|
) => {
|
|
118
|
+
onSave?.();
|
|
116
119
|
saved.push(releaseInfo);
|
|
117
120
|
},
|
|
118
121
|
};
|
|
@@ -127,11 +130,18 @@ type ReleaseOverrides = {
|
|
|
127
130
|
onOversizedPatch?: OversizedPatchPolicy;
|
|
128
131
|
/** Passed in when the case has to read the uploads of a release that threw. */
|
|
129
132
|
uploads?: Uploads;
|
|
133
|
+
/** What the consumer already has released, which the asset diff bases are picked from. */
|
|
134
|
+
releaseHistory?: ReleaseHistoryInterface;
|
|
135
|
+
bundleDownloader?: CliConfigInterface['bundleDownloader'];
|
|
136
|
+
diffBaseCount?: number;
|
|
130
137
|
};
|
|
131
138
|
|
|
132
139
|
async function runRelease(staged: StagedBundle, overrides: ReleaseOverrides = {}) {
|
|
133
140
|
const uploads: Uploads = overrides.uploads ?? [];
|
|
134
|
-
const
|
|
141
|
+
const uploadCountsWhenHistorySaved: number[] = [];
|
|
142
|
+
const history = historyStore(overrides.releaseHistory, () => {
|
|
143
|
+
uploadCountsWhenHistorySaved.push(uploads.length);
|
|
144
|
+
});
|
|
135
145
|
|
|
136
146
|
await release(
|
|
137
147
|
recordingUploader(uploads, overrides.uploadFailsFor),
|
|
@@ -156,9 +166,11 @@ async function runRelease(staged: StagedBundle, overrides: ReleaseOverrides = {}
|
|
|
156
166
|
undefined,
|
|
157
167
|
overrides.binaryBundlePath,
|
|
158
168
|
overrides.onOversizedPatch,
|
|
169
|
+
overrides.bundleDownloader,
|
|
170
|
+
overrides.diffBaseCount,
|
|
159
171
|
);
|
|
160
172
|
|
|
161
|
-
return { uploads, releaseHistories: history.saved };
|
|
173
|
+
return { uploads, releaseHistories: history.saved, uploadCountsWhenHistorySaved };
|
|
162
174
|
}
|
|
163
175
|
|
|
164
176
|
let logs: string[];
|
|
@@ -580,3 +592,198 @@ describe("release --on-oversized-patch", () => {
|
|
|
580
592
|
expect(uploads).toHaveLength(2);
|
|
581
593
|
});
|
|
582
594
|
});
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* A release can also ship a diff per recently released package: the patch archive without
|
|
598
|
+
* the files that package already holds. The bases come out of the release history and are
|
|
599
|
+
* downloaded through the consumer's `bundleDownloader`, so a release only builds diffs
|
|
600
|
+
* where the consumer supplied one.
|
|
601
|
+
*/
|
|
602
|
+
describe("release with asset diff bases", () => {
|
|
603
|
+
/**
|
|
604
|
+
* The asset both packages hold. Sized like a real asset because a diff is only shipped
|
|
605
|
+
* when it comes out smaller than the patch archive it was derived from.
|
|
606
|
+
*/
|
|
607
|
+
const sharedAsset = crypto.randomBytes(4096);
|
|
608
|
+
|
|
609
|
+
const PREVIOUS_APP_VERSION = '9.9.9';
|
|
610
|
+
|
|
611
|
+
function updateFiles(): Record<string, Buffer | string> {
|
|
612
|
+
return { 'main.jsbundle': fs.readFileSync(targetFixture), 'assets/keep.png': sharedAsset };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
type StagedBaseRelease = { downloadUrl: string, packageHash: string, archivePath: string };
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* A package released earlier, as the history describes it and as the CDN serves it.
|
|
619
|
+
*
|
|
620
|
+
* @param ownAsset An asset only this release holds, which is what makes two staged base releases two packages rather than one.
|
|
621
|
+
*/
|
|
622
|
+
async function stageBaseRelease(caseName: string, ownAsset?: string): Promise<StagedBaseRelease> {
|
|
623
|
+
const caseDir = fs.mkdtempSync(path.join(workDir, `${caseName}-base-`));
|
|
624
|
+
const contentsPath = path.join(caseDir, CONTENTS_DIR_NAME);
|
|
625
|
+
const files: Record<string, Buffer | string> = {
|
|
626
|
+
'main.jsbundle': fs.readFileSync(baseFixture),
|
|
627
|
+
'assets/keep.png': sharedAsset,
|
|
628
|
+
...(ownAsset === undefined ? {} : { 'assets/dropped.png': ownAsset }),
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
632
|
+
const filePath = path.join(contentsPath, relativePath);
|
|
633
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
634
|
+
fs.writeFileSync(filePath, content);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const archiveDir = path.join(caseDir, 'archive');
|
|
638
|
+
const { bundleFileName } = await makeCodePushBundle(contentsPath, archiveDir);
|
|
639
|
+
|
|
640
|
+
return {
|
|
641
|
+
downloadUrl: `https://cdn.example.com/released/${bundleFileName}`,
|
|
642
|
+
packageHash: bundleFileName,
|
|
643
|
+
archivePath: path.join(archiveDir, bundleFileName),
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function releaseHistoryOf(base: StagedBaseRelease, versions = [PREVIOUS_APP_VERSION]): ReleaseHistoryInterface {
|
|
648
|
+
return Object.fromEntries(versions.map((version) => [
|
|
649
|
+
version,
|
|
650
|
+
{
|
|
651
|
+
enabled: true,
|
|
652
|
+
mandatory: false,
|
|
653
|
+
downloadUrl: base.downloadUrl,
|
|
654
|
+
packageHash: base.packageHash,
|
|
655
|
+
},
|
|
656
|
+
]));
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** Serves the staged base archives, and records which releases were asked for. */
|
|
660
|
+
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);
|
|
664
|
+
if (!base) {
|
|
665
|
+
throw new Error(`the downloader was called with an unexpected url: ${downloadUrl}`);
|
|
666
|
+
}
|
|
667
|
+
return { downloadedFilePath: base.archivePath };
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
it("publishes asset diff archives against recent releases and records them in the history", async () => {
|
|
672
|
+
const staged = await stageBundleOutput("asset-diff", updateFiles());
|
|
673
|
+
const base = await stageBaseRelease("asset-diff");
|
|
674
|
+
const downloads: string[] = [];
|
|
675
|
+
|
|
676
|
+
const { uploads, releaseHistories, uploadCountsWhenHistorySaved } = await runRelease(staged, {
|
|
677
|
+
binaryBundlePath: baseFixture,
|
|
678
|
+
releaseHistory: releaseHistoryOf(base),
|
|
679
|
+
bundleDownloader: recordingDownloader([base], downloads),
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const diffFileName = `${staged.bundleFileName}${ASSET_DIFF_ARCHIVE_INFIX}${base.packageHash}.zip`;
|
|
683
|
+
// The diff is the last artifact: it is an optimisation on top of the patch, which
|
|
684
|
+
// is itself an optimisation on top of the full archive.
|
|
685
|
+
expect(uploads.map(({ filePath }) => path.basename(filePath))).toEqual([
|
|
686
|
+
staged.bundleFileName,
|
|
687
|
+
`${staged.bundleFileName}${BINARY_PATCH_ARCHIVE_SUFFIX}`,
|
|
688
|
+
diffFileName,
|
|
689
|
+
]);
|
|
690
|
+
expect(downloads).toEqual([base.downloadUrl]);
|
|
691
|
+
// Every artifact is uploaded before the history points at any of them.
|
|
692
|
+
expect(uploadCountsWhenHistorySaved).toEqual([3]);
|
|
693
|
+
expect(releaseHistories[0][APP_VERSION].diffPackages).toEqual({
|
|
694
|
+
[base.packageHash]: uploads[2].downloadUrl,
|
|
695
|
+
});
|
|
696
|
+
expect(fs.existsSync(path.join(staged.bundleDirectory, diffFileName))).toBe(true);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
it("publishes without diff archives when the config has no bundle downloader", async () => {
|
|
700
|
+
const staged = await stageBundleOutput("no-downloader", updateFiles());
|
|
701
|
+
const base = await stageBaseRelease("no-downloader");
|
|
702
|
+
|
|
703
|
+
const { uploads, releaseHistories } = await runRelease(staged, {
|
|
704
|
+
binaryBundlePath: baseFixture,
|
|
705
|
+
releaseHistory: releaseHistoryOf(base),
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
expect(uploads).toHaveLength(2);
|
|
709
|
+
// A release without diffs says nothing about them, so a client reading this history
|
|
710
|
+
// behaves exactly as it did before asset diffs existed.
|
|
711
|
+
expect(releaseHistories[0][APP_VERSION]).not.toHaveProperty('diffPackages');
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
it("publishes without diff archives when --diff-base-count is zero", async () => {
|
|
715
|
+
const staged = await stageBundleOutput("zero-bases", updateFiles());
|
|
716
|
+
const base = await stageBaseRelease("zero-bases");
|
|
717
|
+
const downloads: string[] = [];
|
|
718
|
+
|
|
719
|
+
const { uploads, releaseHistories } = await runRelease(staged, {
|
|
720
|
+
binaryBundlePath: baseFixture,
|
|
721
|
+
releaseHistory: releaseHistoryOf(base),
|
|
722
|
+
bundleDownloader: recordingDownloader([base], downloads),
|
|
723
|
+
diffBaseCount: 0,
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
expect(uploads).toHaveLength(2);
|
|
727
|
+
expect(downloads).toEqual([]);
|
|
728
|
+
expect(releaseHistories[0][APP_VERSION]).not.toHaveProperty('diffPackages');
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it("builds one diff archive when two releases hold the same package", async () => {
|
|
732
|
+
const staged = await stageBundleOutput("repeated-base", updateFiles());
|
|
733
|
+
const base = await stageBaseRelease("repeated-base");
|
|
734
|
+
const downloads: string[] = [];
|
|
735
|
+
|
|
736
|
+
const { uploads, releaseHistories } = await runRelease(staged, {
|
|
737
|
+
binaryBundlePath: baseFixture,
|
|
738
|
+
releaseHistory: releaseHistoryOf(base, ['9.9.8', PREVIOUS_APP_VERSION]),
|
|
739
|
+
bundleDownloader: recordingDownloader([base], downloads),
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
expect(downloads).toHaveLength(2);
|
|
743
|
+
expect(uploads.filter(({ filePath }) => filePath.includes(ASSET_DIFF_ARCHIVE_INFIX))).toHaveLength(1);
|
|
744
|
+
expect(releaseHistories[0][APP_VERSION].diffPackages).toEqual({
|
|
745
|
+
[base.packageHash]: uploads[2].downloadUrl,
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it("releases the diffs that uploaded when one of them is rejected", async () => {
|
|
750
|
+
const staged = await stageBundleOutput("diff-upload-failure", updateFiles());
|
|
751
|
+
// Two packages to diff against, one of whose diffs the storage backend refuses.
|
|
752
|
+
const rejected = await stageBaseRelease("diff-upload-failure-rejected");
|
|
753
|
+
const kept = await stageBaseRelease("diff-upload-failure-kept", 'an asset of the older release');
|
|
754
|
+
const downloads: string[] = [];
|
|
755
|
+
|
|
756
|
+
const { uploads, releaseHistories } = await runRelease(staged, {
|
|
757
|
+
binaryBundlePath: baseFixture,
|
|
758
|
+
releaseHistory: {
|
|
759
|
+
...releaseHistoryOf(kept, ['9.9.8']),
|
|
760
|
+
...releaseHistoryOf(rejected, [PREVIOUS_APP_VERSION]),
|
|
761
|
+
},
|
|
762
|
+
bundleDownloader: recordingDownloader([kept, rejected], downloads),
|
|
763
|
+
uploadFailsFor: (filePath) => filePath.includes(rejected.packageHash),
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
expect(uploads.map(({ filePath }) => path.basename(filePath))).toEqual([
|
|
767
|
+
staged.bundleFileName,
|
|
768
|
+
`${staged.bundleFileName}${BINARY_PATCH_ARCHIVE_SUFFIX}`,
|
|
769
|
+
`${staged.bundleFileName}${ASSET_DIFF_ARCHIVE_INFIX}${kept.packageHash}.zip`,
|
|
770
|
+
]);
|
|
771
|
+
// The release goes out with the two artifacts it needs, and describes only the diff
|
|
772
|
+
// a client can actually download.
|
|
773
|
+
expect(releaseHistories[0][APP_VERSION].diffPackages).toEqual({
|
|
774
|
+
[kept.packageHash]: uploads[2].downloadUrl,
|
|
775
|
+
});
|
|
776
|
+
expect(logs.filter((line) => line.startsWith('warn:')).join('\n')).toMatch(
|
|
777
|
+
new RegExp(`Skipping the asset diff archive against ${rejected.packageHash}`),
|
|
778
|
+
);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it("still picks the full bundle when diff archives sit in the output directory", async () => {
|
|
782
|
+
const staged = await stageBundleOutput("leftover-diff");
|
|
783
|
+
fs.writeFileSync(path.join(staged.bundleDirectory, assetDiffArchiveName('stale', 'stale-base')), 'stale');
|
|
784
|
+
|
|
785
|
+
const { uploads } = await runRelease(staged, { binaryBundlePath: baseFixture });
|
|
786
|
+
|
|
787
|
+
expect(path.basename(uploads[0].filePath)).toBe(staged.bundleFileName);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
@@ -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 { 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,6 +84,22 @@ 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
|
+
platform,
|
|
99
|
+
identifier,
|
|
100
|
+
})
|
|
101
|
+
: [];
|
|
102
|
+
|
|
80
103
|
// Every artifact is uploaded before the release history is touched, so a failed
|
|
81
104
|
// upload leaves the history describing only updates that can actually be downloaded.
|
|
82
105
|
const downloadUrl = await uploadArtifact(bundleUploader, bundleFilePath, platform, identifier, 'bundle');
|
|
@@ -85,6 +108,22 @@ export async function release(
|
|
|
85
108
|
patchDownloadUrl = await uploadArtifact(bundleUploader, binaryPatch.patchBundleFilePath, platform, identifier, 'binary patch bundle');
|
|
86
109
|
console.log(`log: Binary patch archive uploaded (download url: ${patchDownloadUrl})`);
|
|
87
110
|
}
|
|
111
|
+
const diffPackages: Record<string, string> = {};
|
|
112
|
+
for (const { basePackageHash, filePath } of assetDiffArtifacts) {
|
|
113
|
+
// A diff is an optimisation on top of the patch archive, which is uploaded by now:
|
|
114
|
+
// one that cannot be uploaded is left out of the history instead of failing the
|
|
115
|
+
// release the other two artifacts are ready for.
|
|
116
|
+
try {
|
|
117
|
+
const diffDownloadUrl = await uploadFile(bundleUploader, filePath, platform, identifier);
|
|
118
|
+
diffPackages[basePackageHash] = diffDownloadUrl;
|
|
119
|
+
console.log(`log: Asset diff archive against ${basePackageHash} uploaded (download url: ${diffDownloadUrl})`);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.warn(
|
|
122
|
+
`warn: Skipping the asset diff archive against ${basePackageHash}: it could not be uploaded ` +
|
|
123
|
+
`(${error instanceof Error ? error.message : String(error)}).`,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
88
127
|
|
|
89
128
|
await addToReleaseHistory(
|
|
90
129
|
appVersion,
|
|
@@ -99,6 +138,7 @@ export async function release(
|
|
|
99
138
|
mandatory,
|
|
100
139
|
enable,
|
|
101
140
|
rollout,
|
|
141
|
+
Object.keys(diffPackages).length > 0 ? diffPackages : undefined,
|
|
102
142
|
)
|
|
103
143
|
|
|
104
144
|
if (!skipCleanup) {
|
|
@@ -111,9 +151,11 @@ function cleanUpOutputs(dir: string) {
|
|
|
111
151
|
}
|
|
112
152
|
|
|
113
153
|
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(
|
|
154
|
+
// A previous release of the same bundle may have left its patch and diff archives
|
|
155
|
+
// here, and those are derived from the bundle file rather than candidates for release.
|
|
156
|
+
const files = fs.readdirSync(bundleDirectory).filter(
|
|
157
|
+
(file) => !file.endsWith(BINARY_PATCH_ARCHIVE_SUFFIX) && !file.includes(ASSET_DIFF_ARCHIVE_INFIX),
|
|
158
|
+
);
|
|
117
159
|
if (files.length !== 1) {
|
|
118
160
|
console.error('The bundlePath must contain only one file.');
|
|
119
161
|
process.exit(1);
|
|
@@ -229,6 +271,76 @@ async function makeBinaryPatchArtifact({
|
|
|
229
271
|
}
|
|
230
272
|
}
|
|
231
273
|
|
|
274
|
+
type AssetDiffArtifact = { basePackageHash: string, filePath: string };
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Builds a diff artifact per recently released package, for the clients holding one.
|
|
278
|
+
*
|
|
279
|
+
* A base that cannot be downloaded or verified is left out by the resolver, and a diff
|
|
280
|
+
* that comes out no smaller than the patch archive is not published. Either way the
|
|
281
|
+
* release goes out with its full and patch archives, serving fewer diffs.
|
|
282
|
+
*
|
|
283
|
+
* @return {Promise<AssetDiffArtifact[]>} The diff archives to upload, each naming the package it was built against.
|
|
284
|
+
*/
|
|
285
|
+
async function makeAssetDiffArtifacts({
|
|
286
|
+
patchBundleFilePath,
|
|
287
|
+
releaseHistory,
|
|
288
|
+
diffBaseCount,
|
|
289
|
+
bundleDownloader,
|
|
290
|
+
bundleDirectory,
|
|
291
|
+
packageHash,
|
|
292
|
+
platform,
|
|
293
|
+
identifier,
|
|
294
|
+
}: {
|
|
295
|
+
patchBundleFilePath: string;
|
|
296
|
+
releaseHistory: ReleaseHistoryInterface;
|
|
297
|
+
diffBaseCount: number;
|
|
298
|
+
bundleDownloader: NonNullable<CliConfigInterface['bundleDownloader']>;
|
|
299
|
+
bundleDirectory: string;
|
|
300
|
+
packageHash: string;
|
|
301
|
+
platform: 'ios' | 'android';
|
|
302
|
+
identifier: string | undefined;
|
|
303
|
+
}): Promise<AssetDiffArtifact[]> {
|
|
304
|
+
const bases = await resolveAssetDiffBases({
|
|
305
|
+
releaseHistory,
|
|
306
|
+
diffBaseCount,
|
|
307
|
+
bundleDownloader,
|
|
308
|
+
platform,
|
|
309
|
+
identifier,
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
const artifacts: AssetDiffArtifact[] = [];
|
|
313
|
+
const builtAgainst = new Set<string>();
|
|
314
|
+
for (const base of bases) {
|
|
315
|
+
// Releasing identical contents twice leaves two history entries describing one
|
|
316
|
+
// package, and their diff is the same archive built - and named - twice.
|
|
317
|
+
if (builtAgainst.has(base.basePackageHash)) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
builtAgainst.add(base.basePackageHash);
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const assetDiff = await makeAssetDiffBundle({
|
|
324
|
+
patchBundleFilePath,
|
|
325
|
+
baseBundleFilePath: base.baseBundleFilePath,
|
|
326
|
+
bundleDirectory,
|
|
327
|
+
packageHash,
|
|
328
|
+
basePackageHash: base.basePackageHash,
|
|
329
|
+
});
|
|
330
|
+
if (assetDiff) {
|
|
331
|
+
artifacts.push({ basePackageHash: base.basePackageHash, filePath: assetDiff.diffBundleFilePath });
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
console.warn(
|
|
335
|
+
`warn: Skipping the asset diff against ${base.basePackageHash}: it could not be built ` +
|
|
336
|
+
`(${error instanceof Error ? error.message : String(error)}).`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return artifacts;
|
|
342
|
+
}
|
|
343
|
+
|
|
232
344
|
/**
|
|
233
345
|
* Cross-checks the base bundle against the record a build hook exported next to it.
|
|
234
346
|
*
|
|
@@ -303,6 +415,7 @@ function warnOnBaseBundleMismatch(outputPath: string, baseBundlePath: string): v
|
|
|
303
415
|
}
|
|
304
416
|
}
|
|
305
417
|
|
|
418
|
+
/** Uploads an artifact the release cannot go out without, so a failure ends the program. */
|
|
306
419
|
async function uploadArtifact(
|
|
307
420
|
bundleUploader: CliConfigInterface['bundleUploader'],
|
|
308
421
|
filePath: string,
|
|
@@ -311,10 +424,19 @@ async function uploadArtifact(
|
|
|
311
424
|
artifactName: string,
|
|
312
425
|
): Promise<string> {
|
|
313
426
|
try {
|
|
314
|
-
|
|
315
|
-
return downloadUrl
|
|
427
|
+
return await uploadFile(bundleUploader, filePath, platform, identifier);
|
|
316
428
|
} catch (error) {
|
|
317
429
|
console.error(`Failed to upload the ${artifactName} file. Exiting the program.\n`, error)
|
|
318
430
|
process.exit(1)
|
|
319
431
|
}
|
|
320
432
|
}
|
|
433
|
+
|
|
434
|
+
async function uploadFile(
|
|
435
|
+
bundleUploader: CliConfigInterface['bundleUploader'],
|
|
436
|
+
filePath: string,
|
|
437
|
+
platform: 'ios' | 'android',
|
|
438
|
+
identifier: string | undefined,
|
|
439
|
+
): Promise<string> {
|
|
440
|
+
const { downloadUrl } = await bundleUploader(filePath, platform, identifier);
|
|
441
|
+
return downloadUrl
|
|
442
|
+
}
|
|
@@ -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;
|
|
@@ -10,6 +10,7 @@ jest.mock("./release.js");
|
|
|
10
10
|
jest.mock("../../utils/fsUtils.js", () => ({
|
|
11
11
|
findAndReadConfigFile: () => ({
|
|
12
12
|
bundleUploader: async () => ({ downloadUrl: 'https://cdn.example.com/bundle' }),
|
|
13
|
+
bundleDownloader: async () => ({ downloadedFilePath: '/tmp/base.zip' }),
|
|
13
14
|
getReleaseHistory: async () => ({}),
|
|
14
15
|
setReleaseHistory: async () => undefined,
|
|
15
16
|
}),
|
|
@@ -26,6 +27,8 @@ const ARG_INDEX = {
|
|
|
26
27
|
bundleDirectory: 16,
|
|
27
28
|
baseBundlePath: 19,
|
|
28
29
|
onOversizedPatch: 20,
|
|
30
|
+
bundleDownloader: 21,
|
|
31
|
+
diffBaseCount: 22,
|
|
29
32
|
};
|
|
30
33
|
/**
|
|
31
34
|
* Parses a `release` invocation against the real command definition. Commander is asked
|
|
@@ -93,4 +96,26 @@ describe("release command options", () => {
|
|
|
93
96
|
const { release } = await import("./release.js");
|
|
94
97
|
expect(jest.mocked(release)).not.toHaveBeenCalled();
|
|
95
98
|
});
|
|
99
|
+
it("hands the release the bundle downloader from the config and three diff bases by default", async () => {
|
|
100
|
+
const args = await runReleaseCommand(['-b', '1.0.0', '-v', '1.0.1']);
|
|
101
|
+
expect(typeof args[ARG_INDEX.bundleDownloader]).toBe('function');
|
|
102
|
+
expect(args[ARG_INDEX.diffBaseCount]).toBe(3);
|
|
103
|
+
});
|
|
104
|
+
it("passes the chosen asset diff base count through to the release", async () => {
|
|
105
|
+
const args = await runReleaseCommand(['-b', '1.0.0', '-v', '1.0.1', '--diff-base-count', '5']);
|
|
106
|
+
expect(args[ARG_INDEX.diffBaseCount]).toBe(5);
|
|
107
|
+
});
|
|
108
|
+
it.each([
|
|
109
|
+
['is negative', '-1'],
|
|
110
|
+
['is not a number at all', 'many'],
|
|
111
|
+
])("rejects an asset diff base count that %s", async (_caseName, value) => {
|
|
112
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
113
|
+
jest.spyOn(process, 'exit').mockImplementation(((code) => {
|
|
114
|
+
throw new Error(`process.exit(${code})`);
|
|
115
|
+
}));
|
|
116
|
+
await expect(parseReleaseCommand(['-b', '1.0.0', '-v', '1.0.1', '--diff-base-count', value]))
|
|
117
|
+
.rejects.toThrow('process.exit(1)');
|
|
118
|
+
const { release } = await import("./release.js");
|
|
119
|
+
expect(jest.mocked(release)).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
96
121
|
});
|