@bravemobile/react-native-code-push 13.1.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 +18 -1
- package/android/app/proguard-rules.pro +0 -7
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePush.java +3 -0
- package/android/app/src/main/java/com/microsoft/codepush/react/CodePushNativeModule.java +25 -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/cli/dist/utils/fsUtils.js +35 -9
- package/cli/package.json +5 -1
- package/cli/utils/fsUtils.ts +43 -9
- package/ios/CodePush/CodePush.h +8 -3
- package/ios/CodePush/CodePush.mm +61 -1
- package/package.json +14 -6
- 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
|
@@ -394,7 +394,18 @@ module.exports = Config;
|
|
|
394
394
|
- Modifying an existing release history with the `update-history` command.
|
|
395
395
|
|
|
396
396
|
|
|
397
|
-
**(2)
|
|
397
|
+
**(2) A `code-push.config.ts` (TypeScript) file needs a loader installed.**
|
|
398
|
+
|
|
399
|
+
Install `tsx`, which requires no further setup:
|
|
400
|
+
|
|
401
|
+
```bash
|
|
402
|
+
npm install --save-dev tsx
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
`ts-node` also keeps working, and is used when `tsx` is not installed. Note that `ts-node` is no
|
|
406
|
+
longer maintained, so support for it is **deprecated and will be removed in a future major
|
|
407
|
+
version** — please migrate to `tsx`. Until then, `ts-node` needs the following `tsconfig.json`
|
|
408
|
+
setup:
|
|
398
409
|
|
|
399
410
|
```diff
|
|
400
411
|
{
|
|
@@ -414,6 +425,8 @@ module.exports = Config;
|
|
|
414
425
|
|
|
415
426
|
```
|
|
416
427
|
|
|
428
|
+
A `code-push.config.js` (JavaScript) file needs neither.
|
|
429
|
+
|
|
417
430
|
|
|
418
431
|
### 6. Diff Updates (Optional)
|
|
419
432
|
|
|
@@ -488,6 +501,8 @@ npx code-push release --framework expo --binary-version 1.0.0 --app-version 1.0.
|
|
|
488
501
|
> `--app-version` should be greater than `--binary-version` (SemVer comparison).
|
|
489
502
|
|
|
490
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.
|
|
491
506
|
|
|
492
507
|
#### `update-history`
|
|
493
508
|
|
|
@@ -496,6 +511,8 @@ Update the release history for a specific CodePush update.
|
|
|
496
511
|
- Use the `--mandatory` option to make the update as mandatory or optional.
|
|
497
512
|
- Use the `--rollout` option to change the rollout percentage of the update. (0~100, inclusive)
|
|
498
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.
|
|
499
516
|
|
|
500
517
|
**Example:**
|
|
501
518
|
- Rollback the CodePush update `1.0.1` (targeting the binary app version `1.0.0`).
|
|
@@ -21,10 +21,6 @@
|
|
|
21
21
|
private final ** mBundleLoader;
|
|
22
22
|
}
|
|
23
23
|
# bridgeless
|
|
24
|
-
-keepclassmembers class com.facebook.react.defaults.DefaultReactHostDelegate {
|
|
25
|
-
private ** jsBundleLoader;
|
|
26
|
-
}
|
|
27
|
-
# bridgeless
|
|
28
24
|
-keepclassmembers class * extends com.facebook.react.runtime.ReactHostDelegate {
|
|
29
25
|
private ** jsBundleLoader;
|
|
30
26
|
private ** _jsBundleLoader;
|
|
@@ -35,9 +31,6 @@
|
|
|
35
31
|
private final ** reactHostDelegate; # RN 0.81+
|
|
36
32
|
}
|
|
37
33
|
|
|
38
|
-
# Can't find referenced class org.bouncycastle.**
|
|
39
|
-
-dontwarn com.nimbusds.jose.**
|
|
40
|
-
|
|
41
34
|
# The binary patch applier is found by name from the native library.
|
|
42
35
|
-keepclasseswithmembernames class com.microsoft.codepush.react.HDiffPatchNative {
|
|
43
36
|
native <methods>;
|
|
@@ -51,6 +51,9 @@ public class CodePush extends TurboReactPackage {
|
|
|
51
51
|
return mServerUrl;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Entry point used by React Native autolinking to create and retain the package instance.
|
|
56
|
+
*/
|
|
54
57
|
public static synchronized CodePush getInstance(Context context, boolean isDebugMode) {
|
|
55
58
|
if (mCurrentInstance == null) {
|
|
56
59
|
mCurrentInstance = new CodePush(context, isDebugMode);
|
|
@@ -59,6 +59,15 @@ public class CodePushNativeModule extends NativeCodePushSpec {
|
|
|
59
59
|
private boolean _restartInProgress = false;
|
|
60
60
|
private ArrayList<Boolean> _restartQueue = new ArrayList<>();
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Whether the React Native instance this module was created for is still the one running.
|
|
64
|
+
*
|
|
65
|
+
* A reload replaces the instance while work this module started can still be in flight,
|
|
66
|
+
* and a frame callback posted to the process-wide choreographer outlives the executor
|
|
67
|
+
* this module shuts down. Read from whichever thread that work is on, so it is volatile.
|
|
68
|
+
*/
|
|
69
|
+
private volatile boolean mGenerationAlive = true;
|
|
70
|
+
|
|
62
71
|
public CodePushNativeModule(ReactApplicationContext reactContext, CodePush codePush, CodePushUpdateManager codePushUpdateManager, CodePushTelemetryManager codePushTelemetryManager, SettingsManager settingsManager) {
|
|
63
72
|
super(reactContext);
|
|
64
73
|
|
|
@@ -85,6 +94,7 @@ public class CodePushNativeModule extends NativeCodePushSpec {
|
|
|
85
94
|
|
|
86
95
|
@Override
|
|
87
96
|
public void invalidate() {
|
|
97
|
+
mGenerationAlive = false;
|
|
88
98
|
clearLifecycleEventListener();
|
|
89
99
|
mBackgroundExecutor.shutdownNow();
|
|
90
100
|
super.invalidate();
|
|
@@ -264,6 +274,14 @@ public class CodePushNativeModule extends NativeCodePushSpec {
|
|
|
264
274
|
}
|
|
265
275
|
|
|
266
276
|
private void emitDownloadProgressEvent(DownloadProgress downloadProgress) {
|
|
277
|
+
// Every progress event arrives here, including the ones a frame callback delivers
|
|
278
|
+
// after this module was invalidated: shutting the executor down does not cancel a
|
|
279
|
+
// callback already posted to the choreographer, and the context it would emit
|
|
280
|
+
// through has been destroyed by then.
|
|
281
|
+
if (!mGenerationAlive) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
267
285
|
if (mEventEmitterCallback != null) {
|
|
268
286
|
emitOnDownloadProgress(downloadProgress.createWritableMap());
|
|
269
287
|
return;
|
|
@@ -275,6 +293,13 @@ public class CodePushNativeModule extends NativeCodePushSpec {
|
|
|
275
293
|
}
|
|
276
294
|
|
|
277
295
|
private void restartAppInternal(boolean onlyIfUpdateIsPending) {
|
|
296
|
+
// A restart requested by an instance that has been replaced would reload the one
|
|
297
|
+
// that replaced it, which nobody asked for.
|
|
298
|
+
if (!mGenerationAlive) {
|
|
299
|
+
CodePushUtils.log("Ignoring a restart requested by a React Native instance that is no longer running");
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
278
303
|
if (this._restartInProgress) {
|
|
279
304
|
CodePushUtils.log("Restart request queued until the current restart is completed");
|
|
280
305
|
this._restartQueue.add(onlyIfUpdateIsPending);
|
|
@@ -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
|
}
|