From 72464c94c4816eaf1a83e79a6c52138f9397adf0 Mon Sep 17 00:00:00 2001 From: Dima Date: Fri, 11 Oct 2024 18:12:35 +0700 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] [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/13] 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/13] [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/13] [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/13] 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 b766a0d4fc959b212b5b6d603d6c6bc659a63a46 Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Thu, 10 Oct 2024 12:46:07 +0200 Subject: [PATCH 09/13] chore: add .env.example for clearer environment setup Provide a template for environment variables to guide developers in configuring their local setup more easily. --- api/.env.example | 70 ++++++++++++++++++++++++++++++++++++++++++++++ api/ENVIRONMENT.md | 2 +- api/README.md | 9 +++--- 3 files changed, 76 insertions(+), 5 deletions(-) create mode 100644 api/.env.example diff --git a/api/.env.example b/api/.env.example new file mode 100644 index 000000000..90cbef7d6 --- /dev/null +++ b/api/.env.example @@ -0,0 +1,70 @@ +# Environment variables for CodePush Server + +# Storage Configuration +# Set to 'true' to use the local emulator instead of a hosted instance +EMULATED=false + +# Azure Storage Configuration +# The name of your hosted Azure storage instance +AZURE_STORAGE_ACCOUNT= +# The key to your Azure storage instance (if KeyVault credentials are not provided) +AZURE_STORAGE_ACCESS_KEY= + +# Server Configuration +# The URL of your server +SERVER_URL=http://localhost:3000 + +# GitHub OAuth Configuration +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= + +# Microsoft OAuth Configuration +MICROSOFT_CLIENT_ID= +MICROSOFT_CLIENT_SECRET= + +# HTTPS Configuration +# Set to 'true' to enable HTTPS for local deployment +HTTPS= + +# Debugging Configuration +# Turn on CodePush-specific logging of API and Storage requests +LOGGING=false +# Disable the OAuth authentication route +DEBUG_DISABLE_AUTH=false +# Backend id of user to behave as during the debugging session +DEBUG_USER_ID= + +# Redis Configuration +# The IP address where the Redis server is hosted +REDIS_HOST= +# The port which Redis is listening on +REDIS_PORT=6379 +# The key used to authenticate requests to the Redis cache +REDIS_KEY= + +# Unit Testing Configuration +# Set to 'true' to run API unit tests against Azure storage +TEST_AZURE_STORAGE=false +# If TEST_AZURE_STORAGE is set to true, set to the account of the storage you would like to test on +AZURE_STORAGE_ACCOUNT= +# If TEST_AZURE_STORAGE is set to true, set to the access key of the storage you would like to test on +AZURE_STORAGE_ACCESS_KEY= + +# Set to a Azure url to run acquisition tests against that server +AZURE_ACQUISITION_URL= + +# Other Configuration +# Set to 'true' to disable acquisition routes +DISABLE_ACQUISITION=false +# Set to 'true' to disable management routes +DISABLE_MANAGEMENT=false +# Set to 'false' in order to disable account registration +ENABLE_ACCOUNT_REGISTRATION=true +# Set to the max number of megabytes allowed for file uploads +UPLOAD_SIZE_LIMIT_MB=200 + +# To enable generating diffs for releases +ENABLE_PACKAGE_DIFFING=false + +# To enable KeyVault credential resolution +# Add any additional environment variables here diff --git a/api/ENVIRONMENT.md b/api/ENVIRONMENT.md index 0f4067e63..c16f14059 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 21f38bf67..01f7b92e7 100644 --- a/api/README.md +++ b/api/README.md @@ -17,10 +17,11 @@ 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`. +2. Copy the `.env.example` file to a new file named `.env` in the root directory. +3. Fill in the values for each environment variable in the `.env` file according to your development or production setup. +4. Install dependencies by running `npm install`. +5. Build the server by running `npm run build`. +6. Start the server by running `npm run start:env`. By default, local CodePush server runs on HTTP. To run CodePush Server on HTTPS: From 511d0f1ad74b412859844a46af5c91b2e403e869 Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Thu, 10 Oct 2024 12:48:04 +0200 Subject: [PATCH 10/13] chore: improve README formatting for server setup instructions --- api/README.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/api/README.md b/api/README.md index 01f7b92e7..e8f042c3e 100644 --- a/api/README.md +++ b/api/README.md @@ -16,12 +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. -2. Copy the `.env.example` file to a new file named `.env` in the root directory. -3. Fill in the values for each environment variable in the `.env` file according to your development or production setup. -4. Install dependencies by running `npm install`. -5. Build the server by running `npm run build`. -6. 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 f3c4be49ec06aa85271105e89d7d5c1d1ea0367b Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Thu, 10 Oct 2024 14:23:43 +0200 Subject: [PATCH 11/13] chore(gitignore): add Azurite local storage files to ignore --- .gitignore | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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__ From a4f883573474c34e9e990424a50686f06026eefc Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Wed, 13 Nov 2024 12:39:16 +0100 Subject: [PATCH 12/13] chore: update formatting of `.env.example` for better readability --- api/.env.example | 114 ++++++++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 55 deletions(-) diff --git a/api/.env.example b/api/.env.example index 90cbef7d6..7595fe499 100644 --- a/api/.env.example +++ b/api/.env.example @@ -1,70 +1,74 @@ -# Environment variables for CodePush Server +########################################## +# Environment Variables for CodePush Server +########################################## -# Storage Configuration -# Set to 'true' to use the local emulator instead of a hosted instance -EMULATED=false +# ============================== +# Storage Configuration (REQUIRED - choose one) +# ============================== +EMULATED=false # Set to 'true' to use the local emulator -# Azure Storage Configuration -# The name of your hosted Azure storage instance -AZURE_STORAGE_ACCOUNT= -# The key to your Azure storage instance (if KeyVault credentials are not provided) -AZURE_STORAGE_ACCESS_KEY= +# --- Azure Storage Configuration --- +AZURE_STORAGE_ACCOUNT= # Azure storage account name +AZURE_STORAGE_ACCESS_KEY= # Azure storage access key (if KeyVault not used) -# Server Configuration -# The URL of your server -SERVER_URL=http://localhost:3000 +# ============================== +# Server Configuration (REQUIRED) +# ============================== +SERVER_URL=http://localhost:3000 # The URL of your server -# GitHub OAuth Configuration -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= +# ============================== +# Authentication (REQUIRED - at least one provider) +# ============================== -# Microsoft OAuth Configuration -MICROSOFT_CLIENT_ID= -MICROSOFT_CLIENT_SECRET= +# --- GitHub OAuth --- +GITHUB_CLIENT_ID= # GitHub OAuth client ID +GITHUB_CLIENT_SECRET= # GitHub OAuth client secret -# HTTPS Configuration -# Set to 'true' to enable HTTPS for local deployment -HTTPS= +# --- Microsoft OAuth --- +MICROSOFT_CLIENT_ID= # Microsoft OAuth client ID +MICROSOFT_CLIENT_SECRET= # Microsoft OAuth client secret -# Debugging Configuration -# Turn on CodePush-specific logging of API and Storage requests -LOGGING=false -# Disable the OAuth authentication route -DEBUG_DISABLE_AUTH=false -# Backend id of user to behave as during the debugging session -DEBUG_USER_ID= +# ============================== +# 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 -# The IP address where the Redis server is hosted -REDIS_HOST= -# The port which Redis is listening on -REDIS_PORT=6379 -# The key used to authenticate requests to the Redis cache -REDIS_KEY= +# ============================== +REDIS_HOST= # Redis server IP address +REDIS_PORT=6379 # Redis port (default: 6379) +REDIS_KEY= # Redis authentication key +# ============================== # Unit Testing Configuration -# Set to 'true' to run API unit tests against Azure storage -TEST_AZURE_STORAGE=false -# If TEST_AZURE_STORAGE is set to true, set to the account of the storage you would like to test on -AZURE_STORAGE_ACCOUNT= -# If TEST_AZURE_STORAGE is set to true, set to the access key of the storage you would like to test on -AZURE_STORAGE_ACCESS_KEY= +# ============================== +TEST_AZURE_STORAGE=false # Run API unit tests against Azure storage +AZURE_STORAGE_ACCOUNT= # Storage account for testing (if TEST_AZURE_STORAGE=true) +AZURE_STORAGE_ACCESS_KEY= # Storage access key for testing -# Set to a Azure url to run acquisition tests against that server -AZURE_ACQUISITION_URL= +AZURE_ACQUISITION_URL= # URL for acquisition tests +# ============================== # Other Configuration -# Set to 'true' to disable acquisition routes -DISABLE_ACQUISITION=false -# Set to 'true' to disable management routes -DISABLE_MANAGEMENT=false -# Set to 'false' in order to disable account registration -ENABLE_ACCOUNT_REGISTRATION=true -# Set to the max number of megabytes allowed for file uploads -UPLOAD_SIZE_LIMIT_MB=200 - -# To enable generating diffs for releases -ENABLE_PACKAGE_DIFFING=false +# ============================== +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 -# To enable KeyVault credential resolution -# Add any additional environment variables here +# ============================== +# 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) From 1a637a5c674f627238fef9691c0fbf8f9aa58871 Mon Sep 17 00:00:00 2001 From: Piotr Tomczewski Date: Mon, 18 Nov 2024 18:08:54 +0100 Subject: [PATCH 13/13] fix: remove duplicated entries in .example.env --- api/.env.example | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/api/.env.example b/api/.env.example index 7595fe499..352ed8fb9 100644 --- a/api/.env.example +++ b/api/.env.example @@ -50,11 +50,8 @@ REDIS_KEY= # Redis authentication key # ============================== # Unit Testing Configuration # ============================== -TEST_AZURE_STORAGE=false # Run API unit tests against Azure storage -AZURE_STORAGE_ACCOUNT= # Storage account for testing (if TEST_AZURE_STORAGE=true) -AZURE_STORAGE_ACCESS_KEY= # Storage access key for testing - -AZURE_ACQUISITION_URL= # URL for acquisition tests +TEST_AZURE_STORAGE=false # Run API unit tests against Azure storage +AZURE_ACQUISITION_URL= # URL for acquisition tests # ============================== # Other Configuration