From bddd3c57d94e83f8a40b8735fbf3004e2f838c45 Mon Sep 17 00:00:00 2001
From: Joe Gambino <jgambino96@gmail.com>
Date: Mon, 6 Dec 2021 18:37:19 -0800
Subject: [PATCH 1/6] Revert "Revert "[No QA] Ensure bundle version strings are
 compatible before auto-merging CP PR""

---
 .../checkBundleVersionStringMatch/action.yml  |    8 +
 .../checkBundleVersionStringMatch.js          |   16 +
 .../checkBundleVersionStringMatch/index.js    | 1248 +++++++++++++++++
 .github/scripts/buildActions.sh               |    1 +
 .github/workflows/cherryPick.yml              |   19 +-
 5 files changed, 1290 insertions(+), 2 deletions(-)
 create mode 100644 .github/actions/checkBundleVersionStringMatch/action.yml
 create mode 100644 .github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
 create mode 100644 .github/actions/checkBundleVersionStringMatch/index.js

diff --git a/.github/actions/checkBundleVersionStringMatch/action.yml b/.github/actions/checkBundleVersionStringMatch/action.yml
new file mode 100644
index 000000000000..62258adc62b5
--- /dev/null
+++ b/.github/actions/checkBundleVersionStringMatch/action.yml
@@ -0,0 +1,8 @@
+name: 'Check if Bundle Versions Match'
+description: "Check if the CFBundleVersion string is compatible with the CFBundleShortVersionString"
+outputs:
+  BUNDLE_VERSIONS_MATCH:
+    description: Whether or not the bundle versions match
+runs:
+  using: 'node12'
+  main: './index.js'
diff --git a/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js b/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
new file mode 100644
index 000000000000..f601976a2f2a
--- /dev/null
+++ b/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
@@ -0,0 +1,16 @@
+const core = require('@actions/core');
+const {execSync} = require('child_process');
+const {PLIST_PATH} = require('../../libs/nativeVersionUpdater');
+
+const bundleVersion = execSync(`/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" ${PLIST_PATH}`);
+const shortBundleVersion = execSync(`/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" ${PLIST_PATH}`);
+
+console.log(`Bundle Version: ${bundleVersion}`);
+console.log(`Short Bundle Version: ${shortBundleVersion}`);
+if (shortBundleVersion !== (bundleVersion.split('-') || [''])[0]) {
+    console.log('Bundle Versions do not match');
+    core.setOutput('BUNDLE_VERSIONS_MATCH', false);
+} else {
+    console.log('Bundle Versions match');
+    core.setOutput('BUNDLE_VERSIONS_MATCH', true);
+}
diff --git a/.github/actions/checkBundleVersionStringMatch/index.js b/.github/actions/checkBundleVersionStringMatch/index.js
new file mode 100644
index 000000000000..960ac54b3fbf
--- /dev/null
+++ b/.github/actions/checkBundleVersionStringMatch/index.js
@@ -0,0 +1,1248 @@
+/**
+ * NOTE: This is a compiled file. DO NOT directly edit this file.
+ */
+module.exports =
+/******/ (() => { // webpackBootstrap
+/******/ 	var __webpack_modules__ = ({
+
+/***/ 330:
+/***/ ((__unused_webpack_module, __unused_webpack_exports, __nccwpck_require__) => {
+
+const core = __nccwpck_require__(186);
+const {execSync} = __nccwpck_require__(129);
+const {PLIST_PATH} = __nccwpck_require__(322);
+
+const bundleVersion = execSync(`/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" ${PLIST_PATH}`);
+const shortBundleVersion = execSync(`/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" ${PLIST_PATH}`);
+
+console.log(`Bundle Version: ${bundleVersion}`);
+console.log(`Short Bundle Version: ${shortBundleVersion}`);
+if (shortBundleVersion !== (bundleVersion.split('-') || [''])[0]) {
+    console.log('Bundle Versions do not match');
+    core.setOutput('BUNDLE_VERSIONS_MATCH', false);
+} else {
+    console.log('Bundle Versions match');
+    core.setOutput('BUNDLE_VERSIONS_MATCH', true);
+}
+
+
+/***/ }),
+
+/***/ 322:
+/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
+
+const {execSync} = __nccwpck_require__(129);
+const fs = __nccwpck_require__(747).promises;
+const path = __nccwpck_require__(622);
+const getMajorVersion = __nccwpck_require__(688);
+const getMinorVersion = __nccwpck_require__(447);
+const getPatchVersion = __nccwpck_require__(866);
+const getBuildVersion = __nccwpck_require__(16);
+
+// Filepath constants
+const BUILD_GRADLE_PATH = process.env.NODE_ENV === 'test'
+    ? path.resolve(__dirname, '../../android/app/build.gradle')
+    : './android/app/build.gradle';
+const PLIST_PATH = './ios/NewExpensify/Info.plist';
+const PLIST_PATH_TEST = './ios/NewExpensifyTests/Info.plist';
+
+exports.BUILD_GRADLE_PATH = BUILD_GRADLE_PATH;
+exports.PLIST_PATH = PLIST_PATH;
+exports.PLIST_PATH_TEST = PLIST_PATH_TEST;
+
+/**
+ * Pad a number to be two digits (with leading zeros if necessary).
+ *
+ * @param {Number} number - Must be an integer.
+ * @returns {String} - A string representation of the number with length 2.
+ */
+function padToTwoDigits(number) {
+    if (number >= 10) {
+        return number.toString();
+    }
+    return `0${number.toString()}`;
+}
+
+/**
+ * Generate the 10-digit versionCode for android.
+ * This version code allocates two digits each for PREFIX, MAJOR, MINOR, PATCH, and BUILD versions.
+ * As a result, our max version is 99.99.99-99.
+ *
+ * @param {String} npmVersion
+ * @returns {String}
+ */
+exports.generateAndroidVersionCode = function generateAndroidVersionCode(npmVersion) {
+    // All Android versions will be prefixed with '10' due to previous versioning
+    const prefix = '10';
+    return ''.concat(
+        prefix,
+        padToTwoDigits(getMajorVersion(npmVersion) || 0),
+        padToTwoDigits(getMinorVersion(npmVersion) || 0),
+        padToTwoDigits(getPatchVersion(npmVersion) || 0),
+        padToTwoDigits(getBuildVersion(npmVersion) || 0),
+    );
+};
+
+
+/**
+ * Update the Android app versionName and versionCode.
+ *
+ * @param {String} versionName
+ * @param {String} versionCode
+ * @returns {Promise}
+ */
+exports.updateAndroidVersion = function updateAndroidVersion(versionName, versionCode) {
+    console.log('Updating android:', `versionName: ${versionName}`, `versionCode: ${versionCode}`);
+    return fs.readFile(BUILD_GRADLE_PATH, {encoding: 'utf8'})
+        .then((content) => {
+            let updatedContent = content.toString().replace(/versionName "([0-9.-]*)"/, `versionName "${versionName}"`);
+            return updatedContent = updatedContent.replace(/versionCode ([0-9]*)/, `versionCode ${versionCode}`);
+        })
+        .then(updatedContent => fs.writeFile(BUILD_GRADLE_PATH, updatedContent, {encoding: 'utf8'}));
+};
+
+/**
+ * Update the iOS app version.
+ * Updates the CFBundleShortVersionString and the CFBundleVersion.
+ *
+ * @param {String} version
+ * @returns {String}
+ */
+exports.updateiOSVersion = function updateiOSVersion(version) {
+    const shortVersion = version.split('-')[0];
+    const cfVersion = version.includes('-') ? version.replace('-', '.') : `${version}.0`;
+    console.log('Updating iOS', `CFBundleShortVersionString: ${shortVersion}`, `CFBundleVersion: ${cfVersion}`);
+
+    // Update Plists
+    execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${shortVersion}" ${PLIST_PATH}`);
+    execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${shortVersion}" ${PLIST_PATH_TEST}`);
+    execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${cfVersion}" ${PLIST_PATH}`);
+    execSync(`/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${cfVersion}" ${PLIST_PATH_TEST}`);
+
+    // Return the cfVersion so we can set the NEW_IOS_VERSION in ios.yml
+    return cfVersion;
+};
+
+
+/***/ }),
+
+/***/ 351:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+var __importStar = (this && this.__importStar) || function (mod) {
+    if (mod && mod.__esModule) return mod;
+    var result = {};
+    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
+    result["default"] = mod;
+    return result;
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+const os = __importStar(__nccwpck_require__(87));
+const utils_1 = __nccwpck_require__(278);
+/**
+ * Commands
+ *
+ * Command Format:
+ *   ::name key=value,key=value::message
+ *
+ * Examples:
+ *   ::warning::This is the message
+ *   ::set-env name=MY_VAR::some value
+ */
+function issueCommand(command, properties, message) {
+    const cmd = new Command(command, properties, message);
+    process.stdout.write(cmd.toString() + os.EOL);
+}
+exports.issueCommand = issueCommand;
+function issue(name, message = '') {
+    issueCommand(name, {}, message);
+}
+exports.issue = issue;
+const CMD_STRING = '::';
+class Command {
+    constructor(command, properties, message) {
+        if (!command) {
+            command = 'missing.command';
+        }
+        this.command = command;
+        this.properties = properties;
+        this.message = message;
+    }
+    toString() {
+        let cmdStr = CMD_STRING + this.command;
+        if (this.properties && Object.keys(this.properties).length > 0) {
+            cmdStr += ' ';
+            let first = true;
+            for (const key in this.properties) {
+                if (this.properties.hasOwnProperty(key)) {
+                    const val = this.properties[key];
+                    if (val) {
+                        if (first) {
+                            first = false;
+                        }
+                        else {
+                            cmdStr += ',';
+                        }
+                        cmdStr += `${key}=${escapeProperty(val)}`;
+                    }
+                }
+            }
+        }
+        cmdStr += `${CMD_STRING}${escapeData(this.message)}`;
+        return cmdStr;
+    }
+}
+function escapeData(s) {
+    return utils_1.toCommandValue(s)
+        .replace(/%/g, '%25')
+        .replace(/\r/g, '%0D')
+        .replace(/\n/g, '%0A');
+}
+function escapeProperty(s) {
+    return utils_1.toCommandValue(s)
+        .replace(/%/g, '%25')
+        .replace(/\r/g, '%0D')
+        .replace(/\n/g, '%0A')
+        .replace(/:/g, '%3A')
+        .replace(/,/g, '%2C');
+}
+//# sourceMappingURL=command.js.map
+
+/***/ }),
+
+/***/ 186:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
+    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
+    return new (P || (P = Promise))(function (resolve, reject) {
+        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
+        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
+        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
+        step((generator = generator.apply(thisArg, _arguments || [])).next());
+    });
+};
+var __importStar = (this && this.__importStar) || function (mod) {
+    if (mod && mod.__esModule) return mod;
+    var result = {};
+    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
+    result["default"] = mod;
+    return result;
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+const command_1 = __nccwpck_require__(351);
+const file_command_1 = __nccwpck_require__(717);
+const utils_1 = __nccwpck_require__(278);
+const os = __importStar(__nccwpck_require__(87));
+const path = __importStar(__nccwpck_require__(622));
+/**
+ * The code to exit an action
+ */
+var ExitCode;
+(function (ExitCode) {
+    /**
+     * A code indicating that the action was successful
+     */
+    ExitCode[ExitCode["Success"] = 0] = "Success";
+    /**
+     * A code indicating that the action was a failure
+     */
+    ExitCode[ExitCode["Failure"] = 1] = "Failure";
+})(ExitCode = exports.ExitCode || (exports.ExitCode = {}));
+//-----------------------------------------------------------------------
+// Variables
+//-----------------------------------------------------------------------
+/**
+ * Sets env variable for this action and future actions in the job
+ * @param name the name of the variable to set
+ * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function exportVariable(name, val) {
+    const convertedVal = utils_1.toCommandValue(val);
+    process.env[name] = convertedVal;
+    const filePath = process.env['GITHUB_ENV'] || '';
+    if (filePath) {
+        const delimiter = '_GitHubActionsFileCommandDelimeter_';
+        const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`;
+        file_command_1.issueCommand('ENV', commandValue);
+    }
+    else {
+        command_1.issueCommand('set-env', { name }, convertedVal);
+    }
+}
+exports.exportVariable = exportVariable;
+/**
+ * Registers a secret which will get masked from logs
+ * @param secret value of the secret
+ */
+function setSecret(secret) {
+    command_1.issueCommand('add-mask', {}, secret);
+}
+exports.setSecret = setSecret;
+/**
+ * Prepends inputPath to the PATH (for this action and future actions)
+ * @param inputPath
+ */
+function addPath(inputPath) {
+    const filePath = process.env['GITHUB_PATH'] || '';
+    if (filePath) {
+        file_command_1.issueCommand('PATH', inputPath);
+    }
+    else {
+        command_1.issueCommand('add-path', {}, inputPath);
+    }
+    process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`;
+}
+exports.addPath = addPath;
+/**
+ * Gets the value of an input.  The value is also trimmed.
+ *
+ * @param     name     name of the input to get
+ * @param     options  optional. See InputOptions.
+ * @returns   string
+ */
+function getInput(name, options) {
+    const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || '';
+    if (options && options.required && !val) {
+        throw new Error(`Input required and not supplied: ${name}`);
+    }
+    return val.trim();
+}
+exports.getInput = getInput;
+/**
+ * Sets the value of an output.
+ *
+ * @param     name     name of the output to set
+ * @param     value    value to store. Non-string values will be converted to a string via JSON.stringify
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function setOutput(name, value) {
+    command_1.issueCommand('set-output', { name }, value);
+}
+exports.setOutput = setOutput;
+/**
+ * Enables or disables the echoing of commands into stdout for the rest of the step.
+ * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set.
+ *
+ */
+function setCommandEcho(enabled) {
+    command_1.issue('echo', enabled ? 'on' : 'off');
+}
+exports.setCommandEcho = setCommandEcho;
+//-----------------------------------------------------------------------
+// Results
+//-----------------------------------------------------------------------
+/**
+ * Sets the action status to failed.
+ * When the action exits it will be with an exit code of 1
+ * @param message add error issue message
+ */
+function setFailed(message) {
+    process.exitCode = ExitCode.Failure;
+    error(message);
+}
+exports.setFailed = setFailed;
+//-----------------------------------------------------------------------
+// Logging Commands
+//-----------------------------------------------------------------------
+/**
+ * Gets whether Actions Step Debug is on or not
+ */
+function isDebug() {
+    return process.env['RUNNER_DEBUG'] === '1';
+}
+exports.isDebug = isDebug;
+/**
+ * Writes debug message to user log
+ * @param message debug message
+ */
+function debug(message) {
+    command_1.issueCommand('debug', {}, message);
+}
+exports.debug = debug;
+/**
+ * Adds an error issue
+ * @param message error issue message. Errors will be converted to string via toString()
+ */
+function error(message) {
+    command_1.issue('error', message instanceof Error ? message.toString() : message);
+}
+exports.error = error;
+/**
+ * Adds an warning issue
+ * @param message warning issue message. Errors will be converted to string via toString()
+ */
+function warning(message) {
+    command_1.issue('warning', message instanceof Error ? message.toString() : message);
+}
+exports.warning = warning;
+/**
+ * Writes info to log with console.log.
+ * @param message info message
+ */
+function info(message) {
+    process.stdout.write(message + os.EOL);
+}
+exports.info = info;
+/**
+ * Begin an output group.
+ *
+ * Output until the next `groupEnd` will be foldable in this group
+ *
+ * @param name The name of the output group
+ */
+function startGroup(name) {
+    command_1.issue('group', name);
+}
+exports.startGroup = startGroup;
+/**
+ * End an output group.
+ */
+function endGroup() {
+    command_1.issue('endgroup');
+}
+exports.endGroup = endGroup;
+/**
+ * Wrap an asynchronous function call in a group.
+ *
+ * Returns the same type as the function itself.
+ *
+ * @param name The name of the group
+ * @param fn The function to wrap in the group
+ */
+function group(name, fn) {
+    return __awaiter(this, void 0, void 0, function* () {
+        startGroup(name);
+        let result;
+        try {
+            result = yield fn();
+        }
+        finally {
+            endGroup();
+        }
+        return result;
+    });
+}
+exports.group = group;
+//-----------------------------------------------------------------------
+// Wrapper action state
+//-----------------------------------------------------------------------
+/**
+ * Saves state for current action, the state can only be retrieved by this action's post job execution.
+ *
+ * @param     name     name of the state to store
+ * @param     value    value to store. Non-string values will be converted to a string via JSON.stringify
+ */
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function saveState(name, value) {
+    command_1.issueCommand('save-state', { name }, value);
+}
+exports.saveState = saveState;
+/**
+ * Gets the value of an state set by this action's main execution.
+ *
+ * @param     name     name of the state to get
+ * @returns   string
+ */
+function getState(name) {
+    return process.env[`STATE_${name}`] || '';
+}
+exports.getState = getState;
+//# sourceMappingURL=core.js.map
+
+/***/ }),
+
+/***/ 717:
+/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
+
+"use strict";
+
+// For internal use, subject to change.
+var __importStar = (this && this.__importStar) || function (mod) {
+    if (mod && mod.__esModule) return mod;
+    var result = {};
+    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
+    result["default"] = mod;
+    return result;
+};
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+// We use any as a valid input type
+/* eslint-disable @typescript-eslint/no-explicit-any */
+const fs = __importStar(__nccwpck_require__(747));
+const os = __importStar(__nccwpck_require__(87));
+const utils_1 = __nccwpck_require__(278);
+function issueCommand(command, message) {
+    const filePath = process.env[`GITHUB_${command}`];
+    if (!filePath) {
+        throw new Error(`Unable to find environment variable for file command ${command}`);
+    }
+    if (!fs.existsSync(filePath)) {
+        throw new Error(`Missing file at path: ${filePath}`);
+    }
+    fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, {
+        encoding: 'utf8'
+    });
+}
+exports.issueCommand = issueCommand;
+//# sourceMappingURL=file-command.js.map
+
+/***/ }),
+
+/***/ 278:
+/***/ ((__unused_webpack_module, exports) => {
+
+"use strict";
+
+// We use any as a valid input type
+/* eslint-disable @typescript-eslint/no-explicit-any */
+Object.defineProperty(exports, "__esModule", ({ value: true }));
+/**
+ * Sanitizes an input into a string so it can be passed into issueCommand safely
+ * @param input input to sanitize into a string
+ */
+function toCommandValue(input) {
+    if (input === null || input === undefined) {
+        return '';
+    }
+    else if (typeof input === 'string' || input instanceof String) {
+        return input;
+    }
+    return JSON.stringify(input);
+}
+exports.toCommandValue = toCommandValue;
+//# sourceMappingURL=utils.js.map
+
+/***/ }),
+
+/***/ 88:
+/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
+
+const debug = __nccwpck_require__(427)
+const { MAX_LENGTH, MAX_SAFE_INTEGER } = __nccwpck_require__(293)
+const { re, t } = __nccwpck_require__(523)
+
+const parseOptions = __nccwpck_require__(785)
+const { compareIdentifiers } = __nccwpck_require__(463)
+class SemVer {
+  constructor (version, options) {
+    options = parseOptions(options)
+
+    if (version instanceof SemVer) {
+      if (version.loose === !!options.loose &&
+          version.includePrerelease === !!options.includePrerelease) {
+        return version
+      } else {
+        version = version.version
+      }
+    } else if (typeof version !== 'string') {
+      throw new TypeError(`Invalid Version: ${version}`)
+    }
+
+    if (version.length > MAX_LENGTH) {
+      throw new TypeError(
+        `version is longer than ${MAX_LENGTH} characters`
+      )
+    }
+
+    debug('SemVer', version, options)
+    this.options = options
+    this.loose = !!options.loose
+    // this isn't actually relevant for versions, but keep it so that we
+    // don't run into trouble passing this.options around.
+    this.includePrerelease = !!options.includePrerelease
+
+    const m = version.trim().match(options.loose ? re[t.LOOSE] : re[t.FULL])
+
+    if (!m) {
+      throw new TypeError(`Invalid Version: ${version}`)
+    }
+
+    this.raw = version
+
+    // these are actually numbers
+    this.major = +m[1]
+    this.minor = +m[2]
+    this.patch = +m[3]
+
+    if (this.major > MAX_SAFE_INTEGER || this.major < 0) {
+      throw new TypeError('Invalid major version')
+    }
+
+    if (this.minor > MAX_SAFE_INTEGER || this.minor < 0) {
+      throw new TypeError('Invalid minor version')
+    }
+
+    if (this.patch > MAX_SAFE_INTEGER || this.patch < 0) {
+      throw new TypeError('Invalid patch version')
+    }
+
+    // numberify any prerelease numeric ids
+    if (!m[4]) {
+      this.prerelease = []
+    } else {
+      this.prerelease = m[4].split('.').map((id) => {
+        if (/^[0-9]+$/.test(id)) {
+          const num = +id
+          if (num >= 0 && num < MAX_SAFE_INTEGER) {
+            return num
+          }
+        }
+        return id
+      })
+    }
+
+    this.build = m[5] ? m[5].split('.') : []
+    this.format()
+  }
+
+  format () {
+    this.version = `${this.major}.${this.minor}.${this.patch}`
+    if (this.prerelease.length) {
+      this.version += `-${this.prerelease.join('.')}`
+    }
+    return this.version
+  }
+
+  toString () {
+    return this.version
+  }
+
+  compare (other) {
+    debug('SemVer.compare', this.version, this.options, other)
+    if (!(other instanceof SemVer)) {
+      if (typeof other === 'string' && other === this.version) {
+        return 0
+      }
+      other = new SemVer(other, this.options)
+    }
+
+    if (other.version === this.version) {
+      return 0
+    }
+
+    return this.compareMain(other) || this.comparePre(other)
+  }
+
+  compareMain (other) {
+    if (!(other instanceof SemVer)) {
+      other = new SemVer(other, this.options)
+    }
+
+    return (
+      compareIdentifiers(this.major, other.major) ||
+      compareIdentifiers(this.minor, other.minor) ||
+      compareIdentifiers(this.patch, other.patch)
+    )
+  }
+
+  comparePre (other) {
+    if (!(other instanceof SemVer)) {
+      other = new SemVer(other, this.options)
+    }
+
+    // NOT having a prerelease is > having one
+    if (this.prerelease.length && !other.prerelease.length) {
+      return -1
+    } else if (!this.prerelease.length && other.prerelease.length) {
+      return 1
+    } else if (!this.prerelease.length && !other.prerelease.length) {
+      return 0
+    }
+
+    let i = 0
+    do {
+      const a = this.prerelease[i]
+      const b = other.prerelease[i]
+      debug('prerelease compare', i, a, b)
+      if (a === undefined && b === undefined) {
+        return 0
+      } else if (b === undefined) {
+        return 1
+      } else if (a === undefined) {
+        return -1
+      } else if (a === b) {
+        continue
+      } else {
+        return compareIdentifiers(a, b)
+      }
+    } while (++i)
+  }
+
+  compareBuild (other) {
+    if (!(other instanceof SemVer)) {
+      other = new SemVer(other, this.options)
+    }
+
+    let i = 0
+    do {
+      const a = this.build[i]
+      const b = other.build[i]
+      debug('prerelease compare', i, a, b)
+      if (a === undefined && b === undefined) {
+        return 0
+      } else if (b === undefined) {
+        return 1
+      } else if (a === undefined) {
+        return -1
+      } else if (a === b) {
+        continue
+      } else {
+        return compareIdentifiers(a, b)
+      }
+    } while (++i)
+  }
+
+  // preminor will bump the version up to the next minor release, and immediately
+  // down to pre-release. premajor and prepatch work the same way.
+  inc (release, identifier) {
+    switch (release) {
+      case 'premajor':
+        this.prerelease.length = 0
+        this.patch = 0
+        this.minor = 0
+        this.major++
+        this.inc('pre', identifier)
+        break
+      case 'preminor':
+        this.prerelease.length = 0
+        this.patch = 0
+        this.minor++
+        this.inc('pre', identifier)
+        break
+      case 'prepatch':
+        // If this is already a prerelease, it will bump to the next version
+        // drop any prereleases that might already exist, since they are not
+        // relevant at this point.
+        this.prerelease.length = 0
+        this.inc('patch', identifier)
+        this.inc('pre', identifier)
+        break
+      // If the input is a non-prerelease version, this acts the same as
+      // prepatch.
+      case 'prerelease':
+        if (this.prerelease.length === 0) {
+          this.inc('patch', identifier)
+        }
+        this.inc('pre', identifier)
+        break
+
+      case 'major':
+        // If this is a pre-major version, bump up to the same major version.
+        // Otherwise increment major.
+        // 1.0.0-5 bumps to 1.0.0
+        // 1.1.0 bumps to 2.0.0
+        if (
+          this.minor !== 0 ||
+          this.patch !== 0 ||
+          this.prerelease.length === 0
+        ) {
+          this.major++
+        }
+        this.minor = 0
+        this.patch = 0
+        this.prerelease = []
+        break
+      case 'minor':
+        // If this is a pre-minor version, bump up to the same minor version.
+        // Otherwise increment minor.
+        // 1.2.0-5 bumps to 1.2.0
+        // 1.2.1 bumps to 1.3.0
+        if (this.patch !== 0 || this.prerelease.length === 0) {
+          this.minor++
+        }
+        this.patch = 0
+        this.prerelease = []
+        break
+      case 'patch':
+        // If this is not a pre-release version, it will increment the patch.
+        // If it is a pre-release it will bump up to the same patch version.
+        // 1.2.0-5 patches to 1.2.0
+        // 1.2.0 patches to 1.2.1
+        if (this.prerelease.length === 0) {
+          this.patch++
+        }
+        this.prerelease = []
+        break
+      // This probably shouldn't be used publicly.
+      // 1.0.0 'pre' would become 1.0.0-0 which is the wrong direction.
+      case 'pre':
+        if (this.prerelease.length === 0) {
+          this.prerelease = [0]
+        } else {
+          let i = this.prerelease.length
+          while (--i >= 0) {
+            if (typeof this.prerelease[i] === 'number') {
+              this.prerelease[i]++
+              i = -2
+            }
+          }
+          if (i === -1) {
+            // didn't increment anything
+            this.prerelease.push(0)
+          }
+        }
+        if (identifier) {
+          // 1.2.0-beta.1 bumps to 1.2.0-beta.2,
+          // 1.2.0-beta.fooblz or 1.2.0-beta bumps to 1.2.0-beta.0
+          if (this.prerelease[0] === identifier) {
+            if (isNaN(this.prerelease[1])) {
+              this.prerelease = [identifier, 0]
+            }
+          } else {
+            this.prerelease = [identifier, 0]
+          }
+        }
+        break
+
+      default:
+        throw new Error(`invalid increment argument: ${release}`)
+    }
+    this.format()
+    this.raw = this.version
+    return this
+  }
+}
+
+module.exports = SemVer
+
+
+/***/ }),
+
+/***/ 688:
+/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
+
+const SemVer = __nccwpck_require__(88)
+const major = (a, loose) => new SemVer(a, loose).major
+module.exports = major
+
+
+/***/ }),
+
+/***/ 447:
+/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
+
+const SemVer = __nccwpck_require__(88)
+const minor = (a, loose) => new SemVer(a, loose).minor
+module.exports = minor
+
+
+/***/ }),
+
+/***/ 925:
+/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
+
+const {MAX_LENGTH} = __nccwpck_require__(293)
+const { re, t } = __nccwpck_require__(523)
+const SemVer = __nccwpck_require__(88)
+
+const parseOptions = __nccwpck_require__(785)
+const parse = (version, options) => {
+  options = parseOptions(options)
+
+  if (version instanceof SemVer) {
+    return version
+  }
+
+  if (typeof version !== 'string') {
+    return null
+  }
+
+  if (version.length > MAX_LENGTH) {
+    return null
+  }
+
+  const r = options.loose ? re[t.LOOSE] : re[t.FULL]
+  if (!r.test(version)) {
+    return null
+  }
+
+  try {
+    return new SemVer(version, options)
+  } catch (er) {
+    return null
+  }
+}
+
+module.exports = parse
+
+
+/***/ }),
+
+/***/ 866:
+/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
+
+const SemVer = __nccwpck_require__(88)
+const patch = (a, loose) => new SemVer(a, loose).patch
+module.exports = patch
+
+
+/***/ }),
+
+/***/ 16:
+/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
+
+const parse = __nccwpck_require__(925)
+const prerelease = (version, options) => {
+  const parsed = parse(version, options)
+  return (parsed && parsed.prerelease.length) ? parsed.prerelease : null
+}
+module.exports = prerelease
+
+
+/***/ }),
+
+/***/ 293:
+/***/ ((module) => {
+
+// Note: this is the semver.org version of the spec that it implements
+// Not necessarily the package version of this code.
+const SEMVER_SPEC_VERSION = '2.0.0'
+
+const MAX_LENGTH = 256
+const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER ||
+  /* istanbul ignore next */ 9007199254740991
+
+// Max safe segment length for coercion.
+const MAX_SAFE_COMPONENT_LENGTH = 16
+
+module.exports = {
+  SEMVER_SPEC_VERSION,
+  MAX_LENGTH,
+  MAX_SAFE_INTEGER,
+  MAX_SAFE_COMPONENT_LENGTH
+}
+
+
+/***/ }),
+
+/***/ 427:
+/***/ ((module) => {
+
+const debug = (
+  typeof process === 'object' &&
+  process.env &&
+  process.env.NODE_DEBUG &&
+  /\bsemver\b/i.test(process.env.NODE_DEBUG)
+) ? (...args) => console.error('SEMVER', ...args)
+  : () => {}
+
+module.exports = debug
+
+
+/***/ }),
+
+/***/ 463:
+/***/ ((module) => {
+
+const numeric = /^[0-9]+$/
+const compareIdentifiers = (a, b) => {
+  const anum = numeric.test(a)
+  const bnum = numeric.test(b)
+
+  if (anum && bnum) {
+    a = +a
+    b = +b
+  }
+
+  return a === b ? 0
+    : (anum && !bnum) ? -1
+    : (bnum && !anum) ? 1
+    : a < b ? -1
+    : 1
+}
+
+const rcompareIdentifiers = (a, b) => compareIdentifiers(b, a)
+
+module.exports = {
+  compareIdentifiers,
+  rcompareIdentifiers
+}
+
+
+/***/ }),
+
+/***/ 785:
+/***/ ((module) => {
+
+// parse out just the options we care about so we always get a consistent
+// obj with keys in a consistent order.
+const opts = ['includePrerelease', 'loose', 'rtl']
+const parseOptions = options =>
+  !options ? {}
+  : typeof options !== 'object' ? { loose: true }
+  : opts.filter(k => options[k]).reduce((options, k) => {
+    options[k] = true
+    return options
+  }, {})
+module.exports = parseOptions
+
+
+/***/ }),
+
+/***/ 523:
+/***/ ((module, exports, __nccwpck_require__) => {
+
+const { MAX_SAFE_COMPONENT_LENGTH } = __nccwpck_require__(293)
+const debug = __nccwpck_require__(427)
+exports = module.exports = {}
+
+// The actual regexps go on exports.re
+const re = exports.re = []
+const src = exports.src = []
+const t = exports.t = {}
+let R = 0
+
+const createToken = (name, value, isGlobal) => {
+  const index = R++
+  debug(index, value)
+  t[name] = index
+  src[index] = value
+  re[index] = new RegExp(value, isGlobal ? 'g' : undefined)
+}
+
+// The following Regular Expressions can be used for tokenizing,
+// validating, and parsing SemVer version strings.
+
+// ## Numeric Identifier
+// A single `0`, or a non-zero digit followed by zero or more digits.
+
+createToken('NUMERICIDENTIFIER', '0|[1-9]\\d*')
+createToken('NUMERICIDENTIFIERLOOSE', '[0-9]+')
+
+// ## Non-numeric Identifier
+// Zero or more digits, followed by a letter or hyphen, and then zero or
+// more letters, digits, or hyphens.
+
+createToken('NONNUMERICIDENTIFIER', '\\d*[a-zA-Z-][a-zA-Z0-9-]*')
+
+// ## Main Version
+// Three dot-separated numeric identifiers.
+
+createToken('MAINVERSION', `(${src[t.NUMERICIDENTIFIER]})\\.` +
+                   `(${src[t.NUMERICIDENTIFIER]})\\.` +
+                   `(${src[t.NUMERICIDENTIFIER]})`)
+
+createToken('MAINVERSIONLOOSE', `(${src[t.NUMERICIDENTIFIERLOOSE]})\\.` +
+                        `(${src[t.NUMERICIDENTIFIERLOOSE]})\\.` +
+                        `(${src[t.NUMERICIDENTIFIERLOOSE]})`)
+
+// ## Pre-release Version Identifier
+// A numeric identifier, or a non-numeric identifier.
+
+createToken('PRERELEASEIDENTIFIER', `(?:${src[t.NUMERICIDENTIFIER]
+}|${src[t.NONNUMERICIDENTIFIER]})`)
+
+createToken('PRERELEASEIDENTIFIERLOOSE', `(?:${src[t.NUMERICIDENTIFIERLOOSE]
+}|${src[t.NONNUMERICIDENTIFIER]})`)
+
+// ## Pre-release Version
+// Hyphen, followed by one or more dot-separated pre-release version
+// identifiers.
+
+createToken('PRERELEASE', `(?:-(${src[t.PRERELEASEIDENTIFIER]
+}(?:\\.${src[t.PRERELEASEIDENTIFIER]})*))`)
+
+createToken('PRERELEASELOOSE', `(?:-?(${src[t.PRERELEASEIDENTIFIERLOOSE]
+}(?:\\.${src[t.PRERELEASEIDENTIFIERLOOSE]})*))`)
+
+// ## Build Metadata Identifier
+// Any combination of digits, letters, or hyphens.
+
+createToken('BUILDIDENTIFIER', '[0-9A-Za-z-]+')
+
+// ## Build Metadata
+// Plus sign, followed by one or more period-separated build metadata
+// identifiers.
+
+createToken('BUILD', `(?:\\+(${src[t.BUILDIDENTIFIER]
+}(?:\\.${src[t.BUILDIDENTIFIER]})*))`)
+
+// ## Full Version String
+// A main version, followed optionally by a pre-release version and
+// build metadata.
+
+// Note that the only major, minor, patch, and pre-release sections of
+// the version string are capturing groups.  The build metadata is not a
+// capturing group, because it should not ever be used in version
+// comparison.
+
+createToken('FULLPLAIN', `v?${src[t.MAINVERSION]
+}${src[t.PRERELEASE]}?${
+  src[t.BUILD]}?`)
+
+createToken('FULL', `^${src[t.FULLPLAIN]}$`)
+
+// like full, but allows v1.2.3 and =1.2.3, which people do sometimes.
+// also, 1.0.0alpha1 (prerelease without the hyphen) which is pretty
+// common in the npm registry.
+createToken('LOOSEPLAIN', `[v=\\s]*${src[t.MAINVERSIONLOOSE]
+}${src[t.PRERELEASELOOSE]}?${
+  src[t.BUILD]}?`)
+
+createToken('LOOSE', `^${src[t.LOOSEPLAIN]}$`)
+
+createToken('GTLT', '((?:<|>)?=?)')
+
+// Something like "2.*" or "1.2.x".
+// Note that "x.x" is a valid xRange identifer, meaning "any version"
+// Only the first item is strictly required.
+createToken('XRANGEIDENTIFIERLOOSE', `${src[t.NUMERICIDENTIFIERLOOSE]}|x|X|\\*`)
+createToken('XRANGEIDENTIFIER', `${src[t.NUMERICIDENTIFIER]}|x|X|\\*`)
+
+createToken('XRANGEPLAIN', `[v=\\s]*(${src[t.XRANGEIDENTIFIER]})` +
+                   `(?:\\.(${src[t.XRANGEIDENTIFIER]})` +
+                   `(?:\\.(${src[t.XRANGEIDENTIFIER]})` +
+                   `(?:${src[t.PRERELEASE]})?${
+                     src[t.BUILD]}?` +
+                   `)?)?`)
+
+createToken('XRANGEPLAINLOOSE', `[v=\\s]*(${src[t.XRANGEIDENTIFIERLOOSE]})` +
+                        `(?:\\.(${src[t.XRANGEIDENTIFIERLOOSE]})` +
+                        `(?:\\.(${src[t.XRANGEIDENTIFIERLOOSE]})` +
+                        `(?:${src[t.PRERELEASELOOSE]})?${
+                          src[t.BUILD]}?` +
+                        `)?)?`)
+
+createToken('XRANGE', `^${src[t.GTLT]}\\s*${src[t.XRANGEPLAIN]}$`)
+createToken('XRANGELOOSE', `^${src[t.GTLT]}\\s*${src[t.XRANGEPLAINLOOSE]}$`)
+
+// Coercion.
+// Extract anything that could conceivably be a part of a valid semver
+createToken('COERCE', `${'(^|[^\\d])' +
+              '(\\d{1,'}${MAX_SAFE_COMPONENT_LENGTH}})` +
+              `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` +
+              `(?:\\.(\\d{1,${MAX_SAFE_COMPONENT_LENGTH}}))?` +
+              `(?:$|[^\\d])`)
+createToken('COERCERTL', src[t.COERCE], true)
+
+// Tilde ranges.
+// Meaning is "reasonably at or greater than"
+createToken('LONETILDE', '(?:~>?)')
+
+createToken('TILDETRIM', `(\\s*)${src[t.LONETILDE]}\\s+`, true)
+exports.tildeTrimReplace = '$1~'
+
+createToken('TILDE', `^${src[t.LONETILDE]}${src[t.XRANGEPLAIN]}$`)
+createToken('TILDELOOSE', `^${src[t.LONETILDE]}${src[t.XRANGEPLAINLOOSE]}$`)
+
+// Caret ranges.
+// Meaning is "at least and backwards compatible with"
+createToken('LONECARET', '(?:\\^)')
+
+createToken('CARETTRIM', `(\\s*)${src[t.LONECARET]}\\s+`, true)
+exports.caretTrimReplace = '$1^'
+
+createToken('CARET', `^${src[t.LONECARET]}${src[t.XRANGEPLAIN]}$`)
+createToken('CARETLOOSE', `^${src[t.LONECARET]}${src[t.XRANGEPLAINLOOSE]}$`)
+
+// A simple gt/lt/eq thing, or just "" to indicate "any version"
+createToken('COMPARATORLOOSE', `^${src[t.GTLT]}\\s*(${src[t.LOOSEPLAIN]})$|^$`)
+createToken('COMPARATOR', `^${src[t.GTLT]}\\s*(${src[t.FULLPLAIN]})$|^$`)
+
+// An expression to strip any whitespace between the gtlt and the thing
+// it modifies, so that `> 1.2.3` ==> `>1.2.3`
+createToken('COMPARATORTRIM', `(\\s*)${src[t.GTLT]
+}\\s*(${src[t.LOOSEPLAIN]}|${src[t.XRANGEPLAIN]})`, true)
+exports.comparatorTrimReplace = '$1$2$3'
+
+// Something like `1.2.3 - 1.2.4`
+// Note that these all use the loose form, because they'll be
+// checked against either the strict or loose comparator form
+// later.
+createToken('HYPHENRANGE', `^\\s*(${src[t.XRANGEPLAIN]})` +
+                   `\\s+-\\s+` +
+                   `(${src[t.XRANGEPLAIN]})` +
+                   `\\s*$`)
+
+createToken('HYPHENRANGELOOSE', `^\\s*(${src[t.XRANGEPLAINLOOSE]})` +
+                        `\\s+-\\s+` +
+                        `(${src[t.XRANGEPLAINLOOSE]})` +
+                        `\\s*$`)
+
+// Star ranges basically just allow anything at all.
+createToken('STAR', '(<|>)?=?\\s*\\*')
+// >=0.0.0 is like a star
+createToken('GTE0', '^\\s*>=\\s*0\.0\.0\\s*$')
+createToken('GTE0PRE', '^\\s*>=\\s*0\.0\.0-0\\s*$')
+
+
+/***/ }),
+
+/***/ 129:
+/***/ ((module) => {
+
+"use strict";
+module.exports = require("child_process");;
+
+/***/ }),
+
+/***/ 747:
+/***/ ((module) => {
+
+"use strict";
+module.exports = require("fs");;
+
+/***/ }),
+
+/***/ 87:
+/***/ ((module) => {
+
+"use strict";
+module.exports = require("os");;
+
+/***/ }),
+
+/***/ 622:
+/***/ ((module) => {
+
+"use strict";
+module.exports = require("path");;
+
+/***/ })
+
+/******/ 	});
+/************************************************************************/
+/******/ 	// The module cache
+/******/ 	var __webpack_module_cache__ = {};
+/******/ 	
+/******/ 	// The require function
+/******/ 	function __nccwpck_require__(moduleId) {
+/******/ 		// Check if module is in cache
+/******/ 		if(__webpack_module_cache__[moduleId]) {
+/******/ 			return __webpack_module_cache__[moduleId].exports;
+/******/ 		}
+/******/ 		// Create a new module (and put it into the cache)
+/******/ 		var module = __webpack_module_cache__[moduleId] = {
+/******/ 			// no module.id needed
+/******/ 			// no module.loaded needed
+/******/ 			exports: {}
+/******/ 		};
+/******/ 	
+/******/ 		// Execute the module function
+/******/ 		var threw = true;
+/******/ 		try {
+/******/ 			__webpack_modules__[moduleId].call(module.exports, module, module.exports, __nccwpck_require__);
+/******/ 			threw = false;
+/******/ 		} finally {
+/******/ 			if(threw) delete __webpack_module_cache__[moduleId];
+/******/ 		}
+/******/ 	
+/******/ 		// Return the exports of the module
+/******/ 		return module.exports;
+/******/ 	}
+/******/ 	
+/************************************************************************/
+/******/ 	/* webpack/runtime/compat */
+/******/ 	
+/******/ 	__nccwpck_require__.ab = __dirname + "/";/************************************************************************/
+/******/ 	// module exports must be returned from runtime so entry inlining is disabled
+/******/ 	// startup
+/******/ 	// Load entry module and return exports
+/******/ 	return __nccwpck_require__(330);
+/******/ })()
+;
diff --git a/.github/scripts/buildActions.sh b/.github/scripts/buildActions.sh
index 79b80c0704c6..36d8790070c3 100755
--- a/.github/scripts/buildActions.sh
+++ b/.github/scripts/buildActions.sh
@@ -10,6 +10,7 @@ ACTIONS_DIR="$(dirname "$(dirname "$0")")/actions"
 # List of paths to all JS files that implement our GH Actions
 declare -r GITHUB_ACTIONS=(
     "$ACTIONS_DIR/bumpVersion/bumpVersion.js"
+    "$ACTIONS_DIR/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js"
     "$ACTIONS_DIR/checkDeployBlockers/checkDeployBlockers.js"
     "$ACTIONS_DIR/createOrUpdateStagingDeploy/createOrUpdateStagingDeploy.js"
     "$ACTIONS_DIR/getDeployPullRequestList/getDeployPullRequestList.js"
diff --git a/.github/workflows/cherryPick.yml b/.github/workflows/cherryPick.yml
index 712d3304eb5a..9636f9351331 100644
--- a/.github/workflows/cherryPick.yml
+++ b/.github/workflows/cherryPick.yml
@@ -143,8 +143,12 @@ jobs:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
           PULL_REQUEST_NUMBER: ${{ steps.createPullRequest.outputs.pr_number }}
 
-      - name: Auto-assign PR if there are merge conflicts
-        if: ${{ !fromJSON(steps.cherryPick.outputs.SHOULD_AUTOMERGE) || !fromJSON(steps.isPullRequestMergeable.outputs.IS_MERGEABLE) }}
+      - name: Check if ShortVersionString is up to date
+        id: isShortVersionStringUpdated
+        uses: Expensify/App/.github/actions/checkBundleVersionStringMatch@main
+
+      - name: Auto-assign PR if there are merge conflicts or if the bundle versions are mismatched
+        if: ${{ !fromJSON(steps.cherryPick.outputs.SHOULD_AUTOMERGE) || !fromJSON(steps.isPullRequestMergeable.outputs.IS_MERGEABLE) || !fromJSON(steps.isShortVersionStringUpdated.outputs.BUNDLE_VERSIONS_MATCH) }}
         uses: actions-ecosystem/action-add-labels@a8ae047fee0ca28235f9764e1c478d2136dc15c1
         with:
           number: ${{ steps.createPullRequest.outputs.pr_number }}
@@ -171,6 +175,17 @@ jobs:
             Please manually resolve the conflicts, push your changes, and then request another reviewer to review and merge.
             **Important:** There may be conflicts that GitHub is not able to detect, so please _carefully_ review this pull request before approving.
 
+      - name: If PR has a bundle version mismatch, comment with the instructions for assignee
+        if: ${{ !fromJSON(steps.isShortVersionStringUpdated.outputs.BUNDLE_VERSIONS_MATCH) }}
+        uses: actions-ecosystem/action-create-comment@cd098164398331c50e7dfdd0dfa1b564a1873fac
+        with:
+          github_token: ${{ secrets.OS_BOTIFY_TOKEN }}
+          number: ${{ steps.createPullRequest.outputs.pr_number }}
+          body: |
+            The CFBundleShortVersionString value in this PR is not compatible with the CFBundleVersion, so cherry picking it will result in an iOS deploy failure.
+            Please manually resolve the mismatch, push your changes, and then request another reviewer to review and merge.
+            **Important:** This mismatch can be caused by a failed Update Protected Branch workflow followed by a manual CP, but please confirm the cause of the mismatch before updating any version numbers.
+
       - name: Check for an auto approve
         # Important: only auto-approve if there was no merge conflict!
         if: ${{ fromJSON(steps.cherryPick.outputs.SHOULD_AUTOMERGE) && fromJSON(steps.isPullRequestMergeable.outputs.IS_MERGEABLE) }}

From 25a7f3db1b844be52d601bb75f88a2c1b542abda Mon Sep 17 00:00:00 2001
From: Joe Gambino <joe@expensify.com>
Date: Mon, 6 Dec 2021 19:21:28 -0800
Subject: [PATCH 2/6] use grep/sed

---
 .../checkBundleVersionStringMatch.js                      | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js b/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
index f601976a2f2a..47f5331759d9 100644
--- a/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
+++ b/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
@@ -2,12 +2,14 @@ const core = require('@actions/core');
 const {execSync} = require('child_process');
 const {PLIST_PATH} = require('../../libs/nativeVersionUpdater');
 
-const bundleVersion = execSync(`/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" ${PLIST_PATH}`);
-const shortBundleVersion = execSync(`/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" ${PLIST_PATH}`);
+const bundleVersion = execSync(`grep -A1 'CFBundleVersion' ${PLIST_PATH} | grep -v 'CFBundleVersion' | sed 's|[</string>,]||g')`).toString().trim();
+const shortBundleVersion = execSync(`grep -A1 'CFBundleShortVersionString' ${PLIST_PATH} | grep -v 'CFBundleShortVersionString' | sed 's|[</string>,]||g')`).toString().trim();
 
 console.log(`Bundle Version: ${bundleVersion}`);
 console.log(`Short Bundle Version: ${shortBundleVersion}`);
-if (shortBundleVersion !== (bundleVersion.split('-') || [''])[0]) {
+
+const hasValue = shortBundleVersion && bundleVersion;
+if (!hasValue || (shortBundleVersion !== (bundleVersion.split('-') || [''])[0])) {
     console.log('Bundle Versions do not match');
     core.setOutput('BUNDLE_VERSIONS_MATCH', false);
 } else {

From d88a86d309d7a450380ee5eff497d748bf46ab5d Mon Sep 17 00:00:00 2001
From: Joe Gambino <joe@expensify.com>
Date: Tue, 7 Dec 2021 15:59:39 -0800
Subject: [PATCH 3/6] build actions

---
 .../checkBundleVersionStringMatch.js                  |  5 ++++-
 .../actions/checkBundleVersionStringMatch/index.js    | 11 ++++++++---
 2 files changed, 12 insertions(+), 4 deletions(-)

diff --git a/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js b/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
index 47f5331759d9..4a44df103758 100644
--- a/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
+++ b/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
@@ -9,7 +9,10 @@ console.log(`Bundle Version: ${bundleVersion}`);
 console.log(`Short Bundle Version: ${shortBundleVersion}`);
 
 const hasValue = shortBundleVersion && bundleVersion;
-if (!hasValue || (shortBundleVersion !== (bundleVersion.split('-') || [''])[0])) {
+if (!hasValue) {
+    console.log('Failed to get Bundle Versions from plist');
+    core.setOutput('BUNDLE_VERSIONS_MATCH', false);
+} else if (shortBundleVersion !== (bundleVersion.split('-') || [''])[0]) {
     console.log('Bundle Versions do not match');
     core.setOutput('BUNDLE_VERSIONS_MATCH', false);
 } else {
diff --git a/.github/actions/checkBundleVersionStringMatch/index.js b/.github/actions/checkBundleVersionStringMatch/index.js
index 960ac54b3fbf..6bb879167a54 100644
--- a/.github/actions/checkBundleVersionStringMatch/index.js
+++ b/.github/actions/checkBundleVersionStringMatch/index.js
@@ -12,12 +12,17 @@ const core = __nccwpck_require__(186);
 const {execSync} = __nccwpck_require__(129);
 const {PLIST_PATH} = __nccwpck_require__(322);
 
-const bundleVersion = execSync(`/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" ${PLIST_PATH}`);
-const shortBundleVersion = execSync(`/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" ${PLIST_PATH}`);
+const bundleVersion = execSync(`grep -A1 'CFBundleVersion' ${PLIST_PATH} | grep -v 'CFBundleVersion' | sed 's|[</string>,]||g')`).toString().trim();
+const shortBundleVersion = execSync(`grep -A1 'CFBundleShortVersionString' ${PLIST_PATH} | grep -v 'CFBundleShortVersionString' | sed 's|[</string>,]||g')`).toString().trim();
 
 console.log(`Bundle Version: ${bundleVersion}`);
 console.log(`Short Bundle Version: ${shortBundleVersion}`);
-if (shortBundleVersion !== (bundleVersion.split('-') || [''])[0]) {
+
+const hasValue = shortBundleVersion && bundleVersion;
+if (!hasValue) {
+    console.log('Failed to get Bundle Versions from plist');
+    core.setOutput('BUNDLE_VERSIONS_MATCH', false);
+} else if (shortBundleVersion !== (bundleVersion.split('-') || [''])[0]) {
     console.log('Bundle Versions do not match');
     core.setOutput('BUNDLE_VERSIONS_MATCH', false);
 } else {

From 03b41f98717a0f6f90fe5550db983f9ca8e82d7d Mon Sep 17 00:00:00 2001
From: Joe Gambino <joe@expensify.com>
Date: Wed, 8 Dec 2021 14:28:03 -0800
Subject: [PATCH 4/6] remove extra parens

---
 .../checkBundleVersionStringMatch.js                          | 4 ++--
 .github/actions/checkBundleVersionStringMatch/index.js        | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js b/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
index 4a44df103758..0dc080d38922 100644
--- a/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
+++ b/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
@@ -2,8 +2,8 @@ const core = require('@actions/core');
 const {execSync} = require('child_process');
 const {PLIST_PATH} = require('../../libs/nativeVersionUpdater');
 
-const bundleVersion = execSync(`grep -A1 'CFBundleVersion' ${PLIST_PATH} | grep -v 'CFBundleVersion' | sed 's|[</string>,]||g')`).toString().trim();
-const shortBundleVersion = execSync(`grep -A1 'CFBundleShortVersionString' ${PLIST_PATH} | grep -v 'CFBundleShortVersionString' | sed 's|[</string>,]||g')`).toString().trim();
+const bundleVersion = execSync(`grep -A1 'CFBundleVersion' ${PLIST_PATH} | grep -v 'CFBundleVersion' | sed 's|[</string>,]||g'`).toString().trim();
+const shortBundleVersion = execSync(`grep -A1 'CFBundleShortVersionString' ${PLIST_PATH} | grep -v 'CFBundleShortVersionString' | sed 's|[</string>,]||g'`).toString().trim();
 
 console.log(`Bundle Version: ${bundleVersion}`);
 console.log(`Short Bundle Version: ${shortBundleVersion}`);
diff --git a/.github/actions/checkBundleVersionStringMatch/index.js b/.github/actions/checkBundleVersionStringMatch/index.js
index 6bb879167a54..05ee8a465cc8 100644
--- a/.github/actions/checkBundleVersionStringMatch/index.js
+++ b/.github/actions/checkBundleVersionStringMatch/index.js
@@ -12,8 +12,8 @@ const core = __nccwpck_require__(186);
 const {execSync} = __nccwpck_require__(129);
 const {PLIST_PATH} = __nccwpck_require__(322);
 
-const bundleVersion = execSync(`grep -A1 'CFBundleVersion' ${PLIST_PATH} | grep -v 'CFBundleVersion' | sed 's|[</string>,]||g')`).toString().trim();
-const shortBundleVersion = execSync(`grep -A1 'CFBundleShortVersionString' ${PLIST_PATH} | grep -v 'CFBundleShortVersionString' | sed 's|[</string>,]||g')`).toString().trim();
+const bundleVersion = execSync(`grep -A1 'CFBundleVersion' ${PLIST_PATH} | grep -v 'CFBundleVersion' | sed 's|[</string>,]||g'`).toString().trim();
+const shortBundleVersion = execSync(`grep -A1 'CFBundleShortVersionString' ${PLIST_PATH} | grep -v 'CFBundleShortVersionString' | sed 's|[</string>,]||g'`).toString().trim();
 
 console.log(`Bundle Version: ${bundleVersion}`);
 console.log(`Short Bundle Version: ${shortBundleVersion}`);

From 6bbd4cdfd461e0e9670aefc0cd1aa66679fbc9da Mon Sep 17 00:00:00 2001
From: Joe Gambino <joe@expensify.com>
Date: Wed, 8 Dec 2021 16:01:18 -0800
Subject: [PATCH 5/6] fix grep logic

---
 .../checkBundleVersionStringMatch.js                      | 8 ++++----
 .github/actions/checkBundleVersionStringMatch/index.js    | 8 ++++----
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js b/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
index 0dc080d38922..c06c1a23c1cf 100644
--- a/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
+++ b/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
@@ -12,10 +12,10 @@ const hasValue = shortBundleVersion && bundleVersion;
 if (!hasValue) {
     console.log('Failed to get Bundle Versions from plist');
     core.setOutput('BUNDLE_VERSIONS_MATCH', false);
-} else if (shortBundleVersion !== (bundleVersion.split('-') || [''])[0]) {
-    console.log('Bundle Versions do not match');
-    core.setOutput('BUNDLE_VERSIONS_MATCH', false);
-} else {
+} else if (bundleVersion.includes(shortBundleVersion)) {
     console.log('Bundle Versions match');
     core.setOutput('BUNDLE_VERSIONS_MATCH', true);
+} else {
+    console.log('Bundle Versions do not match');
+    core.setOutput('BUNDLE_VERSIONS_MATCH', false);
 }
diff --git a/.github/actions/checkBundleVersionStringMatch/index.js b/.github/actions/checkBundleVersionStringMatch/index.js
index 05ee8a465cc8..c78acd53a96b 100644
--- a/.github/actions/checkBundleVersionStringMatch/index.js
+++ b/.github/actions/checkBundleVersionStringMatch/index.js
@@ -22,12 +22,12 @@ const hasValue = shortBundleVersion && bundleVersion;
 if (!hasValue) {
     console.log('Failed to get Bundle Versions from plist');
     core.setOutput('BUNDLE_VERSIONS_MATCH', false);
-} else if (shortBundleVersion !== (bundleVersion.split('-') || [''])[0]) {
-    console.log('Bundle Versions do not match');
-    core.setOutput('BUNDLE_VERSIONS_MATCH', false);
-} else {
+} else if (bundleVersion.includes(shortBundleVersion)) {
     console.log('Bundle Versions match');
     core.setOutput('BUNDLE_VERSIONS_MATCH', true);
+} else {
+    console.log('Bundle Versions do not match');
+    core.setOutput('BUNDLE_VERSIONS_MATCH', false);
 }
 
 

From c1da6e98db55fa4038e236a6e15b908b8237fc2a Mon Sep 17 00:00:00 2001
From: Joe Gambino <joe@expensify.com>
Date: Wed, 8 Dec 2021 16:06:19 -0800
Subject: [PATCH 6/6] clearer logs

---
 .../checkBundleVersionStringMatch.js                          | 4 ++--
 .github/actions/checkBundleVersionStringMatch/index.js        | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js b/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
index c06c1a23c1cf..68d42427481e 100644
--- a/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
+++ b/.github/actions/checkBundleVersionStringMatch/checkBundleVersionStringMatch.js
@@ -13,9 +13,9 @@ if (!hasValue) {
     console.log('Failed to get Bundle Versions from plist');
     core.setOutput('BUNDLE_VERSIONS_MATCH', false);
 } else if (bundleVersion.includes(shortBundleVersion)) {
-    console.log('Bundle Versions match');
+    console.log('Bundle Versions are compatible');
     core.setOutput('BUNDLE_VERSIONS_MATCH', true);
 } else {
-    console.log('Bundle Versions do not match');
+    console.log('Bundle Versions are not compatible');
     core.setOutput('BUNDLE_VERSIONS_MATCH', false);
 }
diff --git a/.github/actions/checkBundleVersionStringMatch/index.js b/.github/actions/checkBundleVersionStringMatch/index.js
index c78acd53a96b..bedf06208d4c 100644
--- a/.github/actions/checkBundleVersionStringMatch/index.js
+++ b/.github/actions/checkBundleVersionStringMatch/index.js
@@ -23,10 +23,10 @@ if (!hasValue) {
     console.log('Failed to get Bundle Versions from plist');
     core.setOutput('BUNDLE_VERSIONS_MATCH', false);
 } else if (bundleVersion.includes(shortBundleVersion)) {
-    console.log('Bundle Versions match');
+    console.log('Bundle Versions are compatible');
     core.setOutput('BUNDLE_VERSIONS_MATCH', true);
 } else {
-    console.log('Bundle Versions do not match');
+    console.log('Bundle Versions are not compatible');
     core.setOutput('BUNDLE_VERSIONS_MATCH', false);
 }