Skip to content

Commit 736e5b9

Browse files
committed
Merge branch 'master' into v12
# Conflicts: # package-lock.json # package.json
2 parents c1a8644 + 4ab00e9 commit 736e5b9

File tree

9 files changed

+231
-50
lines changed

9 files changed

+231
-50
lines changed

CodePush.js

Lines changed: 113 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,54 @@ const PackageMixins = require("./package-mixins")(NativeCodePush);
99

1010
const DEPLOYMENT_KEY = 'deprecated_deployment_key';
1111

12+
/**
13+
* @param deviceId {string}
14+
* @returns {number}
15+
*/
16+
function hashDeviceId(deviceId) {
17+
let hash = 0;
18+
for (let i = 0; i < deviceId.length; i++) {
19+
hash = ((hash << 5) - hash) + deviceId.charCodeAt(i);
20+
hash |= 0; // Convert to 32bit int
21+
}
22+
return Math.abs(hash);
23+
}
24+
25+
/**
26+
* @param clientId {string}
27+
* @param packageHash {string}
28+
* @returns {number}
29+
*/
30+
function getBucket(clientId, packageHash) {
31+
const hash = hashDeviceId(`${clientId ?? ''}_${packageHash ?? ''}`);
32+
return (Math.abs(hash) % 100);
33+
}
34+
35+
/**
36+
* Note that the `clientUniqueId` value may not guarantee the same value if the app is deleted and re-installed.
37+
* In other words, if a user re-installs the app, the result of this function may change.
38+
* @returns {Promise<boolean>}
39+
*/
40+
async function decideLatestReleaseIsInRollout(versioning, clientId, onRolloutSkipped) {
41+
const [latestVersion, latestReleaseInfo] = versioning.findLatestRelease();
42+
43+
if (latestReleaseInfo.rollout === undefined || latestReleaseInfo.rollout >= 100) {
44+
return true;
45+
}
46+
47+
const bucket = getBucket(clientId, latestReleaseInfo.packageHash);
48+
const inRollout = bucket < latestReleaseInfo.rollout;
49+
50+
log(`Bucket: ${bucket}, rollout: ${latestReleaseInfo.rollout}${inRollout ? 'IN' : 'OUT'}`);
51+
52+
if (!inRollout) {
53+
log(`Skipping update due to rollout. Bucket ${bucket} is not smaller than rollout range ${latestReleaseInfo.rollout}.`);
54+
onRolloutSkipped?.(latestVersion);
55+
}
56+
57+
return inRollout;
58+
}
59+
1260
async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
1361
/*
1462
* Before we ask the server if an update exists, we
@@ -42,6 +90,9 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
4290
}
4391
}
4492

93+
/**
94+
* @type {RemotePackage|null|undefined}
95+
*/
4596
const update = await (async () => {
4697
try {
4798
const updateRequest = {
@@ -58,8 +109,8 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
58109
*/
59110
const updateChecker = sharedCodePushOptions.updateChecker;
60111
if (updateChecker) {
112+
// We do not provide rollout functionality. This could be implemented in the `updateChecker`.
61113
const { update_info } = await updateChecker(updateRequest);
62-
63114
return mapToRemotePackageMetadata(update_info);
64115
} else {
65116
/**
@@ -77,6 +128,9 @@ async function checkForUpdate(handleBinaryVersionMismatchCallback = null) {
77128

78129
const versioning = new SemverVersioning(releaseHistory);
79130

131+
const isInRollout = await decideLatestReleaseIsInRollout(versioning, nativeConfig.clientUniqueId, sharedCodePushOptions?.onRolloutSkipped);
132+
versioning.setIsLatestReleaseInRollout(isInRollout);
133+
80134
const shouldRollbackToBinary = versioning.shouldRollbackToBinary(runtimeVersion)
81135
if (shouldRollbackToBinary) {
82136
// Reset to latest major version and restart
@@ -217,7 +271,7 @@ async function getCurrentPackage() {
217271
async function getUpdateMetadata(updateState) {
218272
let updateMetadata = await NativeCodePush.getUpdateMetadata(updateState || CodePush.UpdateState.RUNNING);
219273
if (updateMetadata) {
220-
updateMetadata = {...PackageMixins.local, ...updateMetadata};
274+
updateMetadata = { ...PackageMixins.local, ...updateMetadata };
221275
updateMetadata.failedInstall = await NativeCodePush.isFailedUpdate(updateMetadata.packageHash);
222276
updateMetadata.isFirstRun = await NativeCodePush.isFirstRun(updateMetadata.packageHash);
223277
}
@@ -424,47 +478,47 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
424478
mandatoryInstallMode: CodePush.InstallMode.IMMEDIATE,
425479
minimumBackgroundDuration: 0,
426480
updateDialog: null,
427-
...options
481+
...options,
428482
};
429483

430484
syncStatusChangeCallback = typeof syncStatusChangeCallback === "function"
431485
? syncStatusChangeCallback
432486
: (syncStatus) => {
433-
switch(syncStatus) {
434-
case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
435-
log("Checking for update.");
436-
break;
437-
case CodePush.SyncStatus.AWAITING_USER_ACTION:
438-
log("Awaiting user action.");
439-
break;
440-
case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
441-
log("Downloading package.");
442-
break;
443-
case CodePush.SyncStatus.INSTALLING_UPDATE:
444-
log("Installing update.");
445-
break;
446-
case CodePush.SyncStatus.UP_TO_DATE:
447-
log("App is up to date.");
448-
break;
449-
case CodePush.SyncStatus.UPDATE_IGNORED:
450-
log("User cancelled the update.");
451-
break;
452-
case CodePush.SyncStatus.UPDATE_INSTALLED:
453-
if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESTART) {
454-
log("Update is installed and will be run on the next app restart.");
455-
} else if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESUME) {
456-
if (syncOptions.minimumBackgroundDuration > 0) {
457-
log(`Update is installed and will be run after the app has been in the background for at least ${syncOptions.minimumBackgroundDuration} seconds.`);
458-
} else {
459-
log("Update is installed and will be run when the app next resumes.");
460-
}
487+
switch (syncStatus) {
488+
case CodePush.SyncStatus.CHECKING_FOR_UPDATE:
489+
log("Checking for update.");
490+
break;
491+
case CodePush.SyncStatus.AWAITING_USER_ACTION:
492+
log("Awaiting user action.");
493+
break;
494+
case CodePush.SyncStatus.DOWNLOADING_PACKAGE:
495+
log("Downloading package.");
496+
break;
497+
case CodePush.SyncStatus.INSTALLING_UPDATE:
498+
log("Installing update.");
499+
break;
500+
case CodePush.SyncStatus.UP_TO_DATE:
501+
log("App is up to date.");
502+
break;
503+
case CodePush.SyncStatus.UPDATE_IGNORED:
504+
log("User cancelled the update.");
505+
break;
506+
case CodePush.SyncStatus.UPDATE_INSTALLED:
507+
if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESTART) {
508+
log("Update is installed and will be run on the next app restart.");
509+
} else if (resolvedInstallMode == CodePush.InstallMode.ON_NEXT_RESUME) {
510+
if (syncOptions.minimumBackgroundDuration > 0) {
511+
log(`Update is installed and will be run after the app has been in the background for at least ${syncOptions.minimumBackgroundDuration} seconds.`);
512+
} else {
513+
log("Update is installed and will be run when the app next resumes.");
461514
}
462-
break;
463-
case CodePush.SyncStatus.UNKNOWN_ERROR:
464-
log("An unknown error occurred.");
465-
break;
466-
}
467-
};
515+
}
516+
break;
517+
case CodePush.SyncStatus.UNKNOWN_ERROR:
518+
log("An unknown error occurred.");
519+
break;
520+
}
521+
};
468522

469523
let remotePackageLabel;
470524
try {
@@ -497,7 +551,7 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
497551

498552
if (!remotePackage || updateShouldBeIgnored) {
499553
if (updateShouldBeIgnored) {
500-
log("An update is available, but it is being ignored due to having been previously rolled back.");
554+
log("An update is available, but it is being ignored due to having been previously rolled back.");
501555
}
502556

503557
const currentPackage = await CodePush.getCurrentPackage();
@@ -536,18 +590,18 @@ async function syncInternal(options = {}, syncStatusChangeCallback, downloadProg
536590
onPress: () => {
537591
syncStatusChangeCallback(CodePush.SyncStatus.UPDATE_IGNORED);
538592
resolve(CodePush.SyncStatus.UPDATE_IGNORED);
539-
}
593+
},
540594
});
541595
}
542596

543597
// Since the install button should be placed to the
544598
// right of any other button, add it last
545599
dialogButtons.push({
546600
text: installButtonText,
547-
onPress:() => {
601+
onPress: () => {
548602
doDownloadAndInstall()
549603
.then(resolve, reject);
550-
}
604+
},
551605
})
552606

553607
// If the update has a description, and the developer
@@ -609,6 +663,9 @@ let CodePush;
609663
*
610664
* onSyncError: (label: string, error: Error) => void | undefined,
611665
* setOnSyncError(onSyncErrorFunction: (label: string, error: Error) => void | undefined): void,
666+
*
667+
* onRolloutSkipped: (label: string, error: Error) => void | undefined,
668+
* setOnRolloutSkipped(onRolloutSkippedFunction: (label: string, error: Error) => void | undefined): void,
612669
* }}
613670
*/
614671
const sharedCodePushOptions = {
@@ -653,6 +710,12 @@ const sharedCodePushOptions = {
653710
if (typeof onSyncErrorFunction !== 'function') throw new Error('Please pass a function to onSyncError');
654711
this.onSyncError = onSyncErrorFunction;
655712
},
713+
onRolloutSkipped: undefined,
714+
setOnRolloutSkipped(onRolloutSkippedFunction) {
715+
if (!onRolloutSkippedFunction) return;
716+
if (typeof onRolloutSkippedFunction !== 'function') throw new Error('Please pass a function to onRolloutSkipped');
717+
this.onRolloutSkipped = onRolloutSkippedFunction;
718+
},
656719
}
657720

658721
function codePushify(options = {}) {
@@ -671,7 +734,7 @@ function codePushify(options = {}) {
671734
throw new Error(
672735
`Unable to find the "Component" class, please either:
673736
1. Upgrade to a newer version of React Native that supports it, or
674-
2. Call the codePush.sync API in your component instead of using the @codePush decorator`
737+
2. Call the codePush.sync API in your component instead of using the @codePush decorator`,
675738
);
676739
}
677740

@@ -688,6 +751,7 @@ function codePushify(options = {}) {
688751
sharedCodePushOptions.setOnDownloadStart(options.onDownloadStart);
689752
sharedCodePushOptions.setOnDownloadSuccess(options.onDownloadSuccess);
690753
sharedCodePushOptions.setOnSyncError(options.onSyncError);
754+
sharedCodePushOptions.setOnRolloutSkipped(options.onRolloutSkipped);
691755

692756
const decorator = (RootComponent) => {
693757
class CodePushComponent extends React.Component {
@@ -730,7 +794,7 @@ function codePushify(options = {}) {
730794
}
731795

732796
render() {
733-
const props = {...this.props};
797+
const props = { ...this.props };
734798

735799
// We can set ref property on class components only (not stateless)
736800
// Check it by render method
@@ -777,7 +841,7 @@ if (NativeCodePush) {
777841
IMMEDIATE: NativeCodePush.codePushInstallModeImmediate, // Restart the app immediately
778842
ON_NEXT_RESTART: NativeCodePush.codePushInstallModeOnNextRestart, // Don't artificially restart the app. Allow the update to be "picked up" on the next app restart
779843
ON_NEXT_RESUME: NativeCodePush.codePushInstallModeOnNextResume, // Restart the app the next time it is resumed from the background
780-
ON_NEXT_SUSPEND: NativeCodePush.codePushInstallModeOnNextSuspend // Restart the app _while_ it is in the background,
844+
ON_NEXT_SUSPEND: NativeCodePush.codePushInstallModeOnNextSuspend, // Restart the app _while_ it is in the background,
781845
// but only after it has been in the background for "minimumBackgroundDuration" seconds (0 by default),
782846
// so that user context isn't lost unless the app suspension is long enough to not matter
783847
},
@@ -790,17 +854,17 @@ if (NativeCodePush) {
790854
CHECKING_FOR_UPDATE: 5,
791855
AWAITING_USER_ACTION: 6,
792856
DOWNLOADING_PACKAGE: 7,
793-
INSTALLING_UPDATE: 8
857+
INSTALLING_UPDATE: 8,
794858
},
795859
CheckFrequency: {
796860
ON_APP_START: 0,
797861
ON_APP_RESUME: 1,
798-
MANUAL: 2
862+
MANUAL: 2,
799863
},
800864
UpdateState: {
801865
RUNNING: NativeCodePush.codePushUpdateStateRunning,
802866
PENDING: NativeCodePush.codePushUpdateStatePending,
803-
LATEST: NativeCodePush.codePushUpdateStateLatest
867+
LATEST: NativeCodePush.codePushUpdateStateLatest,
804868
},
805869
DeploymentStatus: {
806870
FAILED: "DeploymentFailed",
@@ -814,11 +878,11 @@ if (NativeCodePush) {
814878
optionalIgnoreButtonLabel: "Ignore",
815879
optionalInstallButtonLabel: "Install",
816880
optionalUpdateMessage: "An update is available. Would you like to install it?",
817-
title: "Update available"
881+
title: "Update available",
818882
},
819883
DEFAULT_ROLLBACK_RETRY_OPTIONS: {
820884
delayInHours: 24,
821-
maxRetryAttempts: 1
885+
maxRetryAttempts: 1,
822886
},
823887
});
824888
} else {

cli/commands/releaseCommand/addToReleaseHistory.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const fs = require("fs");
2525
* @param identifier {string?}
2626
* @param mandatory {boolean?}
2727
* @param enable {boolean?}
28+
* @param rollout {number?}
2829
* @returns {Promise<void>}
2930
*/
3031
async function addToReleaseHistory(
@@ -38,6 +39,7 @@ async function addToReleaseHistory(
3839
identifier,
3940
mandatory,
4041
enable,
42+
rollout
4143
) {
4244
const releaseHistory = await getReleaseHistory(binaryVersion, platform, identifier);
4345

@@ -54,6 +56,10 @@ async function addToReleaseHistory(
5456
mandatory: mandatory,
5557
downloadUrl: bundleDownloadUrl,
5658
packageHash: packageHash,
59+
};
60+
61+
if (typeof rollout === 'number') {
62+
newReleaseHistory[appVersion].rollout = rollout;
5763
}
5864

5965
try {

cli/commands/releaseCommand/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ program.command('release')
1616
.option('-j, --js-bundle-name <string>', 'JS bundle file name (default-ios: "main.jsbundle" / default-android: "index.android.bundle")')
1717
.option('-m, --mandatory <bool>', 'make the release to be mandatory', parseBoolean, false)
1818
.option('--enable <bool>', 'make the release to be enabled', parseBoolean, true)
19+
.option('--rollout <number>', 'rollout percentage (0-100)', parseFloat)
1920
.option('--skip-bundle <bool>', 'skip bundle process', parseBoolean, false)
2021
.option('--skip-cleanup <bool>', 'skip cleanup process', parseBoolean, false)
2122
.option('--output-bundle-dir <string>', 'name of directory containing the bundle file created by the "bundle" command', OUTPUT_BUNDLE_DIR)
@@ -32,6 +33,7 @@ program.command('release')
3233
* @param {string} options.bundleName
3334
* @param {string} options.mandatory
3435
* @param {string} options.enable
36+
* @param {number} options.rollout
3537
* @param {string} options.skipBundle
3638
* @param {string} options.skipCleanup
3739
* @param {string} options.outputBundleDir
@@ -40,6 +42,11 @@ program.command('release')
4042
.action(async (options) => {
4143
const config = findAndReadConfigFile(process.cwd(), options.config);
4244

45+
if (typeof options.rollout === 'number' && (options.rollout < 0 || options.rollout > 100)) {
46+
console.error('Rollout percentage number must be between 0 and 100 (inclusive).');
47+
process.exit(1);
48+
}
49+
4350
await release(
4451
config.bundleUploader,
4552
config.getReleaseHistory,
@@ -54,6 +61,7 @@ program.command('release')
5461
options.bundleName,
5562
options.mandatory,
5663
options.enable,
64+
options.rollout,
5765
options.skipBundle,
5866
options.skipCleanup,
5967
`${options.outputPath}/${options.outputBundleDir}`,

cli/commands/releaseCommand/release.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const { addToReleaseHistory } = require("./addToReleaseHistory");
3333
* @param jsBundleName {string}
3434
* @param mandatory {boolean}
3535
* @param enable {boolean}
36+
* @param rollout {number}
3637
* @param skipBundle {boolean}
3738
* @param skipCleanup {boolean}
3839
* @param bundleDirectory {string}
@@ -52,6 +53,7 @@ async function release(
5253
jsBundleName,
5354
mandatory,
5455
enable,
56+
rollout,
5557
skipBundle,
5658
skipCleanup,
5759
bundleDirectory,
@@ -82,6 +84,7 @@ async function release(
8284
identifier,
8385
mandatory,
8486
enable,
87+
rollout,
8588
)
8689

8790
if (!skipCleanup) {

0 commit comments

Comments
 (0)