From 72464c94c4816eaf1a83e79a6c52138f9397adf0 Mon Sep 17 00:00:00 2001 From: Dima Date: Fri, 11 Oct 2024 18:12:35 +0700 Subject: [PATCH 01/21] Remove update notifier This PR removes the update check, which logs about newer version for code-push-cli. Since this CLI version is not linked to the deprecated [code-push-cli](https://www.npmjs.com/package/code-push-cli) package and no releases are planned, we don't need the update check. --- cli/package-lock.json | 12 ------------ cli/package.json | 1 - cli/script/command-parser.ts | 9 --------- 3 files changed, 22 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 2f8aa9bf1..a397cbc54 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -25,7 +25,6 @@ "rimraf": "^2.5.1", "semver": "^7.5.3", "simctl": "^2.0.3", - "simple-update-notifier": "^2.0.0", "slash": "1.0.0", "superagent": "^8.0.9", "which": "^1.2.7", @@ -3388,17 +3387,6 @@ "tail": "^0.4.0" } }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/sinon": { "version": "15.1.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.1.2.tgz", diff --git a/cli/package.json b/cli/package.json index e1ab92a2a..580c988a2 100644 --- a/cli/package.json +++ b/cli/package.json @@ -36,7 +36,6 @@ "rimraf": "^2.5.1", "semver": "^7.5.3", "simctl": "^2.0.3", - "simple-update-notifier": "^2.0.0", "slash": "1.0.0", "superagent": "^8.0.9", "which": "^1.2.7", diff --git a/cli/script/command-parser.ts b/cli/script/command-parser.ts index aaa87172d..4afaadb4d 100644 --- a/cli/script/command-parser.ts +++ b/cli/script/command-parser.ts @@ -4,7 +4,6 @@ import * as yargs from "yargs"; import * as cli from "../script/types/cli"; import * as chalk from "chalk"; -const updateNotifier = require("simple-update-notifier"); import backslash = require("backslash"); import parseDuration = require("parse-duration"); @@ -28,7 +27,6 @@ export function showHelp(showRootDescription?: boolean): void { console.log(chalk.cyan("======================================")); console.log(""); console.log("CodePush is a service that enables you to deploy mobile app updates directly to your users' devices.\n"); - updateCheck(); } yargs.showHelp(); @@ -36,13 +34,6 @@ export function showHelp(showRootDescription?: boolean): void { } } -function updateCheck(): void { - const notifier = updateNotifier({ pkg: packageJson }); - if (notifier.update) { - notifier.notify(); - } -} - function accessKeyAdd(commandName: string, yargs: yargs.Argv): void { isValidCommand = true; yargs From f9114eea6bb9c8ffa1d7011f1cfd592727a11d3b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:33:09 +0200 Subject: [PATCH 02/21] Bump cookie and express in /cli (#18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [cookie](https://github.com/jshttp/cookie) to 0.7.1 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `cookie` from 0.6.0 to 0.7.1
Release notes

Sourced from cookie's releases.

0.7.1

Fixed

  • Allow leading dot for domain (#174)
    • Although not permitted in the spec, some users expect this to work and user agents ignore the leading dot according to spec
  • Add fast path for serialize without options, use obj.hasOwnProperty when parsing (#172)

https://github.com/jshttp/cookie/compare/v0.7.0...v0.7.1

0.7.0

https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.0

Commits
Maintainer changes

This version was pushed to npm by blakeembrey, a new releaser for cookie since your current version.


Updates `express` from 4.21.0 to 4.21.1
Release notes

Sourced from express's releases.

4.21.1

What's Changed

Full Changelog: https://github.com/expressjs/express/compare/4.21.0...4.21.1

Changelog

Sourced from express's changelog.

4.21.1 / 2024-10-08

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/code-push-server/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- cli/package-lock.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index a397cbc54..2a51ee275 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1258,9 +1258,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, "engines": { "node": ">= 0.6" @@ -1735,9 +1735,9 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dev": true, "dependencies": { "accepts": "~1.3.8", @@ -1745,7 +1745,7 @@ "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", From 84d2d100e9b78b3fff96289b8e22b6976f8cfadd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 11:33:43 +0200 Subject: [PATCH 03/21] Bump cookie and express in /api (#17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [cookie](https://github.com/jshttp/cookie) to 0.7.1 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `cookie` from 0.6.0 to 0.7.1
Release notes

Sourced from cookie's releases.

0.7.1

Fixed

  • Allow leading dot for domain (#174)
    • Although not permitted in the spec, some users expect this to work and user agents ignore the leading dot according to spec
  • Add fast path for serialize without options, use obj.hasOwnProperty when parsing (#172)

https://github.com/jshttp/cookie/compare/v0.7.0...v0.7.1

0.7.0

https://github.com/jshttp/cookie/compare/v0.6.0...v0.7.0

Commits
Maintainer changes

This version was pushed to npm by blakeembrey, a new releaser for cookie since your current version.


Updates `express` from 4.21.0 to 4.21.1
Release notes

Sourced from express's releases.

4.21.1

What's Changed

Full Changelog: https://github.com/expressjs/express/compare/4.21.0...4.21.1

Changelog

Sourced from express's changelog.

4.21.1 / 2024-10-08

Commits

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/code-push-server/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/package-lock.json | 16 ++++++++-------- api/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index 4575bed7e..df50d9dc1 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -18,7 +18,7 @@ "cookie-session": "^2.0.0", "ejs": "^3.1.10", "email-validator": "1.0.3", - "express": "^4.19.2", + "express": "^4.21.1", "express-domain-middleware": "0.1.0", "express-rate-limit": "^7.4.0", "multer": "^1.4.5-lts.1", @@ -2031,9 +2031,9 @@ } }, "node_modules/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "engines": { "node": ">= 0.6" } @@ -2671,16 +2671,16 @@ } }, "node_modules/express": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", - "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", + "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", - "cookie": "0.6.0", + "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", diff --git a/api/package.json b/api/package.json index 549e859e3..8adc3b7f4 100644 --- a/api/package.json +++ b/api/package.json @@ -32,7 +32,7 @@ "cookie-session": "^2.0.0", "ejs": "^3.1.10", "email-validator": "1.0.3", - "express": "^4.19.2", + "express": "^4.21.1", "express-domain-middleware": "0.1.0", "express-rate-limit": "^7.4.0", "multer": "^1.4.5-lts.1", From 7e9503e473f7bec13fa510828a227722b7a45884 Mon Sep 17 00:00:00 2001 From: Dima Date: Thu, 17 Oct 2024 16:24:31 +0700 Subject: [PATCH 04/21] [CLI] Support Hermes (#5) This PR adds support of Hermes for the code-push cli. It will try checking if hermes is enabled in the build.gradle or Podfile and run the hermes compiler if it is enabled. --- cli/README.md | 21 +++ cli/script/command-executor.ts | 32 +++- cli/script/command-parser.ts | 25 +++ cli/script/react-native-utils.ts | 286 +++++++++++++++++++++++++++++++ cli/script/types/cli.ts | 3 + 5 files changed, 359 insertions(+), 8 deletions(-) create mode 100644 cli/script/react-native-utils.ts diff --git a/cli/README.md b/cli/README.md index b95bd6bab..c281361d8 100644 --- a/cli/README.md +++ b/cli/README.md @@ -392,6 +392,9 @@ code-push-standalone release-react [--sourcemapOutput ] [--targetBinaryVersion ] [--rollout ] +[--useHermes ] +[--podFile ] +[--extraHermesFlags ] ``` The `release-react` command is a React Native-specific version of the "vanilla" [`release`](#releasing-app-updates) command, which supports all of the same parameters (e.g. `--mandatory`, `--description`), yet simplifies the process of releasing updates by performing the following additional behavior: @@ -521,6 +524,24 @@ This specifies the relative path to where the assets, JS bundle and sourcemap fi _NOTE: This parameter can be set using either --outputDir or -o_ +#### Use Hermes parameter + +This parameter enforces the use of the Hermes compiler. If not specified, the automatic checks will be performed, inspecting the `build.gradle` and `Podfile` for the Hermes flag. + +_NOTE: This parameter can be set using either --hermesEnabled or -h_ + +#### Podfile parameter (iOS only) + +The Podfile path will be used for Hermes automatic check. Not used if `--useHermes` is specified. + +_NOTE: This parameter can be set using either --podfile or -pod_ + +#### Extra hermes flags parameter + +Hermes flags which will be passed to Hermes compiler. + +_NOTE: This parameter can be set using either --extraHermesFlags or -hf_ + ## Debugging CodePush Integration Once you've released an update, React Native plugin has been integrated into your app, it can be helpful to diagnose how the plugin is behaving, especially if you run into an issue and want to understand why. In order to debug the CodePush update discovery experience, you can run the following command in order to easily view the diagnostic logs produced by the CodePush plugin within your app: diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index 84377f90e..1544f81ad 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -36,6 +36,12 @@ import { Session, UpdateMetrics, } from "../script/types"; +import { + fileDoesNotExistOrIsDirectory, + getAndroidHermesEnabled, + getiOSHermesEnabled, + runHermesEmitBinaryCommand +} from "./react-native-utils"; const configFilePath: string = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".code-push.config"); const emailValidator = require("email-validator"); @@ -550,14 +556,6 @@ export function execute(command: cli.ICommand) { }); } -function fileDoesNotExistOrIsDirectory(filePath: string): boolean { - try { - return fs.lstatSync(filePath).isDirectory(); - } catch (error) { - return true; - } -} - function getTotalActiveFromDeploymentMetrics(metrics: DeploymentMetrics): number { let totalActive = 0; Object.keys(metrics).forEach((label: string) => { @@ -1307,6 +1305,24 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise = command.sourcemapOutput ) ) + .then(async () => { + const isHermesEnabled = + command.useHermes || + (platform === "android" && (await getAndroidHermesEnabled(command.gradleFile))) || // Check if we have to run hermes to compile JS to Byte Code if Hermes is enabled in build.gradle and we're releasing an Android build + (platform === "ios" && (await getiOSHermesEnabled(command.podFile))); // Check if we have to run hermes to compile JS to Byte Code if Hermes is enabled in Podfile and we're releasing an iOS build + + if (isHermesEnabled) { + log(chalk.cyan("\nRunning hermes compiler...\n")); + await runHermesEmitBinaryCommand( + bundleName, + outputFolder, + command.sourcemapOutput, + command.extraHermesFlags, + command.gradleFile + ); + } + + }) .then(() => { log(chalk.cyan("\nReleasing update contents to CodePush:\n")); return release(releaseCommand); diff --git a/cli/script/command-parser.ts b/cli/script/command-parser.ts index 4afaadb4d..c5d0cd094 100644 --- a/cli/script/command-parser.ts +++ b/cli/script/command-parser.ts @@ -775,6 +775,28 @@ yargs "Path to where the bundle and sourcemap should be written. If omitted, a bundle and sourcemap will not be written.", type: "string", }) + .option("useHermes", { + alias: "h", + default: false, + demand: false, + description: "Enable hermes and bypass automatic checks", + type: "boolean", + }) + .option("podFile", { + alias: "pod", + default: null, + demand: false, + description: "Path to the cocopods config file (iOS only).", + type: "string", + }) + .option("extraHermesFlags", { + alias: "hf", + default: [], + demand: false, + description: + "Flags that get passed to Hermes, JavaScript to bytecode compiler. Can be specified multiple times.", + type: "array", + }) .check((argv: any, aliases: { [aliases: string]: string }): any => { return checkValidReleaseOptions(argv); }); @@ -1169,6 +1191,9 @@ export function createCommand(): cli.ICommand { releaseReactCommand.rollout = getRolloutValue(argv["rollout"] as any); releaseReactCommand.sourcemapOutput = argv["sourcemapOutput"] as any; releaseReactCommand.outputDir = argv["outputDir"] as any; + releaseReactCommand.useHermes = argv["useHermes"] as any; + releaseReactCommand.extraHermesFlags = argv["extraHermesFlags"] as any; + releaseReactCommand.podFile = argv["podFile"] as any; } break; diff --git a/cli/script/react-native-utils.ts b/cli/script/react-native-utils.ts new file mode 100644 index 000000000..d22dfa387 --- /dev/null +++ b/cli/script/react-native-utils.ts @@ -0,0 +1,286 @@ +import * as fs from "fs"; +import * as chalk from "chalk"; +import * as path from "path"; +import * as childProcess from "child_process"; +import { coerce, compare } from "semver"; + +const g2js = require("gradle-to-js/lib/parser"); + +export async function runHermesEmitBinaryCommand( + bundleName: string, + outputFolder: string, + sourcemapOutput: string, + extraHermesFlags: string[], + gradleFile: string +): Promise { + const hermesArgs: string[] = []; + const envNodeArgs: string = process.env.CODE_PUSH_NODE_ARGS; + + if (typeof envNodeArgs !== "undefined") { + Array.prototype.push.apply(hermesArgs, envNodeArgs.trim().split(/\s+/)); + } + + Array.prototype.push.apply(hermesArgs, [ + "-emit-binary", + "-out", + path.join(outputFolder, bundleName + ".hbc"), + path.join(outputFolder, bundleName), + ...extraHermesFlags, + ]); + + if (sourcemapOutput) { + hermesArgs.push("-output-source-map"); + } + + console.log(chalk.cyan("Converting JS bundle to byte code via Hermes, running command:\n")); + const hermesCommand = await getHermesCommand(gradleFile); + const hermesProcess = childProcess.spawn(hermesCommand, hermesArgs); + console.log(`${hermesCommand} ${hermesArgs.join(" ")}`); + + return new Promise((resolve, reject) => { + hermesProcess.stdout.on("data", (data: Buffer) => { + console.log(data.toString().trim()); + }); + + hermesProcess.stderr.on("data", (data: Buffer) => { + console.error(data.toString().trim()); + }); + + hermesProcess.on("close", (exitCode: number, signal: string) => { + if (exitCode !== 0) { + reject(new Error(`"hermes" command failed (exitCode=${exitCode}, signal=${signal}).`)); + } + // Copy HBC bundle to overwrite JS bundle + const source = path.join(outputFolder, bundleName + ".hbc"); + const destination = path.join(outputFolder, bundleName); + fs.copyFile(source, destination, (err) => { + if (err) { + console.error(err); + reject(new Error(`Copying file ${source} to ${destination} failed. "hermes" previously exited with code ${exitCode}.`)); + } + fs.unlink(source, (err) => { + if (err) { + console.error(err); + reject(err); + } + resolve(null as void); + }); + }); + }); + }).then(() => { + if (!sourcemapOutput) { + // skip source map compose if source map is not enabled + return; + } + + const composeSourceMapsPath = getComposeSourceMapsPath(); + if (!composeSourceMapsPath) { + throw new Error("react-native compose-source-maps.js scripts is not found"); + } + + const jsCompilerSourceMapFile = path.join(outputFolder, bundleName + ".hbc" + ".map"); + if (!fs.existsSync(jsCompilerSourceMapFile)) { + throw new Error(`sourcemap file ${jsCompilerSourceMapFile} is not found`); + } + + return new Promise((resolve, reject) => { + const composeSourceMapsArgs = [composeSourceMapsPath, sourcemapOutput, jsCompilerSourceMapFile, "-o", sourcemapOutput]; + + // https://github.com/facebook/react-native/blob/master/react.gradle#L211 + // https://github.com/facebook/react-native/blob/master/scripts/react-native-xcode.sh#L178 + // packager.sourcemap.map + hbc.sourcemap.map = sourcemap.map + const composeSourceMapsProcess = childProcess.spawn("node", composeSourceMapsArgs); + console.log(`${composeSourceMapsPath} ${composeSourceMapsArgs.join(" ")}`); + + composeSourceMapsProcess.stdout.on("data", (data: Buffer) => { + console.log(data.toString().trim()); + }); + + composeSourceMapsProcess.stderr.on("data", (data: Buffer) => { + console.error(data.toString().trim()); + }); + + composeSourceMapsProcess.on("close", (exitCode: number, signal: string) => { + if (exitCode !== 0) { + reject(new Error(`"compose-source-maps" command failed (exitCode=${exitCode}, signal=${signal}).`)); + } + + // Delete the HBC sourceMap, otherwise it will be included in 'code-push' bundle as well + fs.unlink(jsCompilerSourceMapFile, (err) => { + if (err) { + console.error(err); + reject(err); + } + + resolve(null); + }); + }); + }); + }); +} + +export function fileDoesNotExistOrIsDirectory(filePath: string): boolean { + try { + return fs.lstatSync(filePath).isDirectory(); + } catch (error) { + return true; + } +} + +function parseBuildGradleFile(gradleFile: string) { + let buildGradlePath: string = path.join("android", "app"); + if (gradleFile) { + buildGradlePath = gradleFile; + } + if (fs.lstatSync(buildGradlePath).isDirectory()) { + buildGradlePath = path.join(buildGradlePath, "build.gradle"); + } + + if (fileDoesNotExistOrIsDirectory(buildGradlePath)) { + throw new Error(`Unable to find gradle file "${buildGradlePath}".`); + } + + return g2js.parseFile(buildGradlePath).catch(() => { + throw new Error(`Unable to parse the "${buildGradlePath}" file. Please ensure it is a well-formed Gradle file.`); + }); +} + +async function getHermesCommandFromGradle(gradleFile: string): Promise { + const buildGradle: any = await parseBuildGradleFile(gradleFile); + const hermesCommandProperty: any = Array.from(buildGradle["project.ext.react"] || []).find((prop: string) => + prop.trim().startsWith("hermesCommand:") + ); + if (hermesCommandProperty) { + return hermesCommandProperty.replace("hermesCommand:", "").trim().slice(1, -1); + } else { + return ""; + } +} + +export function getAndroidHermesEnabled(gradleFile: string): boolean { + return parseBuildGradleFile(gradleFile).then((buildGradle: any) => { + return Array.from(buildGradle["project.ext.react"] || []).some((line: string) => /^enableHermes\s{0,}:\s{0,}true/.test(line)); + }); +} + +export function getiOSHermesEnabled(podFile: string): boolean { + let podPath = path.join("ios", "Podfile"); + if (podFile) { + podPath = podFile; + } + if (fileDoesNotExistOrIsDirectory(podPath)) { + throw new Error(`Unable to find Podfile file "${podPath}".`); + } + + try { + const podFileContents = fs.readFileSync(podPath).toString(); + return /([^#\n]*:?hermes_enabled(\s+|\n+)?(=>|:)(\s+|\n+)?true)/.test(podFileContents); + } catch (error) { + throw error; + } +} + +function getHermesOSBin(): string { + switch (process.platform) { + case "win32": + return "win64-bin"; + case "darwin": + return "osx-bin"; + case "freebsd": + case "linux": + case "sunos": + default: + return "linux64-bin"; + } +} + +function getHermesOSExe(): string { + const react63orAbove = compare(coerce(getReactNativeVersion()).version, "0.63.0") !== -1; + const hermesExecutableName = react63orAbove ? "hermesc" : "hermes"; + switch (process.platform) { + case "win32": + return hermesExecutableName + ".exe"; + default: + return hermesExecutableName; + } +} + +async function getHermesCommand(gradleFile: string): Promise { + const fileExists = (file: string): boolean => { + try { + return fs.statSync(file).isFile(); + } catch (e) { + return false; + } + }; + // Hermes is bundled with react-native since 0.69 + const bundledHermesEngine = path.join(getReactNativePackagePath(), "sdks", "hermesc", getHermesOSBin(), getHermesOSExe()); + if (fileExists(bundledHermesEngine)) { + return bundledHermesEngine; + } + + const gradleHermesCommand = await getHermesCommandFromGradle(gradleFile); + if (gradleHermesCommand) { + return path.join("android", "app", gradleHermesCommand.replace("%OS-BIN%", getHermesOSBin())); + } else { + // assume if hermes-engine exists it should be used instead of hermesvm + const hermesEngine = path.join("node_modules", "hermes-engine", getHermesOSBin(), getHermesOSExe()); + if (fileExists(hermesEngine)) { + return hermesEngine; + } + return path.join("node_modules", "hermesvm", getHermesOSBin(), "hermes"); + } +} + +function getComposeSourceMapsPath(): string { + // detect if compose-source-maps.js script exists + const composeSourceMaps = path.join(getReactNativePackagePath(), "scripts", "compose-source-maps.js"); + if (fs.existsSync(composeSourceMaps)) { + return composeSourceMaps; + } + return null; +} + +function getReactNativePackagePath(): string { + const result = childProcess.spawnSync("node", ["--print", "require.resolve('react-native/package.json')"]); + const packagePath = path.dirname(result.stdout.toString()); + if (result.status === 0 && directoryExistsSync(packagePath)) { + return packagePath; + } + + return path.join("node_modules", "react-native"); +} + +export function directoryExistsSync(dirname: string): boolean { + try { + return fs.statSync(dirname).isDirectory(); + } catch (err) { + if (err.code !== "ENOENT") { + throw err; + } + } + return false; +} + +export function getReactNativeVersion(): string { + let packageJsonFilename; + let projectPackageJson; + try { + packageJsonFilename = path.join(process.cwd(), "package.json"); + projectPackageJson = JSON.parse(fs.readFileSync(packageJsonFilename, "utf-8")); + } catch (error) { + throw new Error( + `Unable to find or read "package.json" in the CWD. The "release-react" command must be executed in a React Native project folder.` + ); + } + + const projectName: string = projectPackageJson.name; + if (!projectName) { + throw new Error(`The "package.json" file in the CWD does not have the "name" field set.`); + } + + return ( + (projectPackageJson.dependencies && projectPackageJson.dependencies["react-native"]) || + (projectPackageJson.devDependencies && projectPackageJson.devDependencies["react-native"]) + ); +} \ No newline at end of file diff --git a/cli/script/types/cli.ts b/cli/script/types/cli.ts index cfd40596c..4a130b226 100644 --- a/cli/script/types/cli.ts +++ b/cli/script/types/cli.ts @@ -198,6 +198,9 @@ export interface IReleaseReactCommand extends IReleaseBaseCommand { sourcemapOutput?: string; outputDir?: string; config?: string; + useHermes?: boolean; + extraHermesFlags?: string[]; + podFile?: string; } export interface IRollbackCommand extends ICommand { From 2f6cfeea3606c227f087f936ead38cd84cf6b24f Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Fri, 18 Oct 2024 14:55:29 +0200 Subject: [PATCH 05/21] feat(auth): conditionally render auth providers (#16) Added flags to toggle GitHub and Microsoft auth in the UI based on availability, improving UX by showing only enabled options. --- api/script/routes/passport-authentication.ts | 6 +++--- api/script/views/authenticate.ejs | 14 +++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/api/script/routes/passport-authentication.ts b/api/script/routes/passport-authentication.ts index 488d0d75a..46bb8f28a 100644 --- a/api/script/routes/passport-authentication.ts +++ b/api/script/routes/passport-authentication.ts @@ -167,17 +167,17 @@ export class PassportAuthentication { router.get("/auth/login", this._cookieSessionMiddleware, (req: Request, res: Response): any => { req.session["hostname"] = req.query.hostname; - res.render("authenticate", { action: "login" }); + res.render("authenticate", { action: "login", isGitHubAuthenticationEnabled, isMicrosoftAuthenticationEnabled }); }); router.get("/auth/link", this._cookieSessionMiddleware, (req: Request, res: Response): any => { req.session["authorization"] = req.query.access_token; - res.render("authenticate", { action: "link" }); + res.render("authenticate", { action: "link", isGitHubAuthenticationEnabled, isMicrosoftAuthenticationEnabled }); }); router.get("/auth/register", this._cookieSessionMiddleware, (req: Request, res: Response): any => { req.session["hostname"] = req.query.hostname; - res.render("authenticate", { action: "register" }); + res.render("authenticate", { action: "register", isGitHubAuthenticationEnabled, isMicrosoftAuthenticationEnabled }); }); return router; diff --git a/api/script/views/authenticate.ejs b/api/script/views/authenticate.ejs index cb6218986..33251b46f 100644 --- a/api/script/views/authenticate.ejs +++ b/api/script/views/authenticate.ejs @@ -85,11 +85,15 @@
Please select an authentication provider for your CodePush account:

- GitHub - - Microsoft (Personal) - - Microsoft (Work)
+ <% if (isGitHubAuthenticationEnabled) { %> + GitHub + + <% } %> + <% if (isMicrosoftAuthenticationEnabled) { %> + Microsoft (Personal) + + Microsoft (Work)
+ <% } %>
From 00322ee00855575b2bcf732be066209f888b2e8c Mon Sep 17 00:00:00 2001 From: Dima Date: Wed, 23 Oct 2024 19:55:55 +0700 Subject: [PATCH 06/21] [CLI] Support code signing (#23) Add support for signing update bundle. --- cli/README.md | 7 ++ cli/package-lock.json | 162 ++++++++++++++++++++++++++----- cli/package.json | 2 + cli/script/command-executor.ts | 19 ++-- cli/script/command-parser.ts | 8 ++ cli/script/react-native-utils.ts | 9 +- cli/script/sign.ts | 80 +++++++++++++++ cli/script/utils/file-utils.ts | 38 ++++++++ 8 files changed, 289 insertions(+), 36 deletions(-) create mode 100644 cli/script/sign.ts create mode 100644 cli/script/utils/file-utils.ts diff --git a/cli/README.md b/cli/README.md index c281361d8..883afd5fe 100644 --- a/cli/README.md +++ b/cli/README.md @@ -395,6 +395,7 @@ code-push-standalone release-react [--useHermes ] [--podFile ] [--extraHermesFlags ] +[--privateKeyPath ] ``` The `release-react` command is a React Native-specific version of the "vanilla" [`release`](#releasing-app-updates) command, which supports all of the same parameters (e.g. `--mandatory`, `--description`), yet simplifies the process of releasing updates by performing the following additional behavior: @@ -542,6 +543,12 @@ Hermes flags which will be passed to Hermes compiler. _NOTE: This parameter can be set using either --extraHermesFlags or -hf_ +#### Private key path parameter + +Private key path which is used for code signing. + +_NOTE: This parameter can be set using either --privateKeyPath or -k_ + ## Debugging CodePush Integration Once you've released an update, React Native plugin has been integrated into your app, it can be helpful to diagnose how the plugin is behaving, especially if you run into an issue and want to understand why. In order to debug the CodePush update discovery experience, you can run the following command in order to easily view the diagnostic logs produced by the CodePush plugin within your app: diff --git a/cli/package-lock.json b/cli/package-lock.json index 2a51ee275..9f7fd9d73 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -13,6 +13,7 @@ "cli-table": "^0.3.11", "email-validator": "^2.0.4", "gradle-to-js": "2.0.1", + "jsonwebtoken": "^9.0.2", "moment": "^2.29.4", "opener": "^1.5.2", "parse-duration": "1.1.0", @@ -27,6 +28,7 @@ "simctl": "^2.0.3", "slash": "1.0.0", "superagent": "^8.0.9", + "temp": "^0.9.4", "which": "^1.2.7", "wordwrap": "1.0.0", "xml2js": "^0.6.0", @@ -1101,6 +1103,11 @@ "node": "*" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1423,6 +1430,14 @@ "node": ">=6.0.0" } }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2489,12 +2504,57 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, "node_modules/just-extend": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", "dev": true }, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -2534,21 +2594,45 @@ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", "dev": true }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, "node_modules/media-typer": { "version": "0.3.0", @@ -2640,6 +2724,14 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/mkdirp": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", @@ -3207,7 +3299,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -3235,12 +3326,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -3572,6 +3660,41 @@ "node": ">= 0.4.0" } }, + "node_modules/temp": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", + "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", + "dependencies": { + "mkdirp": "^0.5.1", + "rimraf": "~2.6.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3807,11 +3930,6 @@ "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/cli/package.json b/cli/package.json index 580c988a2..d2791f500 100644 --- a/cli/package.json +++ b/cli/package.json @@ -24,6 +24,7 @@ "cli-table": "^0.3.11", "email-validator": "^2.0.4", "gradle-to-js": "2.0.1", + "jsonwebtoken": "^9.0.2", "moment": "^2.29.4", "opener": "^1.5.2", "parse-duration": "1.1.0", @@ -38,6 +39,7 @@ "simctl": "^2.0.3", "slash": "1.0.0", "superagent": "^8.0.9", + "temp": "^0.9.4", "which": "^1.2.7", "wordwrap": "1.0.0", "xml2js": "^0.6.0", diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index 1544f81ad..f5a7c47ed 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -21,6 +21,7 @@ const Table = require("cli-table"); const which = require("which"); import wordwrap = require("wordwrap"); import * as cli from "../script/types/cli"; +import sign from "./sign"; import { AccessKey, Account, @@ -37,11 +38,14 @@ import { UpdateMetrics, } from "../script/types"; import { - fileDoesNotExistOrIsDirectory, getAndroidHermesEnabled, getiOSHermesEnabled, runHermesEmitBinaryCommand } from "./react-native-utils"; +import { + fileDoesNotExistOrIsDirectory, + isBinaryOrZip +} from "./utils/file-utils"; const configFilePath: string = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".code-push.config"); const emailValidator = require("email-validator"); @@ -1321,7 +1325,14 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise = command.gradleFile ); } - + }) + .then(async () => { + if (command.privateKeyPath) { + log(chalk.cyan("\nSigning the bundle:\n")); + await sign(command.privateKeyPath, outputFolder); + } else { + console.log("private key was not provided"); + } }) .then(() => { log(chalk.cyan("\nReleasing update contents to CodePush:\n")); @@ -1490,10 +1501,6 @@ function releaseErrorHandler(error: CodePushError, command: cli.ICommand): void } } -function isBinaryOrZip(path: string): boolean { - return path.search(/\.zip$/i) !== -1 || path.search(/\.apk$/i) !== -1 || path.search(/\.ipa$/i) !== -1; -} - function throwForInvalidEmail(email: string): void { if (!emailValidator.validate(email)) { throw new Error('"' + email + '" is an invalid e-mail address.'); diff --git a/cli/script/command-parser.ts b/cli/script/command-parser.ts index c5d0cd094..76e47f50e 100644 --- a/cli/script/command-parser.ts +++ b/cli/script/command-parser.ts @@ -797,6 +797,13 @@ yargs "Flags that get passed to Hermes, JavaScript to bytecode compiler. Can be specified multiple times.", type: "array", }) + .option("privateKeyPath", { + alias: "k", + default: null, + demand: false, + description: "Path to private key used for code signing.", + type: "string", + }) .check((argv: any, aliases: { [aliases: string]: string }): any => { return checkValidReleaseOptions(argv); }); @@ -1194,6 +1201,7 @@ export function createCommand(): cli.ICommand { releaseReactCommand.useHermes = argv["useHermes"] as any; releaseReactCommand.extraHermesFlags = argv["extraHermesFlags"] as any; releaseReactCommand.podFile = argv["podFile"] as any; + releaseReactCommand.privateKeyPath = argv["privateKeyPath"] as any; } break; diff --git a/cli/script/react-native-utils.ts b/cli/script/react-native-utils.ts index d22dfa387..25e578929 100644 --- a/cli/script/react-native-utils.ts +++ b/cli/script/react-native-utils.ts @@ -3,6 +3,7 @@ import * as chalk from "chalk"; import * as path from "path"; import * as childProcess from "child_process"; import { coerce, compare } from "semver"; +import { fileDoesNotExistOrIsDirectory } from "./utils/file-utils"; const g2js = require("gradle-to-js/lib/parser"); @@ -119,14 +120,6 @@ export async function runHermesEmitBinaryCommand( }); } -export function fileDoesNotExistOrIsDirectory(filePath: string): boolean { - try { - return fs.lstatSync(filePath).isDirectory(); - } catch (error) { - return true; - } -} - function parseBuildGradleFile(gradleFile: string) { let buildGradlePath: string = path.join("android", "app"); if (gradleFile) { diff --git a/cli/script/sign.ts b/cli/script/sign.ts new file mode 100644 index 000000000..fd3b8fb59 --- /dev/null +++ b/cli/script/sign.ts @@ -0,0 +1,80 @@ +import * as fs from "fs/promises"; +import * as hashUtils from "./hash-utils"; +import * as path from "path"; +import * as jwt from "jsonwebtoken"; +import { copyFileToTmpDir, isDirectory } from "./utils/file-utils"; + +const CURRENT_CLAIM_VERSION: string = "1.0.0"; +const METADATA_FILE_NAME: string = ".codepushrelease"; + +interface CodeSigningClaims { + claimVersion: string; + contentHash: string; +} + +export default async function sign(privateKeyPath: string, updateContentsPath: string): Promise { + if (!privateKeyPath) { + return Promise.resolve(null); + } + + let privateKey: Buffer; + + try { + privateKey = await fs.readFile(privateKeyPath); + } catch (err) { + return Promise.reject(new Error(`The path specified for the signing key ("${privateKeyPath}") was not valid.`)); + } + + // If releasing a single file, copy the file to a temporary 'CodePush' directory in which to publish the release + try { + if (!isDirectory(updateContentsPath)) { + updateContentsPath = copyFileToTmpDir(updateContentsPath); + } + } catch (error) { + Promise.reject(error); + } + + const signatureFilePath: string = path.join(updateContentsPath, METADATA_FILE_NAME); + let prevSignatureExists = true; + try { + await fs.access(signatureFilePath, fs.constants.F_OK); + } catch (err) { + if (err.code === "ENOENT") { + prevSignatureExists = false; + } else { + return Promise.reject( + new Error( + `Could not delete previous release signature at ${signatureFilePath}. + Please, check your access rights.` + ) + ); + } + } + + if (prevSignatureExists) { + console.log(`Deleting previous release signature at ${signatureFilePath}`); + await fs.rmdir(signatureFilePath); + } + + const hash: string = await hashUtils.generatePackageHashFromDirectory(updateContentsPath, path.join(updateContentsPath, "..")); + const claims: CodeSigningClaims = { + claimVersion: CURRENT_CLAIM_VERSION, + contentHash: hash, + }; + + return new Promise((resolve, reject) => { + jwt.sign(claims, privateKey, { algorithm: "RS256" }, async (err: Error, signedJwt: string) => { + if (err) { + reject(new Error("The specified signing key file was not valid")); + } + + try { + await fs.writeFile(signatureFilePath, signedJwt); + console.log(`Generated a release signature and wrote it to ${signatureFilePath}`); + resolve(null); + } catch (error) { + reject(error); + } + }); + }); +} diff --git a/cli/script/utils/file-utils.ts b/cli/script/utils/file-utils.ts new file mode 100644 index 000000000..25f30ea91 --- /dev/null +++ b/cli/script/utils/file-utils.ts @@ -0,0 +1,38 @@ +import * as fs from "fs"; +import * as path from "path"; +import * as rimraf from "rimraf"; +import * as temp from "temp"; + +export function isBinaryOrZip(path: string): boolean { + return path.search(/\.zip$/i) !== -1 || path.search(/\.apk$/i) !== -1 || path.search(/\.ipa$/i) !== -1; +} + +export function isDirectory(path: string): boolean { + return fs.statSync(path).isDirectory(); +} + +export function copyFileToTmpDir(filePath: string): string { + if (!isDirectory(filePath)) { + const outputFolderPath: string = temp.mkdirSync("code-push"); + rimraf.sync(outputFolderPath); + fs.mkdirSync(outputFolderPath); + + const outputFilePath: string = path.join(outputFolderPath, path.basename(filePath)); + fs.writeFileSync(outputFilePath, fs.readFileSync(filePath)); + + return outputFolderPath; + } +} + +export function fileDoesNotExistOrIsDirectory(path: string): boolean { + try { + return isDirectory(path); + } catch (error) { + return true; + } +} + +export function normalizePath(filePath: string): string { + //replace all backslashes coming from cli running on windows machines by slashes + return filePath.replace(/\\/g, "/"); +} From 6cf36d8004b23455e452054a791f5e9dc08c16a0 Mon Sep 17 00:00:00 2001 From: Dima Date: Wed, 6 Nov 2024 19:54:14 +0700 Subject: [PATCH 07/21] [CLI] Improve binary app version check for iOS In addition to checking Info.plist, also check Xcode project file for MARKETING_VERSION --- cli/README.md | 21 +++++++++ cli/package-lock.json | 66 ++++++++++++++++++++++++++ cli/package.json | 1 + cli/script/command-executor.ts | 80 ++++++++++++++++++++++++-------- cli/script/command-parser.ts | 24 ++++++++++ cli/script/react-native-utils.ts | 6 ++- cli/script/types/cli.ts | 3 ++ cli/script/utils/file-utils.ts | 8 ++++ 8 files changed, 189 insertions(+), 20 deletions(-) diff --git a/cli/README.md b/cli/README.md index 883afd5fe..325919c7d 100644 --- a/cli/README.md +++ b/cli/README.md @@ -396,6 +396,9 @@ code-push-standalone release-react [--podFile ] [--extraHermesFlags ] [--privateKeyPath ] +[--xcodeProjectFile ] +[--xcodeTargetName ] +[--buildConfigurationName ] ``` The `release-react` command is a React Native-specific version of the "vanilla" [`release`](#releasing-app-updates) command, which supports all of the same parameters (e.g. `--mandatory`, `--description`), yet simplifies the process of releasing updates by performing the following additional behavior: @@ -549,6 +552,24 @@ Private key path which is used for code signing. _NOTE: This parameter can be set using either --privateKeyPath or -k_ +#### Xcode project file parameter + +Path to the Xcode project or project.pbxproj file. + +_NOTE: This parameter can be set using either --xcodeProjectFile or -xp_ + +#### Xcode target name parameter + +Name of target (PBXNativeTarget) which specifies the binary version you want to target this release at (iOS only). + +_NOTE: This parameter can be set using either --xcodeTargetName or -xt_ + +#### Build configuration name parameter + +Name of build configuration which specifies the binary version you want to target this release at. For example, 'Debug' or 'Release' (iOS only). + +_NOTE: This parameter can be set using either --buildConfigurationName or -c_ + ## Debugging CodePush Integration Once you've released an update, React Native plugin has been integrated into your app, it can be helpful to diagnose how the plugin is behaving, especially if you run into an issue and want to understand why. In order to debug the CodePush update discovery experience, you can run the following command in order to easily view the diagnostic logs produced by the CodePush plugin within your app: diff --git a/cli/package-lock.json b/cli/package-lock.json index 9f7fd9d73..be476f1bf 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -31,6 +31,7 @@ "temp": "^0.9.4", "which": "^1.2.7", "wordwrap": "1.0.0", + "xcode": "^3.0.1", "xml2js": "^0.6.0", "yargs": "^17.7.2", "yazl": "^2.5.1" @@ -1050,6 +1051,14 @@ } ] }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -1074,6 +1083,25 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/bplist-creator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", + "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", + "dependencies": { + "stream-buffers": "2.2.x" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -3475,6 +3503,16 @@ "tail": "^0.4.0" } }, + "node_modules/simple-plist": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", + "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", + "dependencies": { + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" + } + }, "node_modules/sinon": { "version": "15.1.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-15.1.2.tgz", @@ -3530,6 +3568,14 @@ "node": ">= 0.8" } }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -3820,6 +3866,14 @@ "node": ">= 0.4.0" } }, + "node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -3894,6 +3948,18 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "node_modules/xcode": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", + "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", + "dependencies": { + "simple-plist": "^1.1.0", + "uuid": "^7.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/xml2js": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", diff --git a/cli/package.json b/cli/package.json index d2791f500..ef15ad5e3 100644 --- a/cli/package.json +++ b/cli/package.json @@ -42,6 +42,7 @@ "temp": "^0.9.4", "which": "^1.2.7", "wordwrap": "1.0.0", + "xcode": "^3.0.1", "xml2js": "^0.6.0", "yargs": "^17.7.2", "yazl": "^2.5.1" diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index f5a7c47ed..97d369ec9 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -22,6 +22,7 @@ const which = require("which"); import wordwrap = require("wordwrap"); import * as cli from "../script/types/cli"; import sign from "./sign"; +const xcode = require("xcode"); import { AccessKey, Account, @@ -40,11 +41,13 @@ import { import { getAndroidHermesEnabled, getiOSHermesEnabled, - runHermesEmitBinaryCommand + runHermesEmitBinaryCommand, + isValidVersion } from "./react-native-utils"; import { fileDoesNotExistOrIsDirectory, - isBinaryOrZip + isBinaryOrZip, + fileExists } from "./utils/file-utils"; const configFilePath: string = path.join(process.env.LOCALAPPDATA || process.env.HOME, ".code-push.config"); @@ -855,16 +858,6 @@ function getPackageMetricsString(obj: Package): string { } function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, projectName: string): Promise { - const fileExists = (file: string): boolean => { - try { - return fs.statSync(file).isFile(); - } catch (e) { - return false; - } - }; - - const isValidVersion = (version: string): boolean => !!semver.valid(version) || /^\d+\.\d+$/.test(version); - log(chalk.cyan(`Detecting ${command.platform} app version:\n`)); if (command.platform === "ios") { @@ -914,9 +907,13 @@ function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, proj log(`Using the target binary version value "${parsedPlist.CFBundleShortVersionString}" from "${resolvedPlistFile}".\n`); return Q(parsedPlist.CFBundleShortVersionString); } else { - throw new Error( - `The "CFBundleShortVersionString" key in the "${resolvedPlistFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).` - ); + if (parsedPlist.CFBundleShortVersionString !== "$(MARKETING_VERSION)") { + throw new Error( + `The "CFBundleShortVersionString" key in the "${resolvedPlistFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).` + ); + } + + return getAppVersionFromXcodeProject(command, projectName); } } else { throw new Error(`The "CFBundleShortVersionString" key doesn't exist within the "${resolvedPlistFile}" file.`); @@ -1052,6 +1049,53 @@ function getReactNativeProjectAppVersion(command: cli.IReleaseReactCommand, proj } } +function getAppVersionFromXcodeProject(command: cli.IReleaseReactCommand, projectName: string): Promise { + const pbxprojFileName = "project.pbxproj"; + let resolvedPbxprojFile: string = command.xcodeProjectFile; + if (resolvedPbxprojFile) { + // If the xcode project file path is explicitly provided, then we don't + // need to attempt to "resolve" it within the well-known locations. + if (!resolvedPbxprojFile.endsWith(pbxprojFileName)) { + // Specify path to pbxproj file if the provided file path is an Xcode project file. + resolvedPbxprojFile = path.join(resolvedPbxprojFile, pbxprojFileName); + } + if (!fileExists(resolvedPbxprojFile)) { + throw new Error("The specified pbx project file doesn't exist. Please check that the provided path is correct."); + } + } else { + const iOSDirectory = "ios"; + const xcodeprojDirectory = `${projectName}.xcodeproj`; + const pbxprojKnownLocations = [ + path.join(iOSDirectory, xcodeprojDirectory, pbxprojFileName), + path.join(iOSDirectory, pbxprojFileName), + ]; + resolvedPbxprojFile = pbxprojKnownLocations.find(fileExists); + + if (!resolvedPbxprojFile) { + throw new Error( + `Unable to find either of the following pbxproj files in order to infer your app's binary version: "${pbxprojKnownLocations.join( + '", "' + )}".` + ); + } + } + + const xcodeProj = xcode.project(resolvedPbxprojFile).parseSync(); + const marketingVersion = xcodeProj.getBuildProperty( + "MARKETING_VERSION", + command.buildConfigurationName, + command.xcodeTargetName + ); + if (!isValidVersion(marketingVersion)) { + throw new Error( + `The "MARKETING_VERSION" key in the "${resolvedPbxprojFile}" file needs to specify a valid semver string, containing both a major and minor version (e.g. 1.3.2, 1.1).` + ); + } + console.log(`Using the target binary version value "${marketingVersion}" from "${resolvedPbxprojFile}".\n`); + + return marketingVersion; +} + function printJson(object: any): void { log(JSON.stringify(object, /*replacer=*/ null, /*spacing=*/ 2)); } @@ -1278,10 +1322,6 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise = } } - if (command.appStoreVersion) { - throwForInvalidSemverRange(command.appStoreVersion); - } - const appVersionPromise: Promise = command.appStoreVersion ? Q(command.appStoreVersion) : getReactNativeProjectAppVersion(command, projectName); @@ -1293,7 +1333,9 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise = return appVersionPromise; }) .then((appVersion: string) => { + throwForInvalidSemverRange(appVersion); releaseCommand.appStoreVersion = appVersion; + return createEmptyTempReleaseFolder(outputFolder); }) // This is needed to clear the react native bundler cache: diff --git a/cli/script/command-parser.ts b/cli/script/command-parser.ts index 76e47f50e..afd641c23 100644 --- a/cli/script/command-parser.ts +++ b/cli/script/command-parser.ts @@ -804,6 +804,27 @@ yargs description: "Path to private key used for code signing.", type: "string", }) + .option("xcodeProjectFile", { + alias: "xp", + default: null, + demand: false, + description: "Path to the Xcode project or project.pbxproj file", + type: "string", + }) + .option("xcodeTargetName", { + alias: "xt", + default: undefined, + demand: false, + description: "Name of target (PBXNativeTarget) which specifies the binary version you want to target this release at (iOS only)", + type: "string", + }) + .option("buildConfigurationName", { + alias: "c", + default: undefined, + demand: false, + description: "Name of build configuration which specifies the binary version you want to target this release at. For example, 'Debug' or 'Release' (iOS only)", + type: "string", + }) .check((argv: any, aliases: { [aliases: string]: string }): any => { return checkValidReleaseOptions(argv); }); @@ -1202,6 +1223,9 @@ export function createCommand(): cli.ICommand { releaseReactCommand.extraHermesFlags = argv["extraHermesFlags"] as any; releaseReactCommand.podFile = argv["podFile"] as any; releaseReactCommand.privateKeyPath = argv["privateKeyPath"] as any; + releaseReactCommand.xcodeProjectFile = argv["xcodeProjectFile"] as any; + releaseReactCommand.xcodeTargetName = argv["xcodeTargetName"] as any; + releaseReactCommand.buildConfigurationName = argv["buildConfigurationName"] as any; } break; diff --git a/cli/script/react-native-utils.ts b/cli/script/react-native-utils.ts index 25e578929..9270fed3c 100644 --- a/cli/script/react-native-utils.ts +++ b/cli/script/react-native-utils.ts @@ -2,11 +2,15 @@ import * as fs from "fs"; import * as chalk from "chalk"; import * as path from "path"; import * as childProcess from "child_process"; -import { coerce, compare } from "semver"; +import { coerce, compare, valid } from "semver"; import { fileDoesNotExistOrIsDirectory } from "./utils/file-utils"; const g2js = require("gradle-to-js/lib/parser"); +export function isValidVersion(version: string): boolean { + return !!valid(version) || /^\d+\.\d+$/.test(version); +} + export async function runHermesEmitBinaryCommand( bundleName: string, outputFolder: string, diff --git a/cli/script/types/cli.ts b/cli/script/types/cli.ts index 4a130b226..fe555200d 100644 --- a/cli/script/types/cli.ts +++ b/cli/script/types/cli.ts @@ -201,6 +201,9 @@ export interface IReleaseReactCommand extends IReleaseBaseCommand { useHermes?: boolean; extraHermesFlags?: string[]; podFile?: string; + xcodeProjectFile?: string; + xcodeTargetName?: string; + buildConfigurationName?: string; } export interface IRollbackCommand extends ICommand { diff --git a/cli/script/utils/file-utils.ts b/cli/script/utils/file-utils.ts index 25f30ea91..7881dd169 100644 --- a/cli/script/utils/file-utils.ts +++ b/cli/script/utils/file-utils.ts @@ -11,6 +11,14 @@ export function isDirectory(path: string): boolean { return fs.statSync(path).isDirectory(); } +export function fileExists(file: string): boolean { + try { + return fs.statSync(file).isFile(); + } catch (e) { + return false; + } +}; + export function copyFileToTmpDir(filePath: string): string { if (!isDirectory(filePath)) { const outputFolderPath: string = temp.mkdirSync("code-push"); From 2e7beaedc4c45664ac329d01c335b04feed3d431 Mon Sep 17 00:00:00 2001 From: SergeiPetrovAkvelon <115561537+SergeiPetrovAkvelon@users.noreply.github.com> Date: Wed, 13 Nov 2024 11:20:45 +0100 Subject: [PATCH 08/21] Adding a section in the Readme for setting up Redis server --- api/README.md | 12 ++++++++++++ api/SECURITY.md | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/api/README.md b/api/README.md index b9747afd3..21f38bf67 100644 --- a/api/README.md +++ b/api/README.md @@ -115,3 +115,15 @@ Both work and personal accounts use the same application for authentication. The 1. Only letters are allowed. 1. Maximum 15 characters. + +## Metrics + +Installation metrics allow monitoring release activity via the CLI. For detailed usage instructions, please refer to the [CLI documentation](../cli/README.md#development-parameter). + +Redis is required for Metrics to work. + +### Steps + +1. Install Redis by following [official installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/). +1. TLS is required. Follow [official Redis TLS run guide](https://redis.io/docs/latest/operate/oss_and_stack/management/security/encryption/#running-manually). +1. Set the necessary environment variables for [Redis](./ENVIRONMENT.md#redis). \ No newline at end of file diff --git a/api/SECURITY.md b/api/SECURITY.md index 2ee12dc54..022413d51 100644 --- a/api/SECURITY.md +++ b/api/SECURITY.md @@ -19,4 +19,4 @@ All secrets used in the system should be handled with the utmost care. They must It is essential to review and apply security best practices for all system components. As this setup is minimal, it is the customer’s responsibility to harden the system for production use. - [Azure Storage Security Recommendations](https://learn.microsoft.com/en-us/azure/storage/blobs/security-recommendations) -- [Azure WebApp Security Best Practices](https://learn.microsoft.com/en-us/azure/app-service/overview-security) +- [Azure WebApp Security Best Practices](https://learn.microsoft.com/en-us/azure/app-service/overview-security) \ No newline at end of file From cd62de8ce278249e362c0d8e40c6f539a4c9954d Mon Sep 17 00:00:00 2001 From: velimir-jankovic <84719115+velimir-jankovic@users.noreply.github.com> Date: Thu, 14 Nov 2024 10:34:15 +0100 Subject: [PATCH 09/21] Add support for setting MICROSOFT_TENANT_ID Added new conditionally optional environment variable. Required for single tenant app registration (#39). --- api/ENVIRONMENT.md | 1 + api/README.md | 2 +- api/script/routes/passport-authentication.ts | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/api/ENVIRONMENT.md b/api/ENVIRONMENT.md index 0f4067e63..4719743f8 100644 --- a/api/ENVIRONMENT.md +++ b/api/ENVIRONMENT.md @@ -30,6 +30,7 @@ To emulate Azure Blob Storage locally. Azurite needs to be installed and running - `MICROSOFT_CLIENT_ID` - `MICROSOFT_CLIENT_SECRET` +- `MICROSOFT_TENANT_ID`: Required if application registration is single tenant. ## Optional parameters diff --git a/api/README.md b/api/README.md index 21f38bf67..474a14598 100644 --- a/api/README.md +++ b/api/README.md @@ -102,7 +102,7 @@ Both work and personal accounts use the same application for authentication. The 1. Register an Azure Registered Application following [official guideline](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app#register-an-application) 1. For option `Supported account types`: 1. If you want to support both Personal and Work accounts, select `Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)` - 1. If you want to only support Work accounts, choose either `Accounts in this organizational directory only ( - Single tenant)` or `Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant)` depending if you want to support Single or Multitenant authorization + 1. If you want to only support Work accounts, choose either `Accounts in this organizational directory only ( - Single tenant)` or `Accounts in any organizational directory (Any Microsoft Entra ID tenant - Multitenant)` depending if you want to support Single or Multitenant authorization. Make sure to set `MICROSOFT_TENANT_ID` envrionment variable in case of using single tenant application. 1. If you want to only support Personal accounts, select `Personal Microsoft accounts only` 1. Set up Redirect URI(s) depending on the choice you made for `Supported account types`. If you choose both Personal and Work accounts, you need to add both redirect URIs, otherwise just one of the ones: 1. Personal account: `https://codepush-.azurewebsites.net/auth/callback/microsoft` (for local development it will be either http://localhost:3000/auth/callback/microsoft or https://localhost:8443/auth/callback/microsoft) diff --git a/api/script/routes/passport-authentication.ts b/api/script/routes/passport-authentication.ts index 46bb8f28a..38e0357ab 100644 --- a/api/script/routes/passport-authentication.ts +++ b/api/script/routes/passport-authentication.ts @@ -488,7 +488,9 @@ export class PassportAuthentication { redirectUrl: this.getCallbackUrl(providerName), clientID: microsoftClientId, clientSecret: microsoftClientSecret, - identityMetadata: "https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", + identityMetadata: `https://login.microsoftonline.com/${ + process.env["MICROSOFT_TENANT_ID"] || "common" + }/v2.0/.well-known/openid-configuration`, responseMode: "query", responseType: "code", scope: ["email", "profile"], From 5d42d396f595a7c97b9955f95d94fa88130c911d Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Tue, 19 Nov 2024 10:30:37 +0100 Subject: [PATCH 10/21] chore: add .env.example for easier environment setup and refine .gitignore (#15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I’ve made a couple of updates here to developer experience: - Added `.env.example` for easier environment setup This provides a template for environment variables, helping developers get their local setup configured quickly. - Updated `.gitignore` to exclude Azurite local storage files We’ve added patterns to ignore Azurite's local storage files `(__azurite_db_*, __blobstorage__, __queuestorage__)`, keeping the working directory cleaner during local development. --- .gitignore | 7 ++++- api/.env.example | 71 ++++++++++++++++++++++++++++++++++++++++++++++ api/ENVIRONMENT.md | 2 +- api/README.md | 25 ++++++++++++---- 4 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 api/.env.example diff --git a/.gitignore b/.gitignore index 9e1210e74..1576fba0d 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,9 @@ JsonStorage.json *.crt *.pem *.key -*.cnf \ No newline at end of file +*.cnf + +# Azurite local storage files +__azurite_db_* +__blobstorage__ +__queuestorage__ diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 000000000..352ed8fb9 --- /dev/null +++ b/api/.env.example @@ -0,0 +1,71 @@ +########################################## +# Environment Variables for CodePush Server +########################################## + +# ============================== +# Storage Configuration (REQUIRED - choose one) +# ============================== +EMULATED=false # Set to 'true' to use the local emulator + +# --- Azure Storage Configuration --- +AZURE_STORAGE_ACCOUNT= # Azure storage account name +AZURE_STORAGE_ACCESS_KEY= # Azure storage access key (if KeyVault not used) + +# ============================== +# Server Configuration (REQUIRED) +# ============================== +SERVER_URL=http://localhost:3000 # The URL of your server + +# ============================== +# Authentication (REQUIRED - at least one provider) +# ============================== + +# --- GitHub OAuth --- +GITHUB_CLIENT_ID= # GitHub OAuth client ID +GITHUB_CLIENT_SECRET= # GitHub OAuth client secret + +# --- Microsoft OAuth --- +MICROSOFT_CLIENT_ID= # Microsoft OAuth client ID +MICROSOFT_CLIENT_SECRET= # Microsoft OAuth client secret + +# ============================== +# Optional Configuration +# ============================== + +# --- HTTPS Configuration --- +HTTPS= # Set to 'true' to enable HTTPS for local deployment + +# --- Debugging Configuration --- +LOGGING=false # Enable CodePush-specific logging +DEBUG_DISABLE_AUTH=false # Disable OAuth authentication route +DEBUG_USER_ID= # Backend user ID for debugging session + +# ============================== +# Redis Configuration +# ============================== +REDIS_HOST= # Redis server IP address +REDIS_PORT=6379 # Redis port (default: 6379) +REDIS_KEY= # Redis authentication key + +# ============================== +# Unit Testing Configuration +# ============================== +TEST_AZURE_STORAGE=false # Run API unit tests against Azure storage +AZURE_ACQUISITION_URL= # URL for acquisition tests + +# ============================== +# Other Configuration +# ============================== +DISABLE_ACQUISITION=false # Disable acquisition routes +DISABLE_MANAGEMENT=false # Disable management routes +ENABLE_ACCOUNT_REGISTRATION=true # Enable account registration +UPLOAD_SIZE_LIMIT_MB=200 # Max file upload size (in MB) +ENABLE_PACKAGE_DIFFING=false # Enable generating diffs for releases + +# ============================== +# Azure KeyVault Configuration (Optional) +# ============================== +AZURE_KEYVAULT_ACCOUNT= # Azure KeyVault account name +CLIENT_ID= # Active Directory app client ID +CERTIFICATE_THUMBPRINT= # AD app certificate thumbprint +REFRESH_CREDENTIALS_INTERVAL=86400000 # Credential refresh interval (in ms, default: 1 day) diff --git a/api/ENVIRONMENT.md b/api/ENVIRONMENT.md index 4719743f8..f07c9187f 100644 --- a/api/ENVIRONMENT.md +++ b/api/ENVIRONMENT.md @@ -2,7 +2,7 @@ The CodePush Server is configured using environment variables. -Currently, the following environment variables are available. For convenience, we will also load the server environment from any '.env' file in the api directory, and the test environment from any '.test.env' file in the root directory. +For convenience, we will also load the server environment from any '.env' file in the api directory, and the test environment from any '.test.env' file in the root directory. Use the `.env.example` file as a template for setting up your environment variables. ## Mandatory parameters diff --git a/api/README.md b/api/README.md index 474a14598..760bddf01 100644 --- a/api/README.md +++ b/api/README.md @@ -16,11 +16,26 @@ Additionally, you need to specify [EMULATED](ENVIRONMENT.md#emulated) flag equal #### Steps To run the CodePush Server locally, follow these steps: -1. Clone the CodePush Service repository. -1. Create a `.env` file and configure the mandatory variables as outlined in the `ENVIRONMENT.md` file. -1. Install dependencies by running `npm install`. -1. Build the server by running `npm run build`. -1. Start the server by running `npm run start:env`. + +1. Clone the CodePush Service repository to your local machine. + +2. Copy the `.env.example` file to a new file named `.env` in the root directory: + ````bash + cp .env.example .env + ```` + Fill in the values for each environment variable in the `.env` file according to your development or production setup. +3. Install all necessary dependencies: + ````bash + npm install + ```` +4. Compile the server code: + ````bash + npm run build + ```` +5. Launch the server with the environment-specific start command: + ````bash + npm run start:env + ```` By default, local CodePush server runs on HTTP. To run CodePush Server on HTTPS: From e9ad2caba7b57ec3dd6a7e40751ee6e5b2839741 Mon Sep 17 00:00:00 2001 From: Dori <79495204+minizzang@users.noreply.github.com> Date: Mon, 2 Dec 2024 19:25:40 +0900 Subject: [PATCH 11/21] [CLI] Fix sourcemap output filename (#43) **Fix sourcemapOutput Overwrite** There's an issue where the sourcemap output is overwritten when both the sourcemap output and output directory are used together in the CLI. --- cli/script/command-executor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index 97d369ec9..30b75218c 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -1326,8 +1326,8 @@ export const releaseReact = (command: cli.IReleaseReactCommand): Promise = ? Q(command.appStoreVersion) : getReactNativeProjectAppVersion(command, projectName); - if (command.outputDir) { - command.sourcemapOutput = path.join(command.outputDir, bundleName + ".map"); + if (command.sourcemapOutput && !command.sourcemapOutput.endsWith(".map")) { + command.sourcemapOutput = path.join(command.sourcemapOutput, bundleName + ".map"); } return appVersionPromise; From 338eb344a9ed83f7e0232ef49bbba03e26fdc21e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:14:08 +0100 Subject: [PATCH 12/21] Bump path-to-regexp and express in /api (#56) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) to 0.1.12 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together. Updates `path-to-regexp` from 0.1.10 to 0.1.12
Release notes

Sourced from path-to-regexp's releases.

Fix backtracking (again)

Fixed

https://github.com/pillarjs/path-to-regexp/compare/v0.1.11...v0.1.12

Error on bad input

Changed

  • Add error on bad input values 8f09549

https://github.com/pillarjs/path-to-regexp/compare/v0.1.10...v0.1.11

Commits

Updates `express` from 4.21.1 to 4.21.2
Release notes

Sourced from express's releases.

4.21.2

What's Changed

Full Changelog: https://github.com/expressjs/express/compare/4.21.1...4.21.2

Changelog

Sourced from express's changelog.

4.21.2 / 2024-11-06

  • deps: path-to-regexp@0.1.12
    • Fix backtracking protection
  • deps: path-to-regexp@0.1.11
    • Throws an error on invalid path values
Commits
Maintainer changes

This version was pushed to npm by jonchurch, a new releaser for express since your current version.


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/code-push-server/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/package-lock.json | 22 ++++++++++++++-------- api/package.json | 2 +- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index df50d9dc1..e102e2e9c 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -18,7 +18,7 @@ "cookie-session": "^2.0.0", "ejs": "^3.1.10", "email-validator": "1.0.3", - "express": "^4.21.1", + "express": "^4.21.2", "express-domain-middleware": "0.1.0", "express-rate-limit": "^7.4.0", "multer": "^1.4.5-lts.1", @@ -2671,9 +2671,10 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "license": "MIT", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -2694,7 +2695,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -2709,6 +2710,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-domain-middleware": { @@ -4500,9 +4505,10 @@ } }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" }, "node_modules/path-type": { "version": "4.0.0", diff --git a/api/package.json b/api/package.json index 8adc3b7f4..da3db1ada 100644 --- a/api/package.json +++ b/api/package.json @@ -32,7 +32,7 @@ "cookie-session": "^2.0.0", "ejs": "^3.1.10", "email-validator": "1.0.3", - "express": "^4.21.1", + "express": "^4.21.2", "express-domain-middleware": "0.1.0", "express-rate-limit": "^7.4.0", "multer": "^1.4.5-lts.1", From 4cee15f2fe1f1556b05e92f33865c15b9c5a3d1c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 15:14:10 +0100 Subject: [PATCH 13/21] Bump nanoid from 3.3.7 to 3.3.8 in /api (#59) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
Changelog

Sourced from nanoid's changelog.

3.3.8

  • Fixed a way to break Nano ID by passing non-integer size (by @​myndzi).
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nanoid&package-manager=npm_and_yarn&previous-version=3.3.7&new-version=3.3.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/code-push-server/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- api/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/package-lock.json b/api/package-lock.json index e102e2e9c..45dfbb9c5 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -4082,9 +4082,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "funding": [ { "type": "github", From 73e20676d0bed5e5ac4f33fa10d79cf3331146c7 Mon Sep 17 00:00:00 2001 From: valentin-consulting <46557078+valentin-consulting@users.noreply.github.com> Date: Thu, 19 Dec 2024 18:25:35 +0400 Subject: [PATCH 14/21] fix: Consider app transfer as a valid command (#49) App ownership transfer from CLI always shows help, rather than doing action. It seems command is never marked as valid. --- cli/script/command-parser.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/script/command-parser.ts b/cli/script/command-parser.ts index afd641c23..61fd94052 100644 --- a/cli/script/command-parser.ts +++ b/cli/script/command-parser.ts @@ -322,6 +322,7 @@ yargs .command("list", "Lists the apps associated with your account", (yargs: yargs.Argv) => appList("list", yargs)) .command("ls", "Lists the apps associated with your account", (yargs: yargs.Argv) => appList("ls", yargs)) .command("transfer", "Transfer the ownership of an app to another account", (yargs: yargs.Argv) => { + isValidCommand = true; yargs .usage(USAGE_PREFIX + " app transfer ") .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments From fac126c887e4ba7cf64d4a4fc0d392d885ed6a03 Mon Sep 17 00:00:00 2001 From: Dima Date: Mon, 23 Dec 2024 21:18:20 +0700 Subject: [PATCH 15/21] [Service] Ignore .codepushrelease when computing package hash This change fixes the error: CodePushInvalidUpdateException: The update contents failed the data integrity check. --- api/script/utils/hash-utils.ts | 11 ++++++- cli/README.md | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/api/script/utils/hash-utils.ts b/api/script/utils/hash-utils.ts index abc66fd50..662e6ddbb 100644 --- a/api/script/utils/hash-utils.ts +++ b/api/script/utils/hash-utils.ts @@ -28,6 +28,8 @@ try { import Promise = q.Promise; const HASH_ALGORITHM = "sha256"; +const CODEPUSH_METADATA = '.codepushrelease'; + export function generatePackageHashFromDirectory(directoryPath: string, basePath: string): Promise { if (!fs.lstatSync(directoryPath).isDirectory()) { throw new Error("Not a directory. Please either create a directory, or use hashFile()."); @@ -187,7 +189,14 @@ export class PackageManifest { public computePackageHash(): Promise { let entries: string[] = []; this._map.forEach((hash: string, name: string): void => { - entries.push(name + ":" + hash); + // .codepushrelease (relates to code signing feature) file + // should not be skipped in isIgnored() method. + // But to be equal with hashes computed in SDKs and CLI this file + // should be skipped when computing whole package hash + + if (name !== CODEPUSH_METADATA && !endsWith(name, '/' + CODEPUSH_METADATA)) { + entries.push(name + ':' + hash); + } }); // Make sure this list is alphabetically ordered so that other clients diff --git a/cli/README.md b/cli/README.md index 325919c7d..a11a33e89 100644 --- a/cli/README.md +++ b/cli/README.md @@ -772,3 +772,60 @@ code-push-standalone deployment clear ``` After running this command, client devices configured to receive updates using its associated deployment key will no longer receive the updates that have been cleared. This command is irreversible, and therefore should not be used in a production deployment. + +## Code Signing for CodePush + +Code Signing ensures that updates deployed via CodePush are secure and verified. Follow these steps to set up Code Signing: + +### 1. Generate a Signing Key + +**Create private and public keys using OpenSSL:** + +```shell +# generate private RSA key and write it to private.pem file +openssl genrsa -out private.pem + +# export public key from private.pem into public.pem +openssl rsa -pubout -in private.pem -out public.pem +``` + +### 2. Configure CodePush CLI + +**Specify the path to your private key when releasing updates:** + +```shell +code-push-standalone release-react --privateKeyPath private.pem +``` + +### 3. Configure Your App + +#### iOS + +**Add the public key to your `Info.plist`:** + +- Open your `Info.plist` file. +- Add a new entry: + + ```xml + CodePushPublicKey + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... + -----END PUBLIC KEY----- + ``` + +Replace the placeholder with the actual contents of your `public.pem` file. + +#### Android + +**Add the public key to your `strings.xml`:** + +- Open `res/values/strings.xml`. +- Add the following entry: + + ```xml + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... + -----END PUBLIC KEY----- + ``` + +Replace the placeholder with the actual contents of your `public.pem` file. \ No newline at end of file From 720dc667a3d1b68bde63117e960e8be0851a4535 Mon Sep 17 00:00:00 2001 From: Hiroshi Yoshioka <40815708+hyoshioka0128@users.noreply.github.com> Date: Fri, 27 Dec 2024 19:05:53 +0900 Subject: [PATCH 16/21] =?UTF-8?q?Update=20ENVIRONMENT.md=20(Typo=20"a=20Az?= =?UTF-8?q?ure"=E2=86=92"an=20Azure")=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/microsoft/code-push-server/blob/main/api/ENVIRONMENT.md #PingMSFTDocs --- api/ENVIRONMENT.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/ENVIRONMENT.md b/api/ENVIRONMENT.md index f07c9187f..c7d0e0369 100644 --- a/api/ENVIRONMENT.md +++ b/api/ENVIRONMENT.md @@ -59,8 +59,8 @@ To perform the unit tests against Azure storage: To perform the unit tests against an Azure server: -- `AZURE_MANAGEMENT_URL`: (For unit tests) Set to a Azure url to run management tests against that server. If the server has authentication enabled, also set the TEST_AZURE_STORAGE and AZURE_STORAGE_ACCESS_KEY to the Azure storage used by the server so that the tests can pass authentication. -- `AZURE_ACQUISITION_URL`: (For unit tests) Set to a Azure url to run acquisition tests against that server. If the server has authentication enabled, also set the TEST_AZURE_STORAGE and AZURE_STORAGE_ACCESS_KEY to the Azure storage used by the server so that the tests can pass authentication. +- `AZURE_MANAGEMENT_URL`: (For unit tests) Set to an Azure url to run management tests against that server. If the server has authentication enabled, also set the TEST_AZURE_STORAGE and AZURE_STORAGE_ACCESS_KEY to the Azure storage used by the server so that the tests can pass authentication. +- `AZURE_ACQUISITION_URL`: (For unit tests) Set to an Azure url to run acquisition tests against that server. If the server has authentication enabled, also set the TEST_AZURE_STORAGE and AZURE_STORAGE_ACCESS_KEY to the Azure storage used by the server so that the tests can pass authentication. ### Other From 08e97d751620ad7867bb0c9b94633e94f2c01c68 Mon Sep 17 00:00:00 2001 From: Mikkel Gravgaard Date: Mon, 6 Jan 2025 10:50:22 +0100 Subject: [PATCH 17/21] Add deployment: allow specifying deployment key (#60) When migrating an existing app to the stand-alone server, it makes sense to re-use existing deployment keys. The Rest API already allows for explicitly specifying deployment key, so this PR just adds an optional argument to the `deployment add` command. Example usage: ```bash code-push-standalone deployment add my-app prod existing-key-abc123 ``` --------- Co-authored-by: Mikkel Gravgaard --- cli/README.md | 6 ++++++ cli/script/command-executor.ts | 2 +- cli/script/command-parser.ts | 13 ++++++++++++- cli/script/management-sdk.ts | 4 ++-- cli/script/types/cli.ts | 1 + 5 files changed, 22 insertions(+), 4 deletions(-) diff --git a/cli/README.md b/cli/README.md index a11a33e89..1b82597ca 100644 --- a/cli/README.md +++ b/cli/README.md @@ -204,6 +204,12 @@ If having a staging and production version of your app is enough to meet your ne code-push-standalone deployment add ``` +If you want to re-use an existing deployment key, you can do this with: + +``` +code-push-standalone deployment add -k +``` + Just like with apps, you can remove and rename deployments as well, using the following commands respectively: ``` diff --git a/cli/script/command-executor.ts b/cli/script/command-executor.ts index 30b75218c..5e37358cc 100644 --- a/cli/script/command-executor.ts +++ b/cli/script/command-executor.ts @@ -290,7 +290,7 @@ function deleteFolder(folderPath: string): Promise { } function deploymentAdd(command: cli.IDeploymentAddCommand): Promise { - return sdk.addDeployment(command.appName, command.deploymentName).then((deployment: Deployment): void => { + return sdk.addDeployment(command.appName, command.deploymentName, command.key).then((deployment: Deployment): void => { log( 'Successfully added the "' + command.deploymentName + diff --git a/cli/script/command-parser.ts b/cli/script/command-parser.ts index 61fd94052..43b8b391e 100644 --- a/cli/script/command-parser.ts +++ b/cli/script/command-parser.ts @@ -377,7 +377,14 @@ yargs yargs .usage(USAGE_PREFIX + " deployment add ") .demand(/*count*/ 2, /*max*/ 2) // Require exactly two non-option arguments - .example("deployment add MyApp MyDeployment", 'Adds deployment "MyDeployment" to app "MyApp"'); + .example("deployment add MyApp MyDeployment", 'Adds deployment "MyDeployment" to app "MyApp"') + .example("deployment add MyApp MyDeployment -k abc123", 'Adds deployment key "abc123"') + .option("key", { + alias: "k", + demand: false, + description: "Specify deployment key", + type: "string", + }); addCommonConfiguration(yargs); }) @@ -1046,6 +1053,10 @@ export function createCommand(): cli.ICommand { deploymentAddCommand.appName = arg2; deploymentAddCommand.deploymentName = arg3; + if(argv["key"]){ + deploymentAddCommand.key = argv["key"] as any; + } + } break; diff --git a/cli/script/management-sdk.ts b/cli/script/management-sdk.ts index 0d5af7a90..6b620a02e 100644 --- a/cli/script/management-sdk.ts +++ b/cli/script/management-sdk.ts @@ -253,8 +253,8 @@ class AccountManager { } // Deployments - public addDeployment(appName: string, deploymentName: string): Promise { - const deployment = { name: deploymentName }; + public addDeployment(appName: string, deploymentName: string, deploymentKey?: string): Promise { + const deployment = { name: deploymentName, key: deploymentKey }; return this.post(urlEncode([`/apps/${appName}/deployments/`]), JSON.stringify(deployment), /*expectResponseBody=*/ true).then( (res: JsonResponse) => res.body.deployment ); diff --git a/cli/script/types/cli.ts b/cli/script/types/cli.ts index fe555200d..359ce1fc4 100644 --- a/cli/script/types/cli.ts +++ b/cli/script/types/cli.ts @@ -107,6 +107,7 @@ export interface IDebugCommand extends ICommand { export interface IDeploymentAddCommand extends ICommand { appName: string; deploymentName: string; + key?: string; default: boolean; } From 2330e452587b339f78acedb7434f75659ed923fa Mon Sep 17 00:00:00 2001 From: Mikkel Gravgaard Date: Wed, 12 Feb 2025 15:20:33 +0100 Subject: [PATCH 18/21] Check LOGGING env-var for "true" (#78) LOGGING env var should be exlicitly set to "true", in order to log requests. Currently it just needs to be truethy, which doesn't correspond with the the current default value in the example .env.local file (where it's "false"). Explicit "true" is also how other env-vars are checked, eg https://github.com/microsoft/code-push-server/blob/08e97d751620ad7867bb0c9b94633e94f2c01c68/api/script/default-server.ts#L110 --- api/script/default-server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/script/default-server.ts b/api/script/default-server.ts index 1c9234584..d6b0e6f0d 100644 --- a/api/script/default-server.ts +++ b/api/script/default-server.ts @@ -90,7 +90,7 @@ export function start(done: (err?: any, server?: express.Express, storage?: Stor next(); }); - if (process.env.LOGGING) { + if (process.env.LOGGING === "true") { app.use((req: express.Request, res: express.Response, next: (err?: any) => void): any => { console.log(); // Newline to mark new request console.log(`[REST] Received ${req.method} request at ${req.originalUrl}`); From b4f6dc11223ec20949f25fdcd414b627eb4d4c63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 11:44:37 +0100 Subject: [PATCH 19/21] Bump parse-duration from 1.1.0 to 2.1.3 in /cli (#95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [parse-duration](https://github.com/jkroso/parse-duration) from 1.1.0 to 2.1.3.
Release notes

Sourced from parse-duration's releases.

v2.1.3

What's Changed

  • Better performance / memory #61
  • Safer RE #61
  • Customizable placeholders
  • Type declaraion for rewriting units #62

Full Changelog: https://github.com/jkroso/parse-duration/compare/v2.1.2...v2.1.3

v2.1.2

What's Changed

  • Fixed type definitions
  • Added abbrs mo, mth, microsec, nanosec
  • Removed b abbr
  • Removed μ as greek mu in favor of &micro; left in default set

Full Changelog: https://github.com/jkroso/parse-duration/compare/v2.1.1...v2.1.2

v2.0.0

  • Introduces locales
  • Moves units into parse.unit object #56
  • Abandons CJS format in favour of ESM
  • Adds locale-dependent group/decimal separator #35

v1.1.2

What's Changed

New Contributors

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=parse-duration&package-manager=npm_and_yarn&previous-version=1.1.0&new-version=2.1.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/microsoft/code-push-server/network/alerts).
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- cli/package-lock.json | 9 +++++---- cli/package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index be476f1bf..684735f53 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -16,7 +16,7 @@ "jsonwebtoken": "^9.0.2", "moment": "^2.29.4", "opener": "^1.5.2", - "parse-duration": "1.1.0", + "parse-duration": "2.1.3", "plist": "^3.0.6", "progress": "^2.0.3", "prompt": "^1.3.0", @@ -2945,9 +2945,10 @@ } }, "node_modules/parse-duration": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.0.tgz", - "integrity": "sha512-z6t9dvSJYaPoQq7quMzdEagSFtpGu+utzHqqxmpVWNNZRIXnvqyCvn9XsTdh7c/w0Bqmdz3RB3YnRaKtpRtEXQ==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-2.1.3.tgz", + "integrity": "sha512-MtbharL7Bets65qDBXuDOHHWyY1BxTJZmJ/xGmS90iEbKE0gZ6yZpZtCda7O79GeOi/f0NwBaplIuReExIoVsw==", + "license": "MIT" }, "node_modules/parseurl": { "version": "1.3.3", diff --git a/cli/package.json b/cli/package.json index ef15ad5e3..1c9539fae 100644 --- a/cli/package.json +++ b/cli/package.json @@ -27,7 +27,7 @@ "jsonwebtoken": "^9.0.2", "moment": "^2.29.4", "opener": "^1.5.2", - "parse-duration": "1.1.0", + "parse-duration": "2.1.3", "plist": "^3.0.6", "progress": "^2.0.3", "prompt": "^1.3.0", From 5b42222e0349d5b822e8dbbee7dc8f41ccc6ad50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20L=C3=A4tt?= Date: Fri, 14 Feb 2025 15:54:25 +0200 Subject: [PATCH 20/21] Add upper bound for parse-duration package version (#97) NPM package [parse-duration](https://www.npmjs.com/package/parse-duration) version was bumped from `1.1.0` to `2.1.3` In PR #95 for CodePush CLI. This caused the build to fail with the following error: ```shell % npm run build > code-push-cli@0.0.1 build > tsc script/command-parser.ts:1321:21 - error TS2349: This expression is not callable. Type 'typeof import("/private/tmp/ms-code-push-server/cli/node_modules/parse-duration/index")' has no call signatures. 1321 return Math.floor(parseDuration(durationString)); ~~~~~~~~~~~~~ Found 1 error in script/command-parser.ts:1321 ``` As current code-base is not compatible with `parse-duration` versions `2.0.0+`, restrict `parse-duration` to use version `1.x.y`. --- cli/package-lock.json | 8 ++++---- cli/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 684735f53..7dcf49a0d 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -16,7 +16,7 @@ "jsonwebtoken": "^9.0.2", "moment": "^2.29.4", "opener": "^1.5.2", - "parse-duration": "2.1.3", + "parse-duration": "^1.1.0", "plist": "^3.0.6", "progress": "^2.0.3", "prompt": "^1.3.0", @@ -2945,9 +2945,9 @@ } }, "node_modules/parse-duration": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-2.1.3.tgz", - "integrity": "sha512-MtbharL7Bets65qDBXuDOHHWyY1BxTJZmJ/xGmS90iEbKE0gZ6yZpZtCda7O79GeOi/f0NwBaplIuReExIoVsw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/parse-duration/-/parse-duration-1.1.2.tgz", + "integrity": "sha512-p8EIONG8L0u7f8GFgfVlL4n8rnChTt8O5FSxgxMz2tjc9FMP199wxVKVB6IbKx11uTbKHACSvaLVIKNnoeNR/A==", "license": "MIT" }, "node_modules/parseurl": { diff --git a/cli/package.json b/cli/package.json index 1c9539fae..2c0acd2c7 100644 --- a/cli/package.json +++ b/cli/package.json @@ -27,7 +27,7 @@ "jsonwebtoken": "^9.0.2", "moment": "^2.29.4", "opener": "^1.5.2", - "parse-duration": "2.1.3", + "parse-duration": "^1.1.0", "plist": "^3.0.6", "progress": "^2.0.3", "prompt": "^1.3.0", From ff1d84cde0e18e5760df7b9622c46d5cad236016 Mon Sep 17 00:00:00 2001 From: Lukas Cenovsky <79307699+lucen-ms@users.noreply.github.com> Date: Tue, 20 May 2025 13:27:22 +0200 Subject: [PATCH 21/21] Update README.md (#134) --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cdbda97d6..484e3b18a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,10 @@ -# Visual Studio App Center CodePush Standalone Version +# Archiving this repository + +Visual Studio App Center was retired on March 31, 2025, except for its Analytics and Diagnostics features. You can learn more about the retirement and the Analytics and Diagnostics extension [here](https://aka.ms/appcenter/retire). CodePush, along with other App Center features, was also retired on March 31, 2025. Consequently, we are archiving this repository. + +--- + +## Visual Studio App Center CodePush Standalone Version [CodePush](https://learn.microsoft.com/en-us/appcenter/distribution/codepush/) is an App Center feature that enables React Native developers to deploy mobile app updates directly to their users’ devices. It consists of two parts: CodePush Server where developers can publish app updates to (e.g. JS, HTML, CSS or image changes), and [CodePush React Native Client SDK](https://github.com/Microsoft/react-native-code-push) that enables querying for updates from within an app.