@bravemobile/react-native-code-push 13.2.0 → 13.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/cli/commands/createHistoryCommand/createReleaseHistory.test.ts +2 -2
- package/cli/commands/releaseCommand/addToReleaseHistory.ts +6 -0
- package/cli/commands/releaseCommand/index.test.ts +29 -0
- package/cli/commands/releaseCommand/index.ts +9 -0
- package/cli/commands/releaseCommand/release.test.ts +28 -0
- package/cli/commands/releaseCommand/release.ts +2 -0
- package/cli/commands/updateHistoryCommand/index.test.ts +129 -0
- package/cli/commands/updateHistoryCommand/index.ts +28 -2
- package/cli/commands/updateHistoryCommand/updateReleaseHistory.ts +3 -0
- package/cli/dist/commands/createHistoryCommand/createReleaseHistory.test.js +2 -2
- package/cli/dist/commands/releaseCommand/addToReleaseHistory.js +5 -1
- package/cli/dist/commands/releaseCommand/index.js +7 -1
- package/cli/dist/commands/releaseCommand/index.test.js +22 -0
- package/cli/dist/commands/releaseCommand/release.js +2 -2
- package/cli/dist/commands/releaseCommand/release.test.js +18 -1
- package/cli/dist/commands/updateHistoryCommand/index.js +23 -2
- package/cli/dist/commands/updateHistoryCommand/index.test.js +102 -0
- package/cli/dist/commands/updateHistoryCommand/updateReleaseHistory.js +4 -1
- package/package.json +1 -1
- package/src/CodePush.js +16 -3
- package/src/CodePush.test.js +57 -0
- package/typings/react-native-code-push.d.ts +20 -0
package/README.md
CHANGED
|
@@ -501,6 +501,8 @@ npx code-push release --framework expo --binary-version 1.0.0 --app-version 1.0.
|
|
|
501
501
|
> `--app-version` should be greater than `--binary-version` (SemVer comparison).
|
|
502
502
|
|
|
503
503
|
- `--rollout`: The rollout percentage for the update. (0~100, inclusive)
|
|
504
|
+
- `--minimum-background-duration`: The number of seconds the app must have been in the background before this update is applied on resume, for `ON_NEXT_RESUME` and `ON_NEXT_SUSPEND` installs only. (whole seconds, 0 or greater)
|
|
505
|
+
- The value set on the release takes precedence over the `minimumBackgroundDuration` passed to `sync`, and `0` applies the update on the next resume.
|
|
504
506
|
|
|
505
507
|
#### `update-history`
|
|
506
508
|
|
|
@@ -509,6 +511,8 @@ Update the release history for a specific CodePush update.
|
|
|
509
511
|
- Use the `--mandatory` option to make the update as mandatory or optional.
|
|
510
512
|
- Use the `--rollout` option to change the rollout percentage of the update. (0~100, inclusive)
|
|
511
513
|
- If the rollout percentage is reduced, users who fall outside the new target will have their rollout canceled and rollback to the previous latest version.
|
|
514
|
+
- Use the `--minimum-background-duration` option to change how many seconds the app must have been in the background before the update is applied on resume, for `ON_NEXT_RESUME` and `ON_NEXT_SUSPEND` installs only. (whole seconds, 0 or greater)
|
|
515
|
+
- It can be lowered after a release has gone out - setting it to `0`, for example, applies the update on the next resume instead of waiting.
|
|
512
516
|
|
|
513
517
|
**Example:**
|
|
514
518
|
- Rollback the CodePush update `1.0.1` (targeting the binary app version `1.0.0`).
|
|
@@ -96,8 +96,8 @@ describe("staging the release history a config is handed", () => {
|
|
|
96
96
|
};
|
|
97
97
|
|
|
98
98
|
await Promise.all([
|
|
99
|
-
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "ios", "RN0840", undefined, false, undefined),
|
|
100
|
-
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "android", "RN0840", undefined, false, undefined),
|
|
99
|
+
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "ios", "RN0840", undefined, false, undefined, undefined),
|
|
100
|
+
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "android", "RN0840", undefined, false, undefined, undefined),
|
|
101
101
|
]);
|
|
102
102
|
|
|
103
103
|
expect(staged.ios).toContain("ios-url");
|
|
@@ -15,6 +15,7 @@ export async function addToReleaseHistory(
|
|
|
15
15
|
enable: boolean,
|
|
16
16
|
rollout: number | undefined,
|
|
17
17
|
diffPackages: Record<string, string> | undefined,
|
|
18
|
+
minimumBackgroundDuration: number | undefined,
|
|
18
19
|
): Promise<void> {
|
|
19
20
|
const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
|
|
20
21
|
|
|
@@ -49,6 +50,11 @@ export async function addToReleaseHistory(
|
|
|
49
50
|
newReleaseHistory[appVersion].rollout = rollout;
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
// An entry without it leaves the wait to the sync option, so 0 has to be written.
|
|
54
|
+
if (typeof minimumBackgroundDuration === 'number') {
|
|
55
|
+
newReleaseHistory[appVersion].minimumBackgroundDuration = minimumBackgroundDuration;
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
try {
|
|
53
59
|
await stageReleaseHistoryFile(binaryVersion, newReleaseHistory, platform, (jsonFilePath) =>
|
|
54
60
|
setReleaseHistory(binaryVersion, jsonFilePath, newReleaseHistory, platform, identifier));
|
|
@@ -32,6 +32,7 @@ const ARG_INDEX = {
|
|
|
32
32
|
onOversizedPatch: 20,
|
|
33
33
|
bundleDownloader: 21,
|
|
34
34
|
diffBaseCount: 22,
|
|
35
|
+
minimumBackgroundDuration: 23,
|
|
35
36
|
} as const;
|
|
36
37
|
|
|
37
38
|
/**
|
|
@@ -147,4 +148,32 @@ describe("release command options", () => {
|
|
|
147
148
|
const { release } = await import("./release.js");
|
|
148
149
|
expect(jest.mocked(release)).not.toHaveBeenCalled();
|
|
149
150
|
});
|
|
151
|
+
|
|
152
|
+
it("passes the chosen minimum background duration through to the release", async () => {
|
|
153
|
+
const args = await runReleaseCommand(['-b', '1.0.0', '-v', '1.0.1', '--minimum-background-duration', '600']);
|
|
154
|
+
|
|
155
|
+
expect(args[ARG_INDEX.minimumBackgroundDuration]).toBe(600);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("leaves the minimum background duration unset when the option is not given, so the sync option decides", async () => {
|
|
159
|
+
const args = await runReleaseCommand(['-b', '1.0.0', '-v', '1.0.1']);
|
|
160
|
+
|
|
161
|
+
expect(args[ARG_INDEX.minimumBackgroundDuration]).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it.each([
|
|
165
|
+
['is negative', '-1'],
|
|
166
|
+
['is not a number at all', 'soon'],
|
|
167
|
+
])("rejects a minimum background duration that %s", async (_caseName, value) => {
|
|
168
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
169
|
+
jest.spyOn(process, 'exit').mockImplementation(((code?: number) => {
|
|
170
|
+
throw new Error(`process.exit(${code})`);
|
|
171
|
+
}) as never);
|
|
172
|
+
|
|
173
|
+
await expect(parseReleaseCommand(['-b', '1.0.0', '-v', '1.0.1', '--minimum-background-duration', value]))
|
|
174
|
+
.rejects.toThrow('process.exit(1)');
|
|
175
|
+
|
|
176
|
+
const { release } = await import("./release.js");
|
|
177
|
+
expect(jest.mocked(release)).not.toHaveBeenCalled();
|
|
178
|
+
});
|
|
150
179
|
});
|
|
@@ -31,6 +31,7 @@ type Options = {
|
|
|
31
31
|
binaryBundlePath?: string;
|
|
32
32
|
onOversizedPatch: OversizedPatchPolicy;
|
|
33
33
|
diffBaseCount: number;
|
|
34
|
+
minimumBackgroundDuration?: number;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
program.command('release')
|
|
@@ -57,6 +58,7 @@ program.command('release')
|
|
|
57
58
|
.choices(OVERSIZED_PATCH_POLICIES)
|
|
58
59
|
.default(DEFAULT_OVERSIZED_PATCH_POLICY))
|
|
59
60
|
.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)
|
|
61
|
+
.option('--minimum-background-duration <seconds>', 'seconds the app must have been in the background before this update is applied on resume. Overrides the minimumBackgroundDuration sync option.', parseDecimalInt)
|
|
60
62
|
.action(async (options: Options) => {
|
|
61
63
|
const config = findAndReadConfigFile(process.cwd(), options.config);
|
|
62
64
|
|
|
@@ -70,6 +72,12 @@ program.command('release')
|
|
|
70
72
|
process.exit(1);
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
if (options.minimumBackgroundDuration !== undefined
|
|
76
|
+
&& (!Number.isInteger(options.minimumBackgroundDuration) || options.minimumBackgroundDuration < 0)) {
|
|
77
|
+
console.error('--minimum-background-duration must be a whole number of seconds, 0 or greater.');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
73
81
|
if (options.hashCalc && !options.skipBundle) {
|
|
74
82
|
console.error('--hash-calc option can be used only when --skip-bundle is set to true.');
|
|
75
83
|
process.exit(1);
|
|
@@ -102,6 +110,7 @@ program.command('release')
|
|
|
102
110
|
options.onOversizedPatch,
|
|
103
111
|
config.bundleDownloader,
|
|
104
112
|
options.diffBaseCount,
|
|
113
|
+
options.minimumBackgroundDuration,
|
|
105
114
|
)
|
|
106
115
|
|
|
107
116
|
console.log('🚀 Release completed.')
|
|
@@ -142,6 +142,7 @@ type ReleaseOverrides = {
|
|
|
142
142
|
releaseHistory?: ReleaseHistoryInterface;
|
|
143
143
|
bundleDownloader?: CliConfigInterface['bundleDownloader'];
|
|
144
144
|
diffBaseCount?: number;
|
|
145
|
+
minimumBackgroundDuration?: number;
|
|
145
146
|
};
|
|
146
147
|
|
|
147
148
|
async function runRelease(staged: StagedBundle, overrides: ReleaseOverrides = {}) {
|
|
@@ -176,6 +177,7 @@ async function runRelease(staged: StagedBundle, overrides: ReleaseOverrides = {}
|
|
|
176
177
|
overrides.onOversizedPatch,
|
|
177
178
|
overrides.bundleDownloader,
|
|
178
179
|
overrides.diffBaseCount,
|
|
180
|
+
overrides.minimumBackgroundDuration,
|
|
179
181
|
);
|
|
180
182
|
|
|
181
183
|
return { uploads, releaseHistories: history.saved, uploadCountsWhenHistorySaved };
|
|
@@ -829,3 +831,29 @@ describe("release with asset diff bases", () => {
|
|
|
829
831
|
expect(path.basename(uploads[0].filePath)).toBe(staged.bundleFileName);
|
|
830
832
|
});
|
|
831
833
|
});
|
|
834
|
+
|
|
835
|
+
describe("release --minimum-background-duration", () => {
|
|
836
|
+
it("records the background wait on the release it publishes", async () => {
|
|
837
|
+
const staged = await stageBundleOutput("minimum-background-duration");
|
|
838
|
+
|
|
839
|
+
const { releaseHistories } = await runRelease(staged, { minimumBackgroundDuration: 600 });
|
|
840
|
+
|
|
841
|
+
expect(releaseHistories[0][APP_VERSION].minimumBackgroundDuration).toBe(600);
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
it("releases a background wait of zero seconds as zero, not as an unset option", async () => {
|
|
845
|
+
const staged = await stageBundleOutput("zero-minimum-background-duration");
|
|
846
|
+
|
|
847
|
+
const { releaseHistories } = await runRelease(staged, { minimumBackgroundDuration: 0 });
|
|
848
|
+
|
|
849
|
+
expect(releaseHistories[0][APP_VERSION].minimumBackgroundDuration).toBe(0);
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
it("leaves the release saying nothing about the background wait when the option is not given", async () => {
|
|
853
|
+
const staged = await stageBundleOutput("no-minimum-background-duration");
|
|
854
|
+
|
|
855
|
+
const { releaseHistories } = await runRelease(staged);
|
|
856
|
+
|
|
857
|
+
expect(releaseHistories[0][APP_VERSION]).not.toHaveProperty('minimumBackgroundDuration');
|
|
858
|
+
});
|
|
859
|
+
});
|
|
@@ -49,6 +49,7 @@ export async function release(
|
|
|
49
49
|
onOversizedPatch: OversizedPatchPolicy = DEFAULT_OVERSIZED_PATCH_POLICY,
|
|
50
50
|
bundleDownloader?: CliConfigInterface['bundleDownloader'],
|
|
51
51
|
diffBaseCount: number = DEFAULT_DIFF_BASE_COUNT,
|
|
52
|
+
minimumBackgroundDuration?: number,
|
|
52
53
|
): Promise<void> {
|
|
53
54
|
if (baseBundlePath) {
|
|
54
55
|
// Checked before the bundler runs, so the wrong base bundle costs a second rather
|
|
@@ -167,6 +168,7 @@ export async function release(
|
|
|
167
168
|
enable,
|
|
168
169
|
rollout,
|
|
169
170
|
Object.keys(diffPackages).length > 0 ? diffPackages : undefined,
|
|
171
|
+
minimumBackgroundDuration,
|
|
170
172
|
)
|
|
171
173
|
|
|
172
174
|
if (!skipCleanup) {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
4
|
+
import type { CliConfigInterface, ReleaseHistoryInterface } from "../../../typings/react-native-code-push.d.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Checks the command definition against the entry it saves. Everything this command does
|
|
8
|
+
* ends up in the release history the config is handed, so an option that never reaches it
|
|
9
|
+
* leaves the release exactly as it was - which the command still reports as a success.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const BINARY_VERSION = '1.0.0';
|
|
13
|
+
const APP_VERSION = '1.0.1';
|
|
14
|
+
|
|
15
|
+
let mockConfig: CliConfigInterface;
|
|
16
|
+
|
|
17
|
+
jest.mock("../../utils/fsUtils.js", () => ({
|
|
18
|
+
findAndReadConfigFile: () => mockConfig,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Puts one released version in the config's history and records every history it is
|
|
23
|
+
* handed back, so a case can read the entry as the consumer would store it.
|
|
24
|
+
*/
|
|
25
|
+
function stageReleaseHistory(): ReleaseHistoryInterface[] {
|
|
26
|
+
const releaseHistory: ReleaseHistoryInterface = {
|
|
27
|
+
[APP_VERSION]: {
|
|
28
|
+
enabled: true,
|
|
29
|
+
mandatory: false,
|
|
30
|
+
downloadUrl: 'https://cdn.example.com/bundle',
|
|
31
|
+
packageHash: 'a3f1c0',
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const saved: ReleaseHistoryInterface[] = [];
|
|
35
|
+
|
|
36
|
+
mockConfig = {
|
|
37
|
+
bundleUploader: async () => ({ downloadUrl: 'https://cdn.example.com/bundle' }),
|
|
38
|
+
getReleaseHistory: async () => releaseHistory,
|
|
39
|
+
// The command edits the history in place, so what it saved is copied out here.
|
|
40
|
+
setReleaseHistory: async (_binaryVersion, _jsonFilePath, releaseInfo) => {
|
|
41
|
+
saved.push(structuredClone(releaseInfo));
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return saved;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Parses an `update-history` invocation against the real command definition. Commander is
|
|
50
|
+
* asked to throw instead of exiting, and to keep its diagnostics to itself, so a rejected
|
|
51
|
+
* option can be asserted on without ending the worker or the output.
|
|
52
|
+
*/
|
|
53
|
+
async function parseUpdateHistoryCommand(args: string[]): Promise<void> {
|
|
54
|
+
const { program } = await import("commander");
|
|
55
|
+
await import("./index.js");
|
|
56
|
+
|
|
57
|
+
const updateHistoryCommand = program.commands.find((command) => command.name() === 'update-history');
|
|
58
|
+
updateHistoryCommand?.exitOverride();
|
|
59
|
+
updateHistoryCommand?.configureOutput({ writeErr: () => {} });
|
|
60
|
+
|
|
61
|
+
await program.parseAsync(['update-history', ...args], { from: 'user' });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function runUpdateHistoryCommand(args: string[]): Promise<void> {
|
|
65
|
+
await parseUpdateHistoryCommand(['-b', BINARY_VERSION, '-v', APP_VERSION, ...args]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let saved: ReleaseHistoryInterface[];
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
jest.resetModules();
|
|
72
|
+
saved = stageReleaseHistory();
|
|
73
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
74
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
75
|
+
jest.spyOn(process, 'exit').mockImplementation(((code?: number) => {
|
|
76
|
+
throw new Error(`process.exit(${code})`);
|
|
77
|
+
}) as never);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
jest.restoreAllMocks();
|
|
82
|
+
// The command writes its JSON under the directory it was invoked in.
|
|
83
|
+
fs.rmSync(path.resolve(process.cwd(), "codepush-release-history"), { recursive: true, force: true });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("update-history command options", () => {
|
|
87
|
+
it("lowers the background wait of a release that is already out to zero seconds", async () => {
|
|
88
|
+
await runUpdateHistoryCommand(['--minimum-background-duration', '0']);
|
|
89
|
+
|
|
90
|
+
expect(saved).toHaveLength(1);
|
|
91
|
+
expect(saved[0][APP_VERSION].minimumBackgroundDuration).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("leaves the entry saying nothing about the background wait when only --enable is given", async () => {
|
|
95
|
+
await runUpdateHistoryCommand(['--enable', 'false']);
|
|
96
|
+
|
|
97
|
+
expect(saved[0][APP_VERSION].enabled).toBe(false);
|
|
98
|
+
expect(saved[0][APP_VERSION]).not.toHaveProperty('minimumBackgroundDuration');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("saves the rollout percentage when --rollout is the only option given", async () => {
|
|
102
|
+
await runUpdateHistoryCommand(['--rollout', '50']);
|
|
103
|
+
|
|
104
|
+
expect(saved[0][APP_VERSION].rollout).toBe(50);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("exits without saving anything when no option says what to change", async () => {
|
|
108
|
+
await expect(runUpdateHistoryCommand([])).rejects.toThrow('process.exit(1)');
|
|
109
|
+
|
|
110
|
+
expect(saved).toHaveLength(0);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("rejects a negative background wait and saves nothing", async () => {
|
|
114
|
+
await expect(runUpdateHistoryCommand(['--minimum-background-duration', '-1']))
|
|
115
|
+
.rejects.toThrow('process.exit(1)');
|
|
116
|
+
|
|
117
|
+
expect(saved).toHaveLength(0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it.each([
|
|
121
|
+
['a percentage above 100', '150'],
|
|
122
|
+
['a percentage that is not a number', 'abc'],
|
|
123
|
+
])("rejects %s and saves nothing", async (_scenario, rollout) => {
|
|
124
|
+
await expect(runUpdateHistoryCommand(['--rollout', rollout]))
|
|
125
|
+
.rejects.toThrow('process.exit(1)');
|
|
126
|
+
|
|
127
|
+
expect(saved).toHaveLength(0);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -12,6 +12,7 @@ type Options = {
|
|
|
12
12
|
mandatory?: boolean;
|
|
13
13
|
enable?: boolean;
|
|
14
14
|
rollout?: number;
|
|
15
|
+
minimumBackgroundDuration?: number;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
program.command('update-history')
|
|
@@ -24,14 +25,32 @@ program.command('update-history')
|
|
|
24
25
|
.option('-m, --mandatory <bool>', 'make the release to be mandatory', parseBoolean, undefined)
|
|
25
26
|
.option('-e, --enable <bool>', 'make the release to be enabled', parseBoolean, undefined)
|
|
26
27
|
.option('--rollout <number>', 'rollout percentage (0-100)', parseFloat, undefined)
|
|
28
|
+
.option('--minimum-background-duration <seconds>', 'seconds the app must have been in the background before this update is applied on resume. Overrides the minimumBackgroundDuration sync option.', parseDecimalInt, undefined)
|
|
27
29
|
.action(async (options: Options) => {
|
|
28
30
|
const config = findAndReadConfigFile(process.cwd(), options.config);
|
|
29
31
|
|
|
30
|
-
if (typeof options.mandatory !== "boolean"
|
|
32
|
+
if (typeof options.mandatory !== "boolean"
|
|
33
|
+
&& typeof options.enable !== "boolean"
|
|
34
|
+
&& typeof options.rollout !== "number"
|
|
35
|
+
&& typeof options.minimumBackgroundDuration !== "number") {
|
|
31
36
|
console.error('No options specified. Exiting the program.')
|
|
32
37
|
process.exit(1)
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
// `Number.isFinite` is what rejects a non-numeric `--rollout`: `parseFloat` turns it into
|
|
41
|
+
// NaN, and both `NaN < 0` and `NaN > 100` are false.
|
|
42
|
+
if (options.rollout !== undefined
|
|
43
|
+
&& (!Number.isFinite(options.rollout) || options.rollout < 0 || options.rollout > 100)) {
|
|
44
|
+
console.error('Rollout percentage number must be between 0 and 100 (inclusive).');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (options.minimumBackgroundDuration !== undefined
|
|
49
|
+
&& (!Number.isInteger(options.minimumBackgroundDuration) || options.minimumBackgroundDuration < 0)) {
|
|
50
|
+
console.error('--minimum-background-duration must be a whole number of seconds, 0 or greater.');
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
35
54
|
await updateReleaseHistory(
|
|
36
55
|
options.appVersion,
|
|
37
56
|
options.binaryVersion,
|
|
@@ -41,10 +60,17 @@ program.command('update-history')
|
|
|
41
60
|
options.identifier,
|
|
42
61
|
options.mandatory,
|
|
43
62
|
options.enable,
|
|
44
|
-
options.rollout
|
|
63
|
+
options.rollout,
|
|
64
|
+
options.minimumBackgroundDuration
|
|
45
65
|
)
|
|
46
66
|
});
|
|
47
67
|
|
|
68
|
+
// Not `parseInt` itself: commander hands a coercion function the current value as its
|
|
69
|
+
// second argument, which `parseInt` reads as the radix.
|
|
70
|
+
function parseDecimalInt(value: string): number {
|
|
71
|
+
return parseInt(value, 10);
|
|
72
|
+
}
|
|
73
|
+
|
|
48
74
|
function parseBoolean(value: string) {
|
|
49
75
|
if (value === 'true') return true;
|
|
50
76
|
if (value === 'false') return false;
|
|
@@ -11,6 +11,7 @@ export async function updateReleaseHistory(
|
|
|
11
11
|
mandatory: boolean | undefined,
|
|
12
12
|
enable: boolean | undefined,
|
|
13
13
|
rollout: number | undefined,
|
|
14
|
+
minimumBackgroundDuration: number | undefined,
|
|
14
15
|
): Promise<void> {
|
|
15
16
|
const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
|
|
16
17
|
|
|
@@ -20,6 +21,8 @@ export async function updateReleaseHistory(
|
|
|
20
21
|
if (typeof mandatory === "boolean") updateInfo.mandatory = mandatory;
|
|
21
22
|
if (typeof enable === "boolean") updateInfo.enabled = enable;
|
|
22
23
|
if (typeof rollout === "number") updateInfo.rollout = rollout;
|
|
24
|
+
// 0 is what lowers the wait to "apply on the next resume", so truthiness cannot decide here.
|
|
25
|
+
if (typeof minimumBackgroundDuration === "number") updateInfo.minimumBackgroundDuration = minimumBackgroundDuration;
|
|
23
26
|
|
|
24
27
|
try {
|
|
25
28
|
await stageReleaseHistoryFile(binaryVersion, releaseHistory, platform, (jsonFilePath) =>
|
|
@@ -68,8 +68,8 @@ describe("staging the release history a config is handed", () => {
|
|
|
68
68
|
staged[platform] = fs.readFileSync(jsonFilePath, "utf8");
|
|
69
69
|
};
|
|
70
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),
|
|
71
|
+
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "ios", "RN0840", undefined, false, undefined, undefined),
|
|
72
|
+
updateReleaseHistory("1.0.1", BINARY_VERSION, getReleaseHistory, setReleaseHistory, "android", "RN0840", undefined, false, undefined, undefined),
|
|
73
73
|
]);
|
|
74
74
|
expect(staged.ios).toContain("ios-url");
|
|
75
75
|
expect(staged.ios).not.toContain("android-url");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
2
|
-
export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownloadUrl, binaryPatchDownloadUrl, packageHash, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout, diffPackages) {
|
|
2
|
+
export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownloadUrl, binaryPatchDownloadUrl, packageHash, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout, diffPackages, minimumBackgroundDuration) {
|
|
3
3
|
const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
|
|
4
4
|
const updateInfo = releaseHistory[appVersion];
|
|
5
5
|
if (updateInfo) {
|
|
@@ -26,6 +26,10 @@ export async function addToReleaseHistory(appVersion, binaryVersion, bundleDownl
|
|
|
26
26
|
if (typeof rollout === 'number') {
|
|
27
27
|
newReleaseHistory[appVersion].rollout = rollout;
|
|
28
28
|
}
|
|
29
|
+
// An entry without it leaves the wait to the sync option, so 0 has to be written.
|
|
30
|
+
if (typeof minimumBackgroundDuration === 'number') {
|
|
31
|
+
newReleaseHistory[appVersion].minimumBackgroundDuration = minimumBackgroundDuration;
|
|
32
|
+
}
|
|
29
33
|
try {
|
|
30
34
|
await stageReleaseHistoryFile(binaryVersion, newReleaseHistory, platform, (jsonFilePath) => setReleaseHistory(binaryVersion, jsonFilePath, newReleaseHistory, platform, identifier));
|
|
31
35
|
}
|
|
@@ -27,6 +27,7 @@ program.command('release')
|
|
|
27
27
|
.choices(OVERSIZED_PATCH_POLICIES)
|
|
28
28
|
.default(DEFAULT_OVERSIZED_PATCH_POLICY))
|
|
29
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)
|
|
30
|
+
.option('--minimum-background-duration <seconds>', 'seconds the app must have been in the background before this update is applied on resume. Overrides the minimumBackgroundDuration sync option.', parseDecimalInt)
|
|
30
31
|
.action(async (options) => {
|
|
31
32
|
const config = findAndReadConfigFile(process.cwd(), options.config);
|
|
32
33
|
if (typeof options.rollout === 'number' && (options.rollout < 0 || options.rollout > 100)) {
|
|
@@ -37,13 +38,18 @@ program.command('release')
|
|
|
37
38
|
console.error('--diff-base-count must be a whole number of releases, 0 or greater.');
|
|
38
39
|
process.exit(1);
|
|
39
40
|
}
|
|
41
|
+
if (options.minimumBackgroundDuration !== undefined
|
|
42
|
+
&& (!Number.isInteger(options.minimumBackgroundDuration) || options.minimumBackgroundDuration < 0)) {
|
|
43
|
+
console.error('--minimum-background-duration must be a whole number of seconds, 0 or greater.');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
40
46
|
if (options.hashCalc && !options.skipBundle) {
|
|
41
47
|
console.error('--hash-calc option can be used only when --skip-bundle is set to true.');
|
|
42
48
|
process.exit(1);
|
|
43
49
|
}
|
|
44
50
|
// Resolved before bundling so a wrong path fails now rather than after a build.
|
|
45
51
|
const baseBundlePath = resolveBinaryBundlePathOption(options.binaryBundlePath);
|
|
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);
|
|
52
|
+
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, options.minimumBackgroundDuration);
|
|
47
53
|
console.log('🚀 Release completed.');
|
|
48
54
|
});
|
|
49
55
|
// Not `parseInt` itself: commander hands a coercion function the current value as its
|
|
@@ -29,6 +29,7 @@ const ARG_INDEX = {
|
|
|
29
29
|
onOversizedPatch: 20,
|
|
30
30
|
bundleDownloader: 21,
|
|
31
31
|
diffBaseCount: 22,
|
|
32
|
+
minimumBackgroundDuration: 23,
|
|
32
33
|
};
|
|
33
34
|
/**
|
|
34
35
|
* Parses a `release` invocation against the real command definition. Commander is asked
|
|
@@ -118,4 +119,25 @@ describe("release command options", () => {
|
|
|
118
119
|
const { release } = await import("./release.js");
|
|
119
120
|
expect(jest.mocked(release)).not.toHaveBeenCalled();
|
|
120
121
|
});
|
|
122
|
+
it("passes the chosen minimum background duration through to the release", async () => {
|
|
123
|
+
const args = await runReleaseCommand(['-b', '1.0.0', '-v', '1.0.1', '--minimum-background-duration', '600']);
|
|
124
|
+
expect(args[ARG_INDEX.minimumBackgroundDuration]).toBe(600);
|
|
125
|
+
});
|
|
126
|
+
it("leaves the minimum background duration unset when the option is not given, so the sync option decides", async () => {
|
|
127
|
+
const args = await runReleaseCommand(['-b', '1.0.0', '-v', '1.0.1']);
|
|
128
|
+
expect(args[ARG_INDEX.minimumBackgroundDuration]).toBeUndefined();
|
|
129
|
+
});
|
|
130
|
+
it.each([
|
|
131
|
+
['is negative', '-1'],
|
|
132
|
+
['is not a number at all', 'soon'],
|
|
133
|
+
])("rejects a minimum background duration that %s", async (_caseName, value) => {
|
|
134
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
135
|
+
jest.spyOn(process, 'exit').mockImplementation(((code) => {
|
|
136
|
+
throw new Error(`process.exit(${code})`);
|
|
137
|
+
}));
|
|
138
|
+
await expect(parseReleaseCommand(['-b', '1.0.0', '-v', '1.0.1', '--minimum-background-duration', value]))
|
|
139
|
+
.rejects.toThrow('process.exit(1)');
|
|
140
|
+
const { release } = await import("./release.js");
|
|
141
|
+
expect(jest.mocked(release)).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
121
143
|
});
|
|
@@ -9,7 +9,7 @@ import { resolveAssetDiffBases } from "../../functions/resolveAssetDiffBases.js"
|
|
|
9
9
|
import { BINARY_PATCH_ARCHIVE_SUFFIX, BINARY_PATCH_BASE_RECORD_NAME, DEFAULT_OVERSIZED_PATCH_POLICY, extractCodePushBundleContents, formatBinaryPatchSummary, hashBundleFile, isPatchArchiveOversized, makeBinaryPatchBundle, readBinaryPatchBaseRecord, } from "../../functions/makeBinaryPatchBundle.js";
|
|
10
10
|
/** How many of the most recent releases a release builds asset diff archives against. */
|
|
11
11
|
export const DEFAULT_DIFF_BASE_COUNT = 3;
|
|
12
|
-
export async function release(bundleUploader, getReleaseHistory, setReleaseHistory, binaryVersion, appVersion, framework, platform, identifier, outputPath, entryFile, jsBundleName, mandatory, enable, rollout, skipBundle, skipCleanup, bundleDirectory, outputMetroDir, hashCalc, baseBundlePath, onOversizedPatch = DEFAULT_OVERSIZED_PATCH_POLICY, bundleDownloader, diffBaseCount = DEFAULT_DIFF_BASE_COUNT) {
|
|
12
|
+
export async function release(bundleUploader, getReleaseHistory, setReleaseHistory, binaryVersion, appVersion, framework, platform, identifier, outputPath, entryFile, jsBundleName, mandatory, enable, rollout, skipBundle, skipCleanup, bundleDirectory, outputMetroDir, hashCalc, baseBundlePath, onOversizedPatch = DEFAULT_OVERSIZED_PATCH_POLICY, bundleDownloader, diffBaseCount = DEFAULT_DIFF_BASE_COUNT, minimumBackgroundDuration) {
|
|
13
13
|
if (baseBundlePath) {
|
|
14
14
|
// Checked before the bundler runs, so the wrong base bundle costs a second rather
|
|
15
15
|
// than a full build.
|
|
@@ -92,7 +92,7 @@ export async function release(bundleUploader, getReleaseHistory, setReleaseHisto
|
|
|
92
92
|
`(${error instanceof Error ? error.message : String(error)}).`);
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
|
-
await addToReleaseHistory(appVersion, binaryVersion, downloadUrl, patchDownloadUrl, packageHash, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout, Object.keys(diffPackages).length > 0 ? diffPackages : undefined);
|
|
95
|
+
await addToReleaseHistory(appVersion, binaryVersion, downloadUrl, patchDownloadUrl, packageHash, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout, Object.keys(diffPackages).length > 0 ? diffPackages : undefined, minimumBackgroundDuration);
|
|
96
96
|
if (!skipCleanup) {
|
|
97
97
|
cleanUpOutputs(outputPath);
|
|
98
98
|
}
|
|
@@ -95,7 +95,7 @@ async function runRelease(staged, overrides = {}) {
|
|
|
95
95
|
await release(recordingUploader(uploads, overrides.uploadFailsFor), history.getReleaseHistory, history.setReleaseHistory, BINARY_VERSION, APP_VERSION, undefined, overrides.platform ?? 'ios', undefined, staged.outputPath, 'index.ts',
|
|
96
96
|
// Left unset unless the case is about -j, the way commander leaves it.
|
|
97
97
|
overrides.jsBundleName, false, true, undefined, true, // skipBundle
|
|
98
|
-
overrides.skipCleanup ?? true, staged.bundleDirectory, undefined, undefined, overrides.binaryBundlePath, overrides.onOversizedPatch, overrides.bundleDownloader, overrides.diffBaseCount);
|
|
98
|
+
overrides.skipCleanup ?? true, staged.bundleDirectory, undefined, undefined, overrides.binaryBundlePath, overrides.onOversizedPatch, overrides.bundleDownloader, overrides.diffBaseCount, overrides.minimumBackgroundDuration);
|
|
99
99
|
return { uploads, releaseHistories: history.saved, uploadCountsWhenHistorySaved };
|
|
100
100
|
}
|
|
101
101
|
let logs;
|
|
@@ -563,3 +563,20 @@ describe("release with asset diff bases", () => {
|
|
|
563
563
|
expect(path.basename(uploads[0].filePath)).toBe(staged.bundleFileName);
|
|
564
564
|
});
|
|
565
565
|
});
|
|
566
|
+
describe("release --minimum-background-duration", () => {
|
|
567
|
+
it("records the background wait on the release it publishes", async () => {
|
|
568
|
+
const staged = await stageBundleOutput("minimum-background-duration");
|
|
569
|
+
const { releaseHistories } = await runRelease(staged, { minimumBackgroundDuration: 600 });
|
|
570
|
+
expect(releaseHistories[0][APP_VERSION].minimumBackgroundDuration).toBe(600);
|
|
571
|
+
});
|
|
572
|
+
it("releases a background wait of zero seconds as zero, not as an unset option", async () => {
|
|
573
|
+
const staged = await stageBundleOutput("zero-minimum-background-duration");
|
|
574
|
+
const { releaseHistories } = await runRelease(staged, { minimumBackgroundDuration: 0 });
|
|
575
|
+
expect(releaseHistories[0][APP_VERSION].minimumBackgroundDuration).toBe(0);
|
|
576
|
+
});
|
|
577
|
+
it("leaves the release saying nothing about the background wait when the option is not given", async () => {
|
|
578
|
+
const staged = await stageBundleOutput("no-minimum-background-duration");
|
|
579
|
+
const { releaseHistories } = await runRelease(staged);
|
|
580
|
+
expect(releaseHistories[0][APP_VERSION]).not.toHaveProperty('minimumBackgroundDuration');
|
|
581
|
+
});
|
|
582
|
+
});
|
|
@@ -12,14 +12,35 @@ program.command('update-history')
|
|
|
12
12
|
.option('-m, --mandatory <bool>', 'make the release to be mandatory', parseBoolean, undefined)
|
|
13
13
|
.option('-e, --enable <bool>', 'make the release to be enabled', parseBoolean, undefined)
|
|
14
14
|
.option('--rollout <number>', 'rollout percentage (0-100)', parseFloat, undefined)
|
|
15
|
+
.option('--minimum-background-duration <seconds>', 'seconds the app must have been in the background before this update is applied on resume. Overrides the minimumBackgroundDuration sync option.', parseDecimalInt, undefined)
|
|
15
16
|
.action(async (options) => {
|
|
16
17
|
const config = findAndReadConfigFile(process.cwd(), options.config);
|
|
17
|
-
if (typeof options.mandatory !== "boolean"
|
|
18
|
+
if (typeof options.mandatory !== "boolean"
|
|
19
|
+
&& typeof options.enable !== "boolean"
|
|
20
|
+
&& typeof options.rollout !== "number"
|
|
21
|
+
&& typeof options.minimumBackgroundDuration !== "number") {
|
|
18
22
|
console.error('No options specified. Exiting the program.');
|
|
19
23
|
process.exit(1);
|
|
20
24
|
}
|
|
21
|
-
|
|
25
|
+
// `Number.isFinite` is what rejects a non-numeric `--rollout`: `parseFloat` turns it into
|
|
26
|
+
// NaN, and both `NaN < 0` and `NaN > 100` are false.
|
|
27
|
+
if (options.rollout !== undefined
|
|
28
|
+
&& (!Number.isFinite(options.rollout) || options.rollout < 0 || options.rollout > 100)) {
|
|
29
|
+
console.error('Rollout percentage number must be between 0 and 100 (inclusive).');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
if (options.minimumBackgroundDuration !== undefined
|
|
33
|
+
&& (!Number.isInteger(options.minimumBackgroundDuration) || options.minimumBackgroundDuration < 0)) {
|
|
34
|
+
console.error('--minimum-background-duration must be a whole number of seconds, 0 or greater.');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
await updateReleaseHistory(options.appVersion, options.binaryVersion, config.getReleaseHistory, config.setReleaseHistory, options.platform, options.identifier, options.mandatory, options.enable, options.rollout, options.minimumBackgroundDuration);
|
|
22
38
|
});
|
|
39
|
+
// Not `parseInt` itself: commander hands a coercion function the current value as its
|
|
40
|
+
// second argument, which `parseInt` reads as the radix.
|
|
41
|
+
function parseDecimalInt(value) {
|
|
42
|
+
return parseInt(value, 10);
|
|
43
|
+
}
|
|
23
44
|
function parseBoolean(value) {
|
|
24
45
|
if (value === 'true')
|
|
25
46
|
return true;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
4
|
+
/**
|
|
5
|
+
* Checks the command definition against the entry it saves. Everything this command does
|
|
6
|
+
* ends up in the release history the config is handed, so an option that never reaches it
|
|
7
|
+
* leaves the release exactly as it was - which the command still reports as a success.
|
|
8
|
+
*/
|
|
9
|
+
const BINARY_VERSION = '1.0.0';
|
|
10
|
+
const APP_VERSION = '1.0.1';
|
|
11
|
+
let mockConfig;
|
|
12
|
+
jest.mock("../../utils/fsUtils.js", () => ({
|
|
13
|
+
findAndReadConfigFile: () => mockConfig,
|
|
14
|
+
}));
|
|
15
|
+
/**
|
|
16
|
+
* Puts one released version in the config's history and records every history it is
|
|
17
|
+
* handed back, so a case can read the entry as the consumer would store it.
|
|
18
|
+
*/
|
|
19
|
+
function stageReleaseHistory() {
|
|
20
|
+
const releaseHistory = {
|
|
21
|
+
[APP_VERSION]: {
|
|
22
|
+
enabled: true,
|
|
23
|
+
mandatory: false,
|
|
24
|
+
downloadUrl: 'https://cdn.example.com/bundle',
|
|
25
|
+
packageHash: 'a3f1c0',
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const saved = [];
|
|
29
|
+
mockConfig = {
|
|
30
|
+
bundleUploader: async () => ({ downloadUrl: 'https://cdn.example.com/bundle' }),
|
|
31
|
+
getReleaseHistory: async () => releaseHistory,
|
|
32
|
+
// The command edits the history in place, so what it saved is copied out here.
|
|
33
|
+
setReleaseHistory: async (_binaryVersion, _jsonFilePath, releaseInfo) => {
|
|
34
|
+
saved.push(structuredClone(releaseInfo));
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
return saved;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parses an `update-history` invocation against the real command definition. Commander is
|
|
41
|
+
* asked to throw instead of exiting, and to keep its diagnostics to itself, so a rejected
|
|
42
|
+
* option can be asserted on without ending the worker or the output.
|
|
43
|
+
*/
|
|
44
|
+
async function parseUpdateHistoryCommand(args) {
|
|
45
|
+
const { program } = await import("commander");
|
|
46
|
+
await import("./index.js");
|
|
47
|
+
const updateHistoryCommand = program.commands.find((command) => command.name() === 'update-history');
|
|
48
|
+
updateHistoryCommand?.exitOverride();
|
|
49
|
+
updateHistoryCommand?.configureOutput({ writeErr: () => { } });
|
|
50
|
+
await program.parseAsync(['update-history', ...args], { from: 'user' });
|
|
51
|
+
}
|
|
52
|
+
async function runUpdateHistoryCommand(args) {
|
|
53
|
+
await parseUpdateHistoryCommand(['-b', BINARY_VERSION, '-v', APP_VERSION, ...args]);
|
|
54
|
+
}
|
|
55
|
+
let saved;
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
jest.resetModules();
|
|
58
|
+
saved = stageReleaseHistory();
|
|
59
|
+
jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
60
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
61
|
+
jest.spyOn(process, 'exit').mockImplementation(((code) => {
|
|
62
|
+
throw new Error(`process.exit(${code})`);
|
|
63
|
+
}));
|
|
64
|
+
});
|
|
65
|
+
afterEach(() => {
|
|
66
|
+
jest.restoreAllMocks();
|
|
67
|
+
// The command writes its JSON under the directory it was invoked in.
|
|
68
|
+
fs.rmSync(path.resolve(process.cwd(), "codepush-release-history"), { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
describe("update-history command options", () => {
|
|
71
|
+
it("lowers the background wait of a release that is already out to zero seconds", async () => {
|
|
72
|
+
await runUpdateHistoryCommand(['--minimum-background-duration', '0']);
|
|
73
|
+
expect(saved).toHaveLength(1);
|
|
74
|
+
expect(saved[0][APP_VERSION].minimumBackgroundDuration).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
it("leaves the entry saying nothing about the background wait when only --enable is given", async () => {
|
|
77
|
+
await runUpdateHistoryCommand(['--enable', 'false']);
|
|
78
|
+
expect(saved[0][APP_VERSION].enabled).toBe(false);
|
|
79
|
+
expect(saved[0][APP_VERSION]).not.toHaveProperty('minimumBackgroundDuration');
|
|
80
|
+
});
|
|
81
|
+
it("saves the rollout percentage when --rollout is the only option given", async () => {
|
|
82
|
+
await runUpdateHistoryCommand(['--rollout', '50']);
|
|
83
|
+
expect(saved[0][APP_VERSION].rollout).toBe(50);
|
|
84
|
+
});
|
|
85
|
+
it("exits without saving anything when no option says what to change", async () => {
|
|
86
|
+
await expect(runUpdateHistoryCommand([])).rejects.toThrow('process.exit(1)');
|
|
87
|
+
expect(saved).toHaveLength(0);
|
|
88
|
+
});
|
|
89
|
+
it("rejects a negative background wait and saves nothing", async () => {
|
|
90
|
+
await expect(runUpdateHistoryCommand(['--minimum-background-duration', '-1']))
|
|
91
|
+
.rejects.toThrow('process.exit(1)');
|
|
92
|
+
expect(saved).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
it.each([
|
|
95
|
+
['a percentage above 100', '150'],
|
|
96
|
+
['a percentage that is not a number', 'abc'],
|
|
97
|
+
])("rejects %s and saves nothing", async (_scenario, rollout) => {
|
|
98
|
+
await expect(runUpdateHistoryCommand(['--rollout', rollout]))
|
|
99
|
+
.rejects.toThrow('process.exit(1)');
|
|
100
|
+
expect(saved).toHaveLength(0);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { stageReleaseHistoryFile } from "../../functions/stageReleaseHistoryFile.js";
|
|
2
|
-
export async function updateReleaseHistory(appVersion, binaryVersion, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout) {
|
|
2
|
+
export async function updateReleaseHistory(appVersion, binaryVersion, getReleaseHistory, setReleaseHistory, platform, identifier, mandatory, enable, rollout, minimumBackgroundDuration) {
|
|
3
3
|
const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
|
|
4
4
|
const updateInfo = releaseHistory[appVersion];
|
|
5
5
|
if (!updateInfo)
|
|
@@ -10,6 +10,9 @@ export async function updateReleaseHistory(appVersion, binaryVersion, getRelease
|
|
|
10
10
|
updateInfo.enabled = enable;
|
|
11
11
|
if (typeof rollout === "number")
|
|
12
12
|
updateInfo.rollout = rollout;
|
|
13
|
+
// 0 is what lowers the wait to "apply on the next resume", so truthiness cannot decide here.
|
|
14
|
+
if (typeof minimumBackgroundDuration === "number")
|
|
15
|
+
updateInfo.minimumBackgroundDuration = minimumBackgroundDuration;
|
|
13
16
|
try {
|
|
14
17
|
await stageReleaseHistoryFile(binaryVersion, releaseHistory, platform, (jsonFilePath) => setReleaseHistory(binaryVersion, jsonFilePath, releaseHistory, platform, identifier));
|
|
15
18
|
}
|
package/package.json
CHANGED
package/src/CodePush.js
CHANGED
|
@@ -200,6 +200,11 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
|
|
|
200
200
|
* back on when the diff fails on its asset side.
|
|
201
201
|
*/
|
|
202
202
|
asset_diff_download_url: diffPackageDownloadUrl,
|
|
203
|
+
/**
|
|
204
|
+
* Only present when the release asked for a background duration of its own, which
|
|
205
|
+
* then wins over the one the `sync` call passes.
|
|
206
|
+
*/
|
|
207
|
+
minimum_background_duration: latestReleaseInfo.minimumBackgroundDuration,
|
|
203
208
|
// (`enabled` will always be true in the release information obtained from the previous process.)
|
|
204
209
|
is_available: latestReleaseInfo.enabled,
|
|
205
210
|
package_hash: latestReleaseInfo.packageHash,
|
|
@@ -309,6 +314,11 @@ function mapToRemotePackageMetadata(updateInfo) {
|
|
|
309
314
|
...(updateInfo.asset_diff_download_url
|
|
310
315
|
? { assetDiffDownloadUrl: updateInfo.asset_diff_download_url }
|
|
311
316
|
: {}),
|
|
317
|
+
// Presence is read off the type, not off truthiness, so that a release asking for `0`
|
|
318
|
+
// seconds keeps its own value instead of looking like a release that asked for nothing.
|
|
319
|
+
...(typeof updateInfo.minimum_background_duration === 'number'
|
|
320
|
+
? { minimumBackgroundDuration: updateInfo.minimum_background_duration }
|
|
321
|
+
: {}),
|
|
312
322
|
};
|
|
313
323
|
}
|
|
314
324
|
|
|
@@ -552,6 +562,7 @@ const sync = (() => {
|
|
|
552
562
|
*/
|
|
553
563
|
async function syncInternal(options = {}, syncStatusChangeCallback, downloadProgressCallback, handleBinaryVersionMismatchCallback) {
|
|
554
564
|
let resolvedInstallMode;
|
|
565
|
+
let resolvedMinimumBackgroundDuration;
|
|
555
566
|
const syncOptions = {
|
|
556
567
|
deploymentKey: null,
|
|
557
568
|
ignoreFailedUpdates: true,
|
|
@@ -596,8 +607,8 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
|
|
596
607
|
if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESTART) {
|
|
597
608
|
log("Update is installed and will be run on the next app restart.");
|
|
598
609
|
} else if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESUME) {
|
|
599
|
-
if (
|
|
600
|
-
log(`Update is installed and will be run after the app has been in the background for at least ${
|
|
610
|
+
if (resolvedMinimumBackgroundDuration > 0) {
|
|
611
|
+
log(`Update is installed and will be run after the app has been in the background for at least ${resolvedMinimumBackgroundDuration} seconds.`);
|
|
601
612
|
} else {
|
|
602
613
|
log("Update is installed and will be run when the app next resumes.");
|
|
603
614
|
}
|
|
@@ -630,9 +641,11 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
|
|
|
630
641
|
|
|
631
642
|
// Determine the correct install mode based on whether the update is mandatory or not.
|
|
632
643
|
resolvedInstallMode = localPackage.isMandatory ? syncOptions.mandatoryInstallMode : syncOptions.installMode;
|
|
644
|
+
// `??` rather than `||`, so a release asking for `0` seconds keeps its own value.
|
|
645
|
+
resolvedMinimumBackgroundDuration = remotePackage.minimumBackgroundDuration ?? syncOptions.minimumBackgroundDuration;
|
|
633
646
|
|
|
634
647
|
syncStatusChangeCallback(CodePush.SyncStatus.INSTALLING_UPDATE);
|
|
635
|
-
await localPackage.install(resolvedInstallMode,
|
|
648
|
+
await localPackage.install(resolvedInstallMode, resolvedMinimumBackgroundDuration, () => {
|
|
636
649
|
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_INSTALLED);
|
|
637
650
|
});
|
|
638
651
|
|
package/src/CodePush.test.js
CHANGED
|
@@ -649,3 +649,60 @@ describe('telemetry callback errors', () => {
|
|
|
649
649
|
expect(nativeBridge.saveStatusReportForRetry).not.toHaveBeenCalled();
|
|
650
650
|
});
|
|
651
651
|
});
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* A release can ask for its own minimum background duration - how long the app has to have
|
|
655
|
+
* been in the background before an update installed with `ON_NEXT_RESUME` is applied. The
|
|
656
|
+
* app passes a duration to `sync` for the releases that ask for nothing, and whichever of
|
|
657
|
+
* the two wins is what the native module is given when the update is installed.
|
|
658
|
+
*/
|
|
659
|
+
describe('the minimum background duration an update is installed with', () => {
|
|
660
|
+
/** The release the CLI writes when the release carries its own background duration. */
|
|
661
|
+
function releaseAskingFor(minimumBackgroundDuration) {
|
|
662
|
+
const history = fullOnlyRelease();
|
|
663
|
+
history[LABEL].minimumBackgroundDuration = minimumBackgroundDuration;
|
|
664
|
+
return history;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/** The background duration the native module is given, in seconds. */
|
|
668
|
+
function installedMinimumBackgroundDuration(nativeBridge) {
|
|
669
|
+
expect(nativeBridge.installUpdate).toHaveBeenCalledTimes(1);
|
|
670
|
+
return nativeBridge.installUpdate.mock.calls[0][2];
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
it('takes the one the release history entry asks for over the one the sync call passes', async () => {
|
|
674
|
+
const { CodePush, nativeBridge } = loadCodePush({ releaseHistory: releaseAskingFor(600) });
|
|
675
|
+
|
|
676
|
+
const syncStatus = await CodePush.sync({
|
|
677
|
+
installMode: InstallMode.ON_NEXT_RESUME,
|
|
678
|
+
minimumBackgroundDuration: 3600,
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
|
|
682
|
+
expect(installedMinimumBackgroundDuration(nativeBridge)).toBe(600);
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('keeps the one the sync call passes when the release history entry asks for none', async () => {
|
|
686
|
+
const { CodePush, nativeBridge } = loadCodePush({ releaseHistory: fullOnlyRelease() });
|
|
687
|
+
|
|
688
|
+
const syncStatus = await CodePush.sync({
|
|
689
|
+
installMode: InstallMode.ON_NEXT_RESUME,
|
|
690
|
+
minimumBackgroundDuration: 3600,
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
|
|
694
|
+
expect(installedMinimumBackgroundDuration(nativeBridge)).toBe(3600);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it('runs a release that asks for zero seconds on the next resume, however long the sync call would have waited', async () => {
|
|
698
|
+
const { CodePush, nativeBridge } = loadCodePush({ releaseHistory: releaseAskingFor(0) });
|
|
699
|
+
|
|
700
|
+
const syncStatus = await CodePush.sync({
|
|
701
|
+
installMode: InstallMode.ON_NEXT_RESUME,
|
|
702
|
+
minimumBackgroundDuration: 3600,
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
expect(syncStatus).toBe(CodePush.SyncStatus.UPDATE_INSTALLED);
|
|
706
|
+
expect(installedMinimumBackgroundDuration(nativeBridge)).toBe(0);
|
|
707
|
+
});
|
|
708
|
+
});
|
|
@@ -44,6 +44,13 @@ export interface ReleaseInfo {
|
|
|
44
44
|
*/
|
|
45
45
|
diffPackages?: Record<string, string>;
|
|
46
46
|
packageHash: string;
|
|
47
|
+
/**
|
|
48
|
+
* The minimum number of seconds the app has to have been in the background before this
|
|
49
|
+
* release is applied, with the same meaning as `SyncOptions.minimumBackgroundDuration`.
|
|
50
|
+
* When it is present it takes precedence over the value passed to `sync`, so a single
|
|
51
|
+
* release can be applied sooner or later than the app asks for by default.
|
|
52
|
+
*/
|
|
53
|
+
minimumBackgroundDuration?: number;
|
|
47
54
|
rollout?: number;
|
|
48
55
|
}
|
|
49
56
|
|
|
@@ -73,6 +80,11 @@ export interface UpdateCheckResponse {
|
|
|
73
80
|
should_run_binary_version?: boolean;
|
|
74
81
|
update_app_version?: boolean;
|
|
75
82
|
is_mandatory?: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* The minimum number of seconds the app has to have been in the background before this
|
|
85
|
+
* update is applied. It is only present when the release asked for one of its own.
|
|
86
|
+
*/
|
|
87
|
+
minimum_background_duration?: number;
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
/**
|
|
@@ -345,6 +357,13 @@ export interface RemotePackage extends Package {
|
|
|
345
357
|
* `UpdateArchiveResult`.
|
|
346
358
|
*/
|
|
347
359
|
assetDiffDownloadUrl?: string;
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The minimum number of seconds the app has to have been in the background before this
|
|
363
|
+
* update is applied. It is only present when the release history entry set it, and it
|
|
364
|
+
* then takes precedence over the `minimumBackgroundDuration` passed to `sync`.
|
|
365
|
+
*/
|
|
366
|
+
minimumBackgroundDuration?: number;
|
|
348
367
|
}
|
|
349
368
|
|
|
350
369
|
export interface SyncOptions {
|
|
@@ -372,6 +391,7 @@ export interface SyncOptions {
|
|
|
372
391
|
* only applies to updates which are installed using `InstallMode.ON_NEXT_RESUME` or `InstallMode.ON_NEXT_SUSPEND`, and can be useful
|
|
373
392
|
* for getting your update in front of end users sooner, without being too obtrusive. Defaults to `0`, which has the effect of applying
|
|
374
393
|
* the update immediately after a resume or unless the app suspension is long enough to not matter, regardless how long it was in the background.
|
|
394
|
+
* A `minimumBackgroundDuration` set on the release history entry of the update being installed takes precedence over this option.
|
|
375
395
|
*/
|
|
376
396
|
minimumBackgroundDuration?: number;
|
|
377
397
|
|