diff --git a/.eslintrc.js b/.eslintrc.js index 9f839e45ce75..644b1a201383 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,3 +1,5 @@ +const path = require('path'); + const restrictedImportPaths = [ { name: 'react-native', @@ -96,7 +98,7 @@ module.exports = { plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library'], parser: '@typescript-eslint/parser', parserOptions: { - project: './tsconfig.json', + project: path.resolve(__dirname, './tsconfig.json'), }, env: { jest: true, diff --git a/.github/actions/javascript/authorChecklist/index.js b/.github/actions/javascript/authorChecklist/index.js index 337fe7398fb3..1eb1725fe1c0 100644 --- a/.github/actions/javascript/authorChecklist/index.js +++ b/.github/actions/javascript/authorChecklist/index.js @@ -11710,7 +11710,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -17531,27 +17531,27 @@ exports["default"] = arrayDifference; /***/ }), -/***/ 2877: +/***/ 9491: /***/ ((module) => { -module.exports = eval("require")("encoding"); - +"use strict"; +module.exports = require("assert"); /***/ }), -/***/ 9491: +/***/ 6113: /***/ ((module) => { "use strict"; -module.exports = require("assert"); +module.exports = require("crypto"); /***/ }), -/***/ 6113: +/***/ 3975: /***/ ((module) => { "use strict"; -module.exports = require("crypto"); +module.exports = require("encoding"); /***/ }), diff --git a/.github/actions/javascript/awaitStagingDeploys/index.js b/.github/actions/javascript/awaitStagingDeploys/index.js index 0e0168fdb7ae..d4eb5e1ba3e4 100644 --- a/.github/actions/javascript/awaitStagingDeploys/index.js +++ b/.github/actions/javascript/awaitStagingDeploys/index.js @@ -7373,7 +7373,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -12796,27 +12796,27 @@ exports["default"] = arrayDifference; /***/ }), -/***/ 2877: +/***/ 9491: /***/ ((module) => { -module.exports = eval("require")("encoding"); - +"use strict"; +module.exports = require("assert"); /***/ }), -/***/ 9491: +/***/ 6113: /***/ ((module) => { "use strict"; -module.exports = require("assert"); +module.exports = require("crypto"); /***/ }), -/***/ 6113: +/***/ 3975: /***/ ((module) => { "use strict"; -module.exports = require("crypto"); +module.exports = require("encoding"); /***/ }), diff --git a/.github/actions/javascript/bumpVersion/bumpVersion.ts b/.github/actions/javascript/bumpVersion/bumpVersion.ts index eba79c7c9edb..ff43ab9ee5c5 100644 --- a/.github/actions/javascript/bumpVersion/bumpVersion.ts +++ b/.github/actions/javascript/bumpVersion/bumpVersion.ts @@ -5,6 +5,7 @@ import type {PackageJson} from 'type-fest'; import {promisify} from 'util'; import {generateAndroidVersionCode, updateAndroidVersion, updateiOSVersion} from '@github/libs/nativeVersionUpdater'; import * as versionUpdater from '@github/libs/versionUpdater'; +import type {SemverLevel} from '@github/libs/versionUpdater'; const exec = promisify(originalExec); @@ -43,7 +44,7 @@ function updateNativeVersions(version: string) { } let semanticVersionLevel = core.getInput('SEMVER_LEVEL', {required: true}); -if (!semanticVersionLevel || !Object.keys(versionUpdater.SEMANTIC_VERSION_LEVELS).includes(semanticVersionLevel)) { +if (!semanticVersionLevel || !versionUpdater.isValidSemverLevel(semanticVersionLevel)) { semanticVersionLevel = versionUpdater.SEMANTIC_VERSION_LEVELS.BUILD; console.log(`Invalid input for 'SEMVER_LEVEL': ${semanticVersionLevel}`, `Defaulting to: ${semanticVersionLevel}`); } @@ -53,7 +54,7 @@ if (!previousVersion) { core.setFailed('Error: Could not read package.json'); } -const newVersion = versionUpdater.incrementVersion(previousVersion ?? '', semanticVersionLevel); +const newVersion = versionUpdater.incrementVersion(previousVersion ?? '', semanticVersionLevel as SemverLevel); console.log(`Previous version: ${previousVersion}`, `New version: ${newVersion}`); updateNativeVersions(newVersion); diff --git a/.github/actions/javascript/bumpVersion/index.js b/.github/actions/javascript/bumpVersion/index.js index e1a5cf13a8d9..93ea47bed2ae 100644 --- a/.github/actions/javascript/bumpVersion/index.js +++ b/.github/actions/javascript/bumpVersion/index.js @@ -3473,7 +3473,7 @@ function updateNativeVersions(version) { } } let semanticVersionLevel = core.getInput('SEMVER_LEVEL', { required: true }); -if (!semanticVersionLevel || !Object.keys(versionUpdater.SEMANTIC_VERSION_LEVELS).includes(semanticVersionLevel)) { +if (!semanticVersionLevel || !versionUpdater.isValidSemverLevel(semanticVersionLevel)) { semanticVersionLevel = versionUpdater.SEMANTIC_VERSION_LEVELS.BUILD; console.log(`Invalid input for 'SEMVER_LEVEL': ${semanticVersionLevel}`, `Defaulting to: ${semanticVersionLevel}`); } @@ -3589,7 +3589,7 @@ exports.updateiOSVersion = updateiOSVersion; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getPreviousVersion = exports.incrementPatch = exports.incrementMinor = exports.SEMANTIC_VERSION_LEVELS = exports.MAX_INCREMENTS = exports.incrementVersion = exports.getVersionStringFromNumber = exports.getVersionNumberFromString = void 0; +exports.getPreviousVersion = exports.incrementPatch = exports.incrementMinor = exports.SEMANTIC_VERSION_LEVELS = exports.MAX_INCREMENTS = exports.incrementVersion = exports.getVersionStringFromNumber = exports.getVersionNumberFromString = exports.isValidSemverLevel = void 0; const SEMANTIC_VERSION_LEVELS = { MAJOR: 'MAJOR', MINOR: 'MINOR', @@ -3599,6 +3599,10 @@ const SEMANTIC_VERSION_LEVELS = { exports.SEMANTIC_VERSION_LEVELS = SEMANTIC_VERSION_LEVELS; const MAX_INCREMENTS = 99; exports.MAX_INCREMENTS = MAX_INCREMENTS; +function isValidSemverLevel(str) { + return Object.keys(SEMANTIC_VERSION_LEVELS).includes(str); +} +exports.isValidSemverLevel = isValidSemverLevel; /** * Transforms a versions string into a number */ diff --git a/.github/actions/javascript/checkDeployBlockers/index.js b/.github/actions/javascript/checkDeployBlockers/index.js index 842deb1cbb5d..088839bbb1ad 100644 --- a/.github/actions/javascript/checkDeployBlockers/index.js +++ b/.github/actions/javascript/checkDeployBlockers/index.js @@ -6679,7 +6679,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -12024,27 +12024,27 @@ exports["default"] = arrayDifference; /***/ }), -/***/ 2877: +/***/ 9491: /***/ ((module) => { -module.exports = eval("require")("encoding"); - +"use strict"; +module.exports = require("assert"); /***/ }), -/***/ 9491: +/***/ 6113: /***/ ((module) => { "use strict"; -module.exports = require("assert"); +module.exports = require("crypto"); /***/ }), -/***/ 6113: +/***/ 3975: /***/ ((module) => { "use strict"; -module.exports = require("crypto"); +module.exports = require("encoding"); /***/ }), diff --git a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js index 127fb1fe3dca..a358fdbbbe6e 100644 --- a/.github/actions/javascript/createOrUpdateStagingDeploy/index.js +++ b/.github/actions/javascript/createOrUpdateStagingDeploy/index.js @@ -9434,7 +9434,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -14401,7 +14401,66 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); const child_process_1 = __nccwpck_require__(2081); const CONST_1 = __importDefault(__nccwpck_require__(9873)); const sanitizeStringForJSONParse_1 = __importDefault(__nccwpck_require__(3902)); -const VERSION_UPDATER = __importStar(__nccwpck_require__(8982)); +const VersionUpdater = __importStar(__nccwpck_require__(8982)); +/** + * Check if a tag exists locally or in the remote. + */ +function tagExists(tag) { + try { + // Check if the tag exists locally + (0, child_process_1.execSync)(`git show-ref --tags ${tag}`, { stdio: 'ignore' }); + return true; // Tag exists locally + } + catch (error) { + // Tag does not exist locally, check in remote + let shouldRetry = true; + let needsRepack = false; + let doesTagExist = false; + while (shouldRetry) { + try { + if (needsRepack) { + // We have seen some scenarios where this fixes the git fetch. + // Why? Who knows... https://github.com/Expensify/App/pull/31459 + (0, child_process_1.execSync)('git repack -d', { stdio: 'inherit' }); + } + (0, child_process_1.execSync)(`git ls-remote --exit-code --tags origin ${tag}`, { stdio: 'ignore' }); + doesTagExist = true; + shouldRetry = false; + } + catch (e) { + if (!needsRepack) { + console.log('Attempting to repack and retry...'); + needsRepack = true; + } + else { + console.error("Repack didn't help, giving up..."); + shouldRetry = false; + } + } + } + return doesTagExist; + } +} +/** + * This essentially just calls getPreviousVersion in a loop, until it finds a version for which a tag exists. + * It's useful if we manually perform a version bump, because in that case a tag may not exist for the previous version. + * + * @param tag the current tag + * @param level the Semver level to step backward by + */ +function getPreviousExistingTag(tag, level) { + let previousVersion = VersionUpdater.getPreviousVersion(tag, level); + let tagExistsForPreviousVersion = false; + while (!tagExistsForPreviousVersion) { + if (tagExists(previousVersion)) { + tagExistsForPreviousVersion = true; + break; + } + console.log(`Tag for previous version ${previousVersion} does not exist. Checking for an older version...`); + previousVersion = VersionUpdater.getPreviousVersion(previousVersion, level); + } + return previousVersion; +} /** * @param [shallowExcludeTag] When fetching the given tag, exclude all history reachable by the shallowExcludeTag (used to make fetch much faster) */ @@ -14444,8 +14503,8 @@ function fetchTag(tag, shallowExcludeTag = '') { * Get merge logs between two tags (inclusive) as a JavaScript object. */ function getCommitHistoryAsJSON(fromTag, toTag) { - // Fetch tags, exclude commits reachable from the previous patch version (i.e: previous checklist), so that we don't have to fetch the full history - const previousPatchVersion = VERSION_UPDATER.getPreviousVersion(fromTag, VERSION_UPDATER.SEMANTIC_VERSION_LEVELS.PATCH); + // Fetch tags, excluding commits reachable from the previous patch version (i.e: previous checklist), so that we don't have to fetch the full history + const previousPatchVersion = getPreviousExistingTag(fromTag, VersionUpdater.SEMANTIC_VERSION_LEVELS.PATCH); fetchTag(fromTag, previousPatchVersion); fetchTag(toTag, previousPatchVersion); console.log('Getting pull requests merged between the following tags:', fromTag, toTag); @@ -14517,6 +14576,7 @@ async function getPullRequestsMergedBetween(fromTag, toTag) { return pullRequestNumbers; } exports["default"] = { + getPreviousExistingTag, getValidMergedPRs, getPullRequestsMergedBetween, }; @@ -15000,7 +15060,7 @@ exports["default"] = sanitizeStringForJSONParse; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getPreviousVersion = exports.incrementPatch = exports.incrementMinor = exports.SEMANTIC_VERSION_LEVELS = exports.MAX_INCREMENTS = exports.incrementVersion = exports.getVersionStringFromNumber = exports.getVersionNumberFromString = void 0; +exports.getPreviousVersion = exports.incrementPatch = exports.incrementMinor = exports.SEMANTIC_VERSION_LEVELS = exports.MAX_INCREMENTS = exports.incrementVersion = exports.getVersionStringFromNumber = exports.getVersionNumberFromString = exports.isValidSemverLevel = void 0; const SEMANTIC_VERSION_LEVELS = { MAJOR: 'MAJOR', MINOR: 'MINOR', @@ -15010,6 +15070,10 @@ const SEMANTIC_VERSION_LEVELS = { exports.SEMANTIC_VERSION_LEVELS = SEMANTIC_VERSION_LEVELS; const MAX_INCREMENTS = 99; exports.MAX_INCREMENTS = MAX_INCREMENTS; +function isValidSemverLevel(str) { + return Object.keys(SEMANTIC_VERSION_LEVELS).includes(str); +} +exports.isValidSemverLevel = isValidSemverLevel; /** * Transforms a versions string into a number */ @@ -15126,14 +15190,6 @@ function arrayDifference(array1, array2) { exports["default"] = arrayDifference; -/***/ }), - -/***/ 2877: -/***/ ((module) => { - -module.exports = eval("require")("encoding"); - - /***/ }), /***/ 9491: @@ -15160,6 +15216,14 @@ module.exports = require("crypto"); /***/ }), +/***/ 3975: +/***/ ((module) => { + +"use strict"; +module.exports = require("encoding"); + +/***/ }), + /***/ 2361: /***/ ((module) => { diff --git a/.github/actions/javascript/getArtifactInfo/index.js b/.github/actions/javascript/getArtifactInfo/index.js index 77f61f491fec..5f8fb97d0c27 100644 --- a/.github/actions/javascript/getArtifactInfo/index.js +++ b/.github/actions/javascript/getArtifactInfo/index.js @@ -6679,7 +6679,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -11985,27 +11985,27 @@ exports["default"] = arrayDifference; /***/ }), -/***/ 2877: +/***/ 9491: /***/ ((module) => { -module.exports = eval("require")("encoding"); - +"use strict"; +module.exports = require("assert"); /***/ }), -/***/ 9491: +/***/ 6113: /***/ ((module) => { "use strict"; -module.exports = require("assert"); +module.exports = require("crypto"); /***/ }), -/***/ 6113: +/***/ 3975: /***/ ((module) => { "use strict"; -module.exports = require("crypto"); +module.exports = require("encoding"); /***/ }), diff --git a/.github/actions/javascript/getDeployPullRequestList/index.js b/.github/actions/javascript/getDeployPullRequestList/index.js index 8f9f9deea896..2f0201abf543 100644 --- a/.github/actions/javascript/getDeployPullRequestList/index.js +++ b/.github/actions/javascript/getDeployPullRequestList/index.js @@ -6723,7 +6723,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -11665,7 +11665,66 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); const child_process_1 = __nccwpck_require__(2081); const CONST_1 = __importDefault(__nccwpck_require__(9873)); const sanitizeStringForJSONParse_1 = __importDefault(__nccwpck_require__(3902)); -const VERSION_UPDATER = __importStar(__nccwpck_require__(8982)); +const VersionUpdater = __importStar(__nccwpck_require__(8982)); +/** + * Check if a tag exists locally or in the remote. + */ +function tagExists(tag) { + try { + // Check if the tag exists locally + (0, child_process_1.execSync)(`git show-ref --tags ${tag}`, { stdio: 'ignore' }); + return true; // Tag exists locally + } + catch (error) { + // Tag does not exist locally, check in remote + let shouldRetry = true; + let needsRepack = false; + let doesTagExist = false; + while (shouldRetry) { + try { + if (needsRepack) { + // We have seen some scenarios where this fixes the git fetch. + // Why? Who knows... https://github.com/Expensify/App/pull/31459 + (0, child_process_1.execSync)('git repack -d', { stdio: 'inherit' }); + } + (0, child_process_1.execSync)(`git ls-remote --exit-code --tags origin ${tag}`, { stdio: 'ignore' }); + doesTagExist = true; + shouldRetry = false; + } + catch (e) { + if (!needsRepack) { + console.log('Attempting to repack and retry...'); + needsRepack = true; + } + else { + console.error("Repack didn't help, giving up..."); + shouldRetry = false; + } + } + } + return doesTagExist; + } +} +/** + * This essentially just calls getPreviousVersion in a loop, until it finds a version for which a tag exists. + * It's useful if we manually perform a version bump, because in that case a tag may not exist for the previous version. + * + * @param tag the current tag + * @param level the Semver level to step backward by + */ +function getPreviousExistingTag(tag, level) { + let previousVersion = VersionUpdater.getPreviousVersion(tag, level); + let tagExistsForPreviousVersion = false; + while (!tagExistsForPreviousVersion) { + if (tagExists(previousVersion)) { + tagExistsForPreviousVersion = true; + break; + } + console.log(`Tag for previous version ${previousVersion} does not exist. Checking for an older version...`); + previousVersion = VersionUpdater.getPreviousVersion(previousVersion, level); + } + return previousVersion; +} /** * @param [shallowExcludeTag] When fetching the given tag, exclude all history reachable by the shallowExcludeTag (used to make fetch much faster) */ @@ -11708,8 +11767,8 @@ function fetchTag(tag, shallowExcludeTag = '') { * Get merge logs between two tags (inclusive) as a JavaScript object. */ function getCommitHistoryAsJSON(fromTag, toTag) { - // Fetch tags, exclude commits reachable from the previous patch version (i.e: previous checklist), so that we don't have to fetch the full history - const previousPatchVersion = VERSION_UPDATER.getPreviousVersion(fromTag, VERSION_UPDATER.SEMANTIC_VERSION_LEVELS.PATCH); + // Fetch tags, excluding commits reachable from the previous patch version (i.e: previous checklist), so that we don't have to fetch the full history + const previousPatchVersion = getPreviousExistingTag(fromTag, VersionUpdater.SEMANTIC_VERSION_LEVELS.PATCH); fetchTag(fromTag, previousPatchVersion); fetchTag(toTag, previousPatchVersion); console.log('Getting pull requests merged between the following tags:', fromTag, toTag); @@ -11781,6 +11840,7 @@ async function getPullRequestsMergedBetween(fromTag, toTag) { return pullRequestNumbers; } exports["default"] = { + getPreviousExistingTag, getValidMergedPRs, getPullRequestsMergedBetween, }; @@ -12264,7 +12324,7 @@ exports["default"] = sanitizeStringForJSONParse; "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getPreviousVersion = exports.incrementPatch = exports.incrementMinor = exports.SEMANTIC_VERSION_LEVELS = exports.MAX_INCREMENTS = exports.incrementVersion = exports.getVersionStringFromNumber = exports.getVersionNumberFromString = void 0; +exports.getPreviousVersion = exports.incrementPatch = exports.incrementMinor = exports.SEMANTIC_VERSION_LEVELS = exports.MAX_INCREMENTS = exports.incrementVersion = exports.getVersionStringFromNumber = exports.getVersionNumberFromString = exports.isValidSemverLevel = void 0; const SEMANTIC_VERSION_LEVELS = { MAJOR: 'MAJOR', MINOR: 'MINOR', @@ -12274,6 +12334,10 @@ const SEMANTIC_VERSION_LEVELS = { exports.SEMANTIC_VERSION_LEVELS = SEMANTIC_VERSION_LEVELS; const MAX_INCREMENTS = 99; exports.MAX_INCREMENTS = MAX_INCREMENTS; +function isValidSemverLevel(str) { + return Object.keys(SEMANTIC_VERSION_LEVELS).includes(str); +} +exports.isValidSemverLevel = isValidSemverLevel; /** * Transforms a versions string into a number */ @@ -12390,14 +12454,6 @@ function arrayDifference(array1, array2) { exports["default"] = arrayDifference; -/***/ }), - -/***/ 2877: -/***/ ((module) => { - -module.exports = eval("require")("encoding"); - - /***/ }), /***/ 9491: @@ -12424,6 +12480,14 @@ module.exports = require("crypto"); /***/ }), +/***/ 3975: +/***/ ((module) => { + +"use strict"; +module.exports = require("encoding"); + +/***/ }), + /***/ 2361: /***/ ((module) => { diff --git a/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts b/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts index 262b603124fa..a178d4073cbb 100644 --- a/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts +++ b/.github/actions/javascript/getPreviousVersion/getPreviousVersion.ts @@ -1,17 +1,29 @@ import * as core from '@actions/core'; import {readFileSync} from 'fs'; import type {PackageJson} from 'type-fest'; +import GitUtils from '@github/libs/GitUtils'; import * as versionUpdater from '@github/libs/versionUpdater'; +import type {SemverLevel} from '@github/libs/versionUpdater'; -const semverLevel = core.getInput('SEMVER_LEVEL', {required: true}); -if (!semverLevel || !Object.values(versionUpdater.SEMANTIC_VERSION_LEVELS).includes(semverLevel)) { - core.setFailed(`'Error: Invalid input for 'SEMVER_LEVEL': ${semverLevel}`); +function run() { + const semverLevel = core.getInput('SEMVER_LEVEL', {required: true}); + if (!semverLevel || !versionUpdater.isValidSemverLevel(semverLevel)) { + core.setFailed(`'Error: Invalid input for 'SEMVER_LEVEL': ${semverLevel}`); + } + + const {version: currentVersion}: PackageJson = JSON.parse(readFileSync('./package.json', 'utf8')); + if (!currentVersion) { + core.setFailed('Error: Could not read package.json'); + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const previousVersion = GitUtils.getPreviousExistingTag(currentVersion!, semverLevel as SemverLevel); + core.setOutput('PREVIOUS_VERSION', previousVersion); + return previousVersion; } -const {version: currentVersion}: PackageJson = JSON.parse(readFileSync('./package.json', 'utf8')); -if (!currentVersion) { - core.setFailed('Error: Could not read package.json'); +if (require.main === module) { + run(); } -const previousVersion = versionUpdater.getPreviousVersion(currentVersion ?? '', semverLevel); -core.setOutput('PREVIOUS_VERSION', previousVersion); +export default run; diff --git a/.github/actions/javascript/getPreviousVersion/index.js b/.github/actions/javascript/getPreviousVersion/index.js index 8eac2f62f03e..29d02cc4dbac 100644 --- a/.github/actions/javascript/getPreviousVersion/index.js +++ b/.github/actions/javascript/getPreviousVersion/index.js @@ -2719,20 +2719,316 @@ var __importStar = (this && this.__importStar) || function (mod) { __setModuleDefault(result, mod); return result; }; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", ({ value: true })); const core = __importStar(__nccwpck_require__(186)); const fs_1 = __nccwpck_require__(147); +const GitUtils_1 = __importDefault(__nccwpck_require__(547)); const versionUpdater = __importStar(__nccwpck_require__(982)); -const semverLevel = core.getInput('SEMVER_LEVEL', { required: true }); -if (!semverLevel || !Object.values(versionUpdater.SEMANTIC_VERSION_LEVELS).includes(semverLevel)) { - core.setFailed(`'Error: Invalid input for 'SEMVER_LEVEL': ${semverLevel}`); +function run() { + const semverLevel = core.getInput('SEMVER_LEVEL', { required: true }); + if (!semverLevel || !versionUpdater.isValidSemverLevel(semverLevel)) { + core.setFailed(`'Error: Invalid input for 'SEMVER_LEVEL': ${semverLevel}`); + } + const { version: currentVersion } = JSON.parse((0, fs_1.readFileSync)('./package.json', 'utf8')); + if (!currentVersion) { + core.setFailed('Error: Could not read package.json'); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const previousVersion = GitUtils_1.default.getPreviousExistingTag(currentVersion, semverLevel); + core.setOutput('PREVIOUS_VERSION', previousVersion); + return previousVersion; +} +if (require.main === require.cache[eval('__filename')]) { + run(); +} +exports["default"] = run; + + +/***/ }), + +/***/ 873: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +Object.defineProperty(exports, "__esModule", ({ value: true })); +const GITHUB_BASE_URL_REGEX = new RegExp('https?://(?:github\\.com|api\\.github\\.com)'); +const GIT_CONST = { + GITHUB_OWNER: 'Expensify', + APP_REPO: 'App', +}; +const CONST = { + ...GIT_CONST, + APPLAUSE_BOT: 'applausebot', + OS_BOTIFY: 'OSBotify', + LABELS: { + STAGING_DEPLOY: 'StagingDeployCash', + DEPLOY_BLOCKER: 'DeployBlockerCash', + INTERNAL_QA: 'InternalQA', + }, + DATE_FORMAT_STRING: 'yyyy-MM-dd', + PULL_REQUEST_REGEX: new RegExp(`${GITHUB_BASE_URL_REGEX.source}/.*/.*/pull/([0-9]+).*`), + ISSUE_REGEX: new RegExp(`${GITHUB_BASE_URL_REGEX.source}/.*/.*/issues/([0-9]+).*`), + ISSUE_OR_PULL_REQUEST_REGEX: new RegExp(`${GITHUB_BASE_URL_REGEX.source}/.*/.*/(?:pull|issues)/([0-9]+).*`), + POLL_RATE: 10000, + APP_REPO_URL: `https://github.com/${GIT_CONST.GITHUB_OWNER}/${GIT_CONST.APP_REPO}`, + APP_REPO_GIT_URL: `git@github.com:${GIT_CONST.GITHUB_OWNER}/${GIT_CONST.APP_REPO}.git`, +}; +exports["default"] = CONST; + + +/***/ }), + +/***/ 547: +/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) { + +"use strict"; + +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + __setModuleDefault(result, mod); + return result; +}; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", ({ value: true })); +const child_process_1 = __nccwpck_require__(81); +const CONST_1 = __importDefault(__nccwpck_require__(873)); +const sanitizeStringForJSONParse_1 = __importDefault(__nccwpck_require__(902)); +const VersionUpdater = __importStar(__nccwpck_require__(982)); +/** + * Check if a tag exists locally or in the remote. + */ +function tagExists(tag) { + try { + // Check if the tag exists locally + (0, child_process_1.execSync)(`git show-ref --tags ${tag}`, { stdio: 'ignore' }); + return true; // Tag exists locally + } + catch (error) { + // Tag does not exist locally, check in remote + let shouldRetry = true; + let needsRepack = false; + let doesTagExist = false; + while (shouldRetry) { + try { + if (needsRepack) { + // We have seen some scenarios where this fixes the git fetch. + // Why? Who knows... https://github.com/Expensify/App/pull/31459 + (0, child_process_1.execSync)('git repack -d', { stdio: 'inherit' }); + } + (0, child_process_1.execSync)(`git ls-remote --exit-code --tags origin ${tag}`, { stdio: 'ignore' }); + doesTagExist = true; + shouldRetry = false; + } + catch (e) { + if (!needsRepack) { + console.log('Attempting to repack and retry...'); + needsRepack = true; + } + else { + console.error("Repack didn't help, giving up..."); + shouldRetry = false; + } + } + } + return doesTagExist; + } +} +/** + * This essentially just calls getPreviousVersion in a loop, until it finds a version for which a tag exists. + * It's useful if we manually perform a version bump, because in that case a tag may not exist for the previous version. + * + * @param tag the current tag + * @param level the Semver level to step backward by + */ +function getPreviousExistingTag(tag, level) { + let previousVersion = VersionUpdater.getPreviousVersion(tag, level); + let tagExistsForPreviousVersion = false; + while (!tagExistsForPreviousVersion) { + if (tagExists(previousVersion)) { + tagExistsForPreviousVersion = true; + break; + } + console.log(`Tag for previous version ${previousVersion} does not exist. Checking for an older version...`); + previousVersion = VersionUpdater.getPreviousVersion(previousVersion, level); + } + return previousVersion; } -const { version: currentVersion } = JSON.parse((0, fs_1.readFileSync)('./package.json', 'utf8')); -if (!currentVersion) { - core.setFailed('Error: Could not read package.json'); +/** + * @param [shallowExcludeTag] When fetching the given tag, exclude all history reachable by the shallowExcludeTag (used to make fetch much faster) + */ +function fetchTag(tag, shallowExcludeTag = '') { + let shouldRetry = true; + let needsRepack = false; + while (shouldRetry) { + try { + let command = ''; + if (needsRepack) { + // We have seen some scenarios where this fixes the git fetch. + // Why? Who knows... https://github.com/Expensify/App/pull/31459 + command = 'git repack -d'; + console.log(`Running command: ${command}`); + (0, child_process_1.execSync)(command); + } + command = `git fetch origin tag ${tag} --no-tags`; + // Note that this condition is only ever NOT true in the 1.0.0-0 edge case + if (shallowExcludeTag && shallowExcludeTag !== tag) { + command += ` --shallow-exclude=${shallowExcludeTag}`; + } + console.log(`Running command: ${command}`); + (0, child_process_1.execSync)(command); + shouldRetry = false; + } + catch (e) { + console.error(e); + if (!needsRepack) { + console.log('Attempting to repack and retry...'); + needsRepack = true; + } + else { + console.error("Repack didn't help, giving up..."); + shouldRetry = false; + } + } + } +} +/** + * Get merge logs between two tags (inclusive) as a JavaScript object. + */ +function getCommitHistoryAsJSON(fromTag, toTag) { + // Fetch tags, excluding commits reachable from the previous patch version (i.e: previous checklist), so that we don't have to fetch the full history + const previousPatchVersion = getPreviousExistingTag(fromTag, VersionUpdater.SEMANTIC_VERSION_LEVELS.PATCH); + fetchTag(fromTag, previousPatchVersion); + fetchTag(toTag, previousPatchVersion); + console.log('Getting pull requests merged between the following tags:', fromTag, toTag); + return new Promise((resolve, reject) => { + let stdout = ''; + let stderr = ''; + const args = ['log', '--format={"commit": "%H", "authorName": "%an", "subject": "%s"},', `${fromTag}...${toTag}`]; + console.log(`Running command: git ${args.join(' ')}`); + const spawnedProcess = (0, child_process_1.spawn)('git', args); + spawnedProcess.on('message', console.log); + spawnedProcess.stdout.on('data', (chunk) => { + console.log(chunk.toString()); + stdout += chunk.toString(); + }); + spawnedProcess.stderr.on('data', (chunk) => { + console.error(chunk.toString()); + stderr += chunk.toString(); + }); + spawnedProcess.on('close', (code) => { + if (code !== 0) { + return reject(new Error(`${stderr}`)); + } + resolve(stdout); + }); + spawnedProcess.on('error', (err) => reject(err)); + }).then((stdout) => { + // Sanitize just the text within commit subjects as that's the only potentially un-parseable text. + const sanitizedOutput = stdout.replace(/(?<="subject": ").*?(?="})/g, (subject) => (0, sanitizeStringForJSONParse_1.default)(subject)); + // Then remove newlines, format as JSON and convert to a proper JS object + const json = `[${sanitizedOutput}]`.replace(/(\r\n|\n|\r)/gm, '').replace('},]', '}]'); + return JSON.parse(json); + }); +} +/** + * Parse merged PRs, excluding those from irrelevant branches. + */ +function getValidMergedPRs(commits) { + const mergedPRs = new Set(); + commits.forEach((commit) => { + const author = commit.authorName; + if (author === CONST_1.default.OS_BOTIFY) { + return; + } + const match = commit.subject.match(/Merge pull request #(\d+) from (?!Expensify\/.*-cherry-pick-staging)/); + if (!Array.isArray(match) || match.length < 2) { + return; + } + const pr = Number.parseInt(match[1], 10); + if (mergedPRs.has(pr)) { + // If a PR shows up in the log twice, that means that the PR was deployed in the previous checklist. + // That also means that we don't want to include it in the current checklist, so we remove it now. + mergedPRs.delete(pr); + return; + } + mergedPRs.add(pr); + }); + return Array.from(mergedPRs); } -const previousVersion = versionUpdater.getPreviousVersion(currentVersion ?? '', semverLevel); -core.setOutput('PREVIOUS_VERSION', previousVersion); +/** + * Takes in two git tags and returns a list of PR numbers of all PRs merged between those two tags + */ +async function getPullRequestsMergedBetween(fromTag, toTag) { + console.log(`Looking for commits made between ${fromTag} and ${toTag}...`); + const commitList = await getCommitHistoryAsJSON(fromTag, toTag); + console.log(`Commits made between ${fromTag} and ${toTag}:`, commitList); + // Find which commit messages correspond to merged PR's + const pullRequestNumbers = getValidMergedPRs(commitList).sort((a, b) => a - b); + console.log(`List of pull requests merged between ${fromTag} and ${toTag}`, pullRequestNumbers); + return pullRequestNumbers; +} +exports["default"] = { + getPreviousExistingTag, + getValidMergedPRs, + getPullRequestsMergedBetween, +}; + + +/***/ }), + +/***/ 902: +/***/ ((__unused_webpack_module, exports) => { + +"use strict"; + +/* eslint-disable @typescript-eslint/naming-convention */ +Object.defineProperty(exports, "__esModule", ({ value: true })); +const replacer = (str) => ({ + '\\': '\\\\', + '\t': '\\t', + '\n': '\\n', + '\r': '\\r', + '\f': '\\f', + '"': '\\"', +}[str] ?? ''); +/** + * Replace any characters in the string that will break JSON.parse for our Git Log output + * + * Solution partly taken from SO user Gabriel Rodríguez Flores 🙇 + * https://stackoverflow.com/questions/52789718/how-to-remove-special-characters-before-json-parse-while-file-reading + */ +const sanitizeStringForJSONParse = (inputString) => { + if (typeof inputString !== 'string') { + throw new TypeError('Input must me of type String'); + } + // Replace any newlines and escape backslashes + return inputString.replace(/\\|\t|\n|\r|\f|"/g, replacer); +}; +exports["default"] = sanitizeStringForJSONParse; /***/ }), @@ -2743,7 +3039,7 @@ core.setOutput('PREVIOUS_VERSION', previousVersion); "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getPreviousVersion = exports.incrementPatch = exports.incrementMinor = exports.SEMANTIC_VERSION_LEVELS = exports.MAX_INCREMENTS = exports.incrementVersion = exports.getVersionStringFromNumber = exports.getVersionNumberFromString = void 0; +exports.getPreviousVersion = exports.incrementPatch = exports.incrementMinor = exports.SEMANTIC_VERSION_LEVELS = exports.MAX_INCREMENTS = exports.incrementVersion = exports.getVersionStringFromNumber = exports.getVersionNumberFromString = exports.isValidSemverLevel = void 0; const SEMANTIC_VERSION_LEVELS = { MAJOR: 'MAJOR', MINOR: 'MINOR', @@ -2753,6 +3049,10 @@ const SEMANTIC_VERSION_LEVELS = { exports.SEMANTIC_VERSION_LEVELS = SEMANTIC_VERSION_LEVELS; const MAX_INCREMENTS = 99; exports.MAX_INCREMENTS = MAX_INCREMENTS; +function isValidSemverLevel(str) { + return Object.keys(SEMANTIC_VERSION_LEVELS).includes(str); +} +exports.isValidSemverLevel = isValidSemverLevel; /** * Transforms a versions string into a number */ @@ -2846,6 +3146,14 @@ module.exports = require("assert"); /***/ }), +/***/ 81: +/***/ ((module) => { + +"use strict"; +module.exports = require("child_process"); + +/***/ }), + /***/ 113: /***/ ((module) => { diff --git a/.github/actions/javascript/getPullRequestDetails/index.js b/.github/actions/javascript/getPullRequestDetails/index.js index 14814367e3cd..819fa3ad16a4 100644 --- a/.github/actions/javascript/getPullRequestDetails/index.js +++ b/.github/actions/javascript/getPullRequestDetails/index.js @@ -6679,7 +6679,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -12087,27 +12087,27 @@ exports["default"] = arrayDifference; /***/ }), -/***/ 2877: +/***/ 9491: /***/ ((module) => { -module.exports = eval("require")("encoding"); - +"use strict"; +module.exports = require("assert"); /***/ }), -/***/ 9491: +/***/ 6113: /***/ ((module) => { "use strict"; -module.exports = require("assert"); +module.exports = require("crypto"); /***/ }), -/***/ 6113: +/***/ 3975: /***/ ((module) => { "use strict"; -module.exports = require("crypto"); +module.exports = require("encoding"); /***/ }), diff --git a/.github/actions/javascript/getReleaseBody/index.js b/.github/actions/javascript/getReleaseBody/index.js index 6c746e26a4a4..6260707e1d6c 100644 --- a/.github/actions/javascript/getReleaseBody/index.js +++ b/.github/actions/javascript/getReleaseBody/index.js @@ -6679,7 +6679,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -12031,27 +12031,27 @@ exports["default"] = arrayDifference; /***/ }), -/***/ 2877: +/***/ 9491: /***/ ((module) => { -module.exports = eval("require")("encoding"); - +"use strict"; +module.exports = require("assert"); /***/ }), -/***/ 9491: +/***/ 6113: /***/ ((module) => { "use strict"; -module.exports = require("assert"); +module.exports = require("crypto"); /***/ }), -/***/ 6113: +/***/ 3975: /***/ ((module) => { "use strict"; -module.exports = require("crypto"); +module.exports = require("encoding"); /***/ }), diff --git a/.github/actions/javascript/isStagingDeployLocked/index.js b/.github/actions/javascript/isStagingDeployLocked/index.js index f71b89dc051c..6ba2bd7f9ab2 100644 --- a/.github/actions/javascript/isStagingDeployLocked/index.js +++ b/.github/actions/javascript/isStagingDeployLocked/index.js @@ -6679,7 +6679,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -11985,27 +11985,27 @@ exports["default"] = arrayDifference; /***/ }), -/***/ 2877: +/***/ 9491: /***/ ((module) => { -module.exports = eval("require")("encoding"); - +"use strict"; +module.exports = require("assert"); /***/ }), -/***/ 9491: +/***/ 6113: /***/ ((module) => { "use strict"; -module.exports = require("assert"); +module.exports = require("crypto"); /***/ }), -/***/ 6113: +/***/ 3975: /***/ ((module) => { "use strict"; -module.exports = require("crypto"); +module.exports = require("encoding"); /***/ }), diff --git a/.github/actions/javascript/markPullRequestsAsDeployed/index.js b/.github/actions/javascript/markPullRequestsAsDeployed/index.js index 804d3ea610f3..73ce72460563 100644 --- a/.github/actions/javascript/markPullRequestsAsDeployed/index.js +++ b/.github/actions/javascript/markPullRequestsAsDeployed/index.js @@ -6723,7 +6723,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -12182,27 +12182,27 @@ exports["default"] = arrayDifference; /***/ }), -/***/ 2877: +/***/ 9491: /***/ ((module) => { -module.exports = eval("require")("encoding"); - +"use strict"; +module.exports = require("assert"); /***/ }), -/***/ 9491: +/***/ 6113: /***/ ((module) => { "use strict"; -module.exports = require("assert"); +module.exports = require("crypto"); /***/ }), -/***/ 6113: +/***/ 3975: /***/ ((module) => { "use strict"; -module.exports = require("crypto"); +module.exports = require("encoding"); /***/ }), diff --git a/.github/actions/javascript/postTestBuildComment/index.js b/.github/actions/javascript/postTestBuildComment/index.js index 0b8eb29f1750..ddb85b6a0b15 100644 --- a/.github/actions/javascript/postTestBuildComment/index.js +++ b/.github/actions/javascript/postTestBuildComment/index.js @@ -6723,7 +6723,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -12084,27 +12084,27 @@ exports["default"] = arrayDifference; /***/ }), -/***/ 2877: +/***/ 9491: /***/ ((module) => { -module.exports = eval("require")("encoding"); - +"use strict"; +module.exports = require("assert"); /***/ }), -/***/ 9491: +/***/ 6113: /***/ ((module) => { "use strict"; -module.exports = require("assert"); +module.exports = require("crypto"); /***/ }), -/***/ 6113: +/***/ 3975: /***/ ((module) => { "use strict"; -module.exports = require("crypto"); +module.exports = require("encoding"); /***/ }), diff --git a/.github/actions/javascript/reopenIssueWithComment/index.js b/.github/actions/javascript/reopenIssueWithComment/index.js index d4341ce37dc4..70ce95a72dac 100644 --- a/.github/actions/javascript/reopenIssueWithComment/index.js +++ b/.github/actions/javascript/reopenIssueWithComment/index.js @@ -6679,7 +6679,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -11995,27 +11995,27 @@ exports["default"] = arrayDifference; /***/ }), -/***/ 2877: +/***/ 9491: /***/ ((module) => { -module.exports = eval("require")("encoding"); - +"use strict"; +module.exports = require("assert"); /***/ }), -/***/ 9491: +/***/ 6113: /***/ ((module) => { "use strict"; -module.exports = require("assert"); +module.exports = require("crypto"); /***/ }), -/***/ 6113: +/***/ 3975: /***/ ((module) => { "use strict"; -module.exports = require("crypto"); +module.exports = require("encoding"); /***/ }), diff --git a/.github/actions/javascript/reviewerChecklist/index.js b/.github/actions/javascript/reviewerChecklist/index.js index cc1c0b5a581b..b7d8c4d9c75d 100644 --- a/.github/actions/javascript/reviewerChecklist/index.js +++ b/.github/actions/javascript/reviewerChecklist/index.js @@ -6723,7 +6723,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -12087,27 +12087,27 @@ exports["default"] = arrayDifference; /***/ }), -/***/ 2877: +/***/ 9491: /***/ ((module) => { -module.exports = eval("require")("encoding"); - +"use strict"; +module.exports = require("assert"); /***/ }), -/***/ 9491: +/***/ 6113: /***/ ((module) => { "use strict"; -module.exports = require("assert"); +module.exports = require("crypto"); /***/ }), -/***/ 6113: +/***/ 3975: /***/ ((module) => { "use strict"; -module.exports = require("crypto"); +module.exports = require("encoding"); /***/ }), diff --git a/.github/actions/javascript/verifySignedCommits/index.js b/.github/actions/javascript/verifySignedCommits/index.js index aea35331b1d0..43db8cef405a 100644 --- a/.github/actions/javascript/verifySignedCommits/index.js +++ b/.github/actions/javascript/verifySignedCommits/index.js @@ -6723,7 +6723,7 @@ FetchError.prototype.name = 'FetchError'; let convert; try { - convert = (__nccwpck_require__(2877).convert); + convert = (__nccwpck_require__(3975).convert); } catch (e) {} const INTERNALS = Symbol('Body internals'); @@ -12027,27 +12027,27 @@ exports["default"] = arrayDifference; /***/ }), -/***/ 2877: +/***/ 9491: /***/ ((module) => { -module.exports = eval("require")("encoding"); - +"use strict"; +module.exports = require("assert"); /***/ }), -/***/ 9491: +/***/ 6113: /***/ ((module) => { "use strict"; -module.exports = require("assert"); +module.exports = require("crypto"); /***/ }), -/***/ 6113: +/***/ 3975: /***/ ((module) => { "use strict"; -module.exports = require("crypto"); +module.exports = require("encoding"); /***/ }), diff --git a/.github/libs/GitUtils.ts b/.github/libs/GitUtils.ts index dc8ae037be28..ab4a81f96adf 100644 --- a/.github/libs/GitUtils.ts +++ b/.github/libs/GitUtils.ts @@ -1,7 +1,8 @@ import {execSync, spawn} from 'child_process'; import CONST from './CONST'; import sanitizeStringForJSONParse from './sanitizeStringForJSONParse'; -import * as VERSION_UPDATER from './versionUpdater'; +import * as VersionUpdater from './versionUpdater'; +import type {SemverLevel} from './versionUpdater'; type CommitType = { commit: string; @@ -9,6 +10,64 @@ type CommitType = { authorName: string; }; +/** + * Check if a tag exists locally or in the remote. + */ +function tagExists(tag: string) { + try { + // Check if the tag exists locally + execSync(`git show-ref --tags ${tag}`, {stdio: 'ignore'}); + return true; // Tag exists locally + } catch (error) { + // Tag does not exist locally, check in remote + let shouldRetry = true; + let needsRepack = false; + let doesTagExist = false; + while (shouldRetry) { + try { + if (needsRepack) { + // We have seen some scenarios where this fixes the git fetch. + // Why? Who knows... https://github.com/Expensify/App/pull/31459 + execSync('git repack -d', {stdio: 'inherit'}); + } + execSync(`git ls-remote --exit-code --tags origin ${tag}`, {stdio: 'ignore'}); + doesTagExist = true; + shouldRetry = false; + } catch (e) { + if (!needsRepack) { + console.log('Attempting to repack and retry...'); + needsRepack = true; + } else { + console.error("Repack didn't help, giving up..."); + shouldRetry = false; + } + } + } + return doesTagExist; + } +} + +/** + * This essentially just calls getPreviousVersion in a loop, until it finds a version for which a tag exists. + * It's useful if we manually perform a version bump, because in that case a tag may not exist for the previous version. + * + * @param tag the current tag + * @param level the Semver level to step backward by + */ +function getPreviousExistingTag(tag: string, level: SemverLevel) { + let previousVersion = VersionUpdater.getPreviousVersion(tag, level); + let tagExistsForPreviousVersion = false; + while (!tagExistsForPreviousVersion) { + if (tagExists(previousVersion)) { + tagExistsForPreviousVersion = true; + break; + } + console.log(`Tag for previous version ${previousVersion} does not exist. Checking for an older version...`); + previousVersion = VersionUpdater.getPreviousVersion(previousVersion, level); + } + return previousVersion; +} + /** * @param [shallowExcludeTag] When fetching the given tag, exclude all history reachable by the shallowExcludeTag (used to make fetch much faster) */ @@ -53,8 +112,8 @@ function fetchTag(tag: string, shallowExcludeTag = '') { * Get merge logs between two tags (inclusive) as a JavaScript object. */ function getCommitHistoryAsJSON(fromTag: string, toTag: string): Promise { - // Fetch tags, exclude commits reachable from the previous patch version (i.e: previous checklist), so that we don't have to fetch the full history - const previousPatchVersion = VERSION_UPDATER.getPreviousVersion(fromTag, VERSION_UPDATER.SEMANTIC_VERSION_LEVELS.PATCH); + // Fetch tags, excluding commits reachable from the previous patch version (i.e: previous checklist), so that we don't have to fetch the full history + const previousPatchVersion = getPreviousExistingTag(fromTag, VersionUpdater.SEMANTIC_VERSION_LEVELS.PATCH); fetchTag(fromTag, previousPatchVersion); fetchTag(toTag, previousPatchVersion); @@ -138,6 +197,7 @@ async function getPullRequestsMergedBetween(fromTag: string, toTag: string) { } export default { + getPreviousExistingTag, getValidMergedPRs, getPullRequestsMergedBetween, }; diff --git a/.github/libs/versionUpdater.ts b/.github/libs/versionUpdater.ts index 9b60fb82bd43..d76acaefe4a1 100644 --- a/.github/libs/versionUpdater.ts +++ b/.github/libs/versionUpdater.ts @@ -1,12 +1,19 @@ +import type {ValueOf} from 'type-fest'; + const SEMANTIC_VERSION_LEVELS = { MAJOR: 'MAJOR', MINOR: 'MINOR', PATCH: 'PATCH', BUILD: 'BUILD', } as const; +type SemverLevel = ValueOf; const MAX_INCREMENTS = 99 as const; +function isValidSemverLevel(str: string): str is SemverLevel { + return Object.keys(SEMANTIC_VERSION_LEVELS).includes(str); +} + /** * Transforms a versions string into a number */ @@ -46,7 +53,7 @@ const incrementPatch = (major: number, minor: number, patch: number): string => /** * Increments a build version */ -const incrementVersion = (version: string, level: string): string => { +const incrementVersion = (version: string, level: SemverLevel): string => { const [major, minor, patch, build] = getVersionNumberFromString(version); // Majors will always be incremented @@ -99,7 +106,9 @@ function getPreviousVersion(currentVersion: string, level: string): string { return getVersionStringFromNumber(major, minor, patch, build - 1); } +export type {SemverLevel}; export { + isValidSemverLevel, getVersionNumberFromString, getVersionStringFromNumber, incrementVersion, diff --git a/.github/scripts/buildActions.sh b/.github/scripts/buildActions.sh index 30c284a776be..27d65ca99c9b 100755 --- a/.github/scripts/buildActions.sh +++ b/.github/scripts/buildActions.sh @@ -43,7 +43,7 @@ for ((i=0; i < ${#GITHUB_ACTIONS[@]}; i++)); do ACTION_DIR=$(dirname "$ACTION") # Build the action in the background - ncc build -t "$ACTION" -o "$ACTION_DIR" & + npx ncc build --transpile-only --external encoding "$ACTION" -o "$ACTION_DIR" & ASYNC_BUILDS[i]=$! done diff --git a/.github/scripts/verifyActions.sh b/.github/scripts/verifyActions.sh index ddc8fa0a3226..17316e1aac70 100755 --- a/.github/scripts/verifyActions.sh +++ b/.github/scripts/verifyActions.sh @@ -22,7 +22,7 @@ if [[ EXIT_CODE -eq 0 ]]; then echo -e "${GREEN}Github Actions are up to date!${NC}" exit 0 else - echo -e "${RED}Error: Diff found when Github Actions were rebuilt. Did you forget to run \`npm run gh-actions-build\` after a clean install (\`rm -rf node_modules && npm i\`)? Do you need to merge main? Did you try running \`git config --global core.autocrlf false\` then \`npm run gh-actions-build\` again?${NC}" + echo -e "${RED}Error: Diff found when Github Actions were rebuilt. Did you forget to run \`npm run gh-actions-build\` after a clean install (\`rm -rf node_modules && npm i\`)? Do you need to merge main? Did you try running \`git config --global core.autocrlf false\` then \`npm run gh-actions-build\` again? Did you try running \`npx ncc cache clean\`?${NC}" echo "$DIFF_OUTPUT" | "$LIB_PATH/diff-so-fancy" | less --tabs=4 -RFX exit 1 fi diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d6b346cb3995..36d4248fcc3c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,7 +5,7 @@ on: pull_request: types: [opened, synchronize] branches-ignore: [staging, production] - paths: ['**.js', '**.ts', '**.tsx', '**.sh', 'package.json', 'package-lock.json'] + paths: ['**.js', '**.ts', '**.tsx', 'package.json', 'package-lock.json'] concurrency: group: ${{ github.ref == 'refs/heads/main' && format('{0}-{1}', github.ref, github.sha) || github.ref }}-jest @@ -54,20 +54,3 @@ jobs: - name: Storybook run run: npm run storybook -- --smoke-test --ci - - shellTests: - if: ${{ github.actor != 'OSBotify' && github.actor != 'imgbot[bot]' || github.event_name == 'workflow_call' }} - runs-on: ubuntu-latest - name: Shell tests - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: ./.github/actions/composite/setupNode - - - name: Install ts-node - run: npm i -g ts-node - - - name: Test CI git logic - run: tests/unit/CIGitLogicTest.sh diff --git a/tests/unit/CIGitLogicTest.sh b/tests/unit/CIGitLogicTest.sh deleted file mode 100755 index a2333faaf3f8..000000000000 --- a/tests/unit/CIGitLogicTest.sh +++ /dev/null @@ -1,429 +0,0 @@ -#!/bin/bash - -# Fail immediately if there is an error thrown -set -e - -TEST_DIR=$(dirname "$(dirname "$(cd "$(dirname "$0")" || exit 1; pwd)/$(basename "$0")")") -declare -r SCRIPTS_DIR="$TEST_DIR/../scripts" -declare -r DUMMY_DIR="$HOME/DumDumRepo" -declare -r GIT_REMOTE="$HOME/dummyGitRemotes/DumDumRepo" -declare -r SEMVER_LEVEL_BUILD='BUILD' -declare -r SEMVER_LEVEL_PATCH='PATCH' - -declare -r bumpVersion="$TEST_DIR/utils/bumpVersion.ts" -declare -r getPreviousVersion="$TEST_DIR/utils/getPreviousVersion.ts" -declare -r getPullRequestsMergedBetween="$TEST_DIR/utils/getPullRequestsMergedBetween.ts" - -source "$SCRIPTS_DIR/shellUtils.sh" - -function setup_git_as_human { - info "Switching to human git user" - git config --local user.name test - git config --local user.email test@test.com -} - -function setup_git_as_osbotify { - info "Switching to OSBotify git user" - git config --local user.name OSBotify - git config --local user.email infra+osbotify@expensify.com -} - -function print_version { - < package.json jq -r .version -} - -function init_git_server { - info "Initializing git server..." - cd "$HOME" || exit 1 - rm -rf "$GIT_REMOTE" || exit 1 - mkdir -p "$GIT_REMOTE" - cd "$GIT_REMOTE" || exit 1 - git init -b main - setup_git_as_human - npm init -y - npm version --no-git-tag-version 1.0.0-0 - npm install lodash - echo "node_modules/" >> .gitignore - git add -A - git commit -m "Initial commit" - git switch -c staging - git tag "$(print_version)" - git branch production - git config --local receive.denyCurrentBranch ignore - success "Initialized git server in $GIT_REMOTE" -} - -# Note that instead of doing a git clone, we checkout the repo following the same steps used by actions/checkout -function checkout_repo { - info "Checking out repo at $DUMMY_DIR" - - if [ -d "$DUMMY_DIR" ]; then - info "Found existing directory at $DUMMY_DIR, deleting it to simulate a fresh checkout..." - cd "$HOME" || exit 1 - rm -rf "$DUMMY_DIR" - fi - - mkdir "$DUMMY_DIR" - cd "$DUMMY_DIR" || exit 1 - git init - git remote add origin "$GIT_REMOTE" - git fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +refs/heads/main:refs/remotes/origin/main - git checkout --progress --force -B main refs/remotes/origin/main - success "Checked out repo at $DUMMY_DIR!" -} - -function bump_version { - info "Bumping version..." - setup_git_as_osbotify - git switch main - npm --no-git-tag-version version "$(ts-node "$bumpVersion" "$(print_version)" "$1")" - git add package.json package-lock.json - git commit -m "Update version to $(print_version)" - git push origin main - success "Version bumped to $(print_version) on main" -} - -function update_staging_from_main { - info "Recreating staging from main..." - git switch main - if git rev-parse --verify staging 2>/dev/null; then - git branch -D staging - fi - git switch -c staging - git push --force origin staging - success "Recreated staging from main!" -} - -function update_production_from_staging { - info "Recreating production from staging..." - - if ! git rev-parse --verify staging 2>/dev/null; then - git fetch origin staging --depth=1 - fi - git switch staging - - if git rev-parse --verify production 2>/dev/null; then - git branch -D production - fi - git switch -c production - git push --force origin production - - success "Recreated production from staging!" -} - -function create_basic_pr { - info "Creating PR #$1..." - checkout_repo - setup_git_as_human - git pull - git switch -c "pr-$1" - echo "Changes from PR #$1" >> "PR$1.txt" - git add "PR$1.txt" - git commit -m "Changes from PR #$1" - success "Created PR #$1 in branch pr-$1" -} - -function merge_pr { - info "Merging PR #$1 to main" - git switch main - git merge "pr-$1" --no-ff -m "Merge pull request #$1 from Expensify/pr-$1" - git push origin main - git branch -d "pr-$1" - success "Merged PR #$1 to main" -} - -function cherry_pick_pr { - info "Cherry-picking PR $1 to staging..." - merge_pr "$1" - PR_MERGE_COMMIT="$(git rev-parse HEAD)" - - bump_version "$SEMVER_LEVEL_BUILD" - VERSION_BUMP_COMMIT="$(git rev-parse HEAD)" - - checkout_repo - setup_git_as_osbotify - PREVIOUS_PATCH_VERSION="$(ts-node "$getPreviousVersion" "$(print_version)" "$SEMVER_LEVEL_PATCH")" - git fetch origin main staging --no-tags --shallow-exclude="$PREVIOUS_PATCH_VERSION" - - git switch staging - git switch -c cherry-pick-staging - git cherry-pick -x --mainline 1 "$VERSION_BUMP_COMMIT" - setup_git_as_human - git cherry-pick -x --mainline 1 --strategy=recursive -Xtheirs "$PR_MERGE_COMMIT" - setup_git_as_osbotify - - git switch staging - git merge cherry-pick-staging --no-ff -m "Merge pull request #$(($1 + 1)) from Expensify/cherry-pick-staging" - git branch -d cherry-pick-staging - git push origin staging - info "Merged PR #$(($1 + 1)) into staging" - - tag_staging - - success "Successfully cherry-picked PR #$1 to staging!" -} - -function tag_staging { - info "Tagging new version from the staging branch..." - checkout_repo - setup_git_as_osbotify - if ! git rev-parse --verify staging 2>/dev/null; then - git fetch origin staging --depth=1 - fi - git switch staging - git tag "$(print_version)" - git push --tags - success "Created new tag $(print_version)" -} - -function deploy_staging { - info "Deploying staging..." - checkout_repo - bump_version "$SEMVER_LEVEL_BUILD" - update_staging_from_main - tag_staging - success "Deployed v$(print_version) to staging!" -} - -function deploy_production { - info "Checklist closed, deploying production and staging..." - - info "Deploying production..." - update_production_from_staging - success "Deployed v$(print_version) to production!" - - info "Deploying staging..." - bump_version "$SEMVER_LEVEL_PATCH" - update_staging_from_main - tag_staging - success "Deployed v$(print_version) to staging!" -} - -function assert_prs_merged_between { - checkout_repo - output=$(ts-node "$getPullRequestsMergedBetween" "$1" "$2") - info "Checking output of getPullRequestsMergedBetween $1 $2" - assert_equal "$output" "$3" -} - -### Phase 0: Verify necessary tools are installed (all tools should be pre-installed on all GitHub Actions runners) - -if ! command -v jq &> /dev/null; then - error "command jq could not be found, install it with \`brew install jq\` (macOS) or \`apt-get install jq\` (Linux) and re-run this script" - exit 1 -fi - -if ! command -v npm &> /dev/null; then - error "command npm could not be found, install it and re-run this script" - exit 1 -fi - - -### Setup -title "Starting setup" - -init_git_server -checkout_repo - -success "Setup complete!" - - -title "Scenario #1: Merge a pull request while the checklist is unlocked" - -create_basic_pr 1 -merge_pr 1 -deploy_staging - -# Verify output for checklist and deploy comment -assert_prs_merged_between '1.0.0-0' '1.0.0-1' "[ 1 ]" - -success "Scenario #1 completed successfully!" - - -title "Scenario #2: Merge a pull request with the checklist locked, but don't CP it" - -create_basic_pr 2 -merge_pr 2 - -success "Scenario #2 completed successfully!" - -title "Scenario #3: Merge a pull request with the checklist locked and CP it to staging" - -create_basic_pr 3 -cherry_pick_pr 3 - -# Verify output for checklist -assert_prs_merged_between '1.0.0-0' '1.0.0-2' "[ 1, 3 ]" - -# Verify output for deploy comment -assert_prs_merged_between '1.0.0-1' '1.0.0-2' "[ 3 ]" - -success "Scenario #3 completed successfully!" - - -title "Scenario #4: Close the checklist" - -deploy_production - -# Verify output for release body and production deploy comments -assert_prs_merged_between '1.0.0-0' '1.0.0-2' "[ 1, 3 ]" - -# Verify output for new checklist and staging deploy comments -assert_prs_merged_between '1.0.0-2' '1.0.1-0' "[ 2 ]" - -success "Scenario #4 completed successfully!" - - -title "Scenario #5: Merging another pull request when the checklist is unlocked" - -create_basic_pr 5 -merge_pr 5 -deploy_staging - -# Verify output for checklist -assert_prs_merged_between '1.0.0-2' '1.0.1-1' "[ 2, 5 ]" - -# Verify output for deploy comment -assert_prs_merged_between '1.0.1-0' '1.0.1-1' "[ 5 ]" - -success "Scenario #5 completed successfully!" - -title "Scenario #6: Deploying a PR, then CPing a revert, then adding the same code back again before the next production deploy results in the correct code on staging and production." - -info "Creating myFile.txt in PR #6" -setup_git_as_human -git switch main -git switch -c pr-6 -echo "Changes from PR #6" >> myFile.txt -git add myFile.txt -git commit -m "Add myFile.txt in PR #6" - -merge_pr 6 -deploy_staging - -# Verify output for checklist -assert_prs_merged_between '1.0.0-2' '1.0.1-2' "[ 2, 5, 6 ]" - -# Verify output for deploy comment -assert_prs_merged_between '1.0.1-1' '1.0.1-2' "[ 6 ]" - -info "Appending and prepending content to myFile.txt in PR #7" -setup_git_as_human -git switch main -git switch -c pr-7 -printf "[DEBUG] Before:\n\n%s" "$(cat myFile.txt)" -printf "Prepended content\n%s" "$(cat myFile.txt)" > myFile.txt -printf "\nAppended content\n" >> myFile.txt -printf "\n\n[DEBUG] After:\n\n%s\n" "$(cat myFile.txt)" -git add myFile.txt -git commit -m "Append and prepend content in myFile.txt" - -merge_pr 7 -deploy_staging - -# Verify output for checklist -assert_prs_merged_between '1.0.0-2' '1.0.1-3' "[ 2, 5, 6, 7 ]" - -# Verify output for deploy comment -assert_prs_merged_between '1.0.1-2' '1.0.1-3' "[ 7 ]" - -info "Making an unrelated change in PR #8" -setup_git_as_human -git switch main -git switch -c pr-8 -echo "some content" >> anotherFile.txt -git add anotherFile.txt -git commit -m "Create another file" - -merge_pr 8 - -info "Reverting the append + prepend on main in PR #9" -setup_git_as_human -git switch main -git switch -c pr-9 -printf "Before:\n\n%s\n" "$(cat myFile.txt)" -echo "some content" > myFile.txt -printf "\nAfter:\n\n%s\n" "$(cat myFile.txt)" -git add myFile.txt -git commit -m "Revert append and prepend" - -cherry_pick_pr 9 - -info "Verifying that the revert is present on staging, but the unrelated change is not" -if [[ "$(cat myFile.txt)" != "some content" ]]; then - error "Revert did not make it to staging" - exit 1 -else - success "Revert made it to staging" -fi -if [[ -f anotherFile.txt ]]; then - error "Unrelated change made it to staging" - exit 1 -else - success "Unrelated change not on staging yet" -fi - -info "Repeating previously reverted append + prepend on main in PR #10" -setup_git_as_human -git switch main -git switch -c pr-10 -printf "[DEBUG] Before:\n\n%s" "$(cat myFile.txt)" -printf "Prepended content\n%s" "$(cat myFile.txt)" > myFile.txt -printf "\nAppended content\n" >> myFile.txt -printf "\n\n[DEBUG] After:\n\n%s\n" "$(cat myFile.txt)" -git add myFile.txt -git commit -m "Append and prepend content in myFile.txt" - -merge_pr 10 -deploy_production - -# Verify production release list -assert_prs_merged_between '1.0.0-2' '1.0.1-4' '[ 2, 5, 6, 7, 9 ]' - -# Verify PR list for the new checklist -assert_prs_merged_between '1.0.1-4' '1.0.2-0' '[ 8, 10 ]' - -success "Scenario #6 completed successfully!" - -title "Scenario #7: Force-pushing to a branch after rebasing older commits" - -create_basic_pr 11 -git push origin pr-11 - -create_basic_pr 12 -merge_pr 12 -deploy_staging - -# Verify PRs for checklist -assert_prs_merged_between '1.0.1-4' '1.0.2-1' '[ 8, 10, 12 ]' - -# Verify PRs for deploy comments -assert_prs_merged_between '1.0.2-0' '1.0.2-1' '[ 12 ]' - -info "Rebasing PR #11 onto main and merging it..." -checkout_repo -setup_git_as_human -git fetch origin pr-11 -git switch pr-11 -git rebase main -Xours -git push --force origin pr-11 -merge_pr 11 -success "Rebased PR #11 and merged it to main..." - -deploy_production - -# Verify PRs for deploy comments / release -assert_prs_merged_between '1.0.1-4' '1.0.2-1' '[ 8, 10, 12 ]' - -# Verify PRs for new checklist -assert_prs_merged_between '1.0.2-1' '1.0.3-0' '[ 11 ]' - -success "Scenario #7 complete!" - - -### Cleanup -title "Cleaning up..." -cd "$TEST_DIR" || exit 1 -rm -rf "$DUMMY_DIR" -rm -rf "$GIT_REMOTE" -success "All tests passed! Hooray!" diff --git a/tests/unit/CIGitLogicTest.ts b/tests/unit/CIGitLogicTest.ts new file mode 100644 index 000000000000..cd98891f1f61 --- /dev/null +++ b/tests/unit/CIGitLogicTest.ts @@ -0,0 +1,478 @@ +/** + * @jest-environment node + * @jest-config bail=true + */ + +/* eslint-disable no-console */ +import * as core from '@actions/core'; +import {execSync} from 'child_process'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import getPreviousVersion from '@github/actions/javascript/getPreviousVersion/getPreviousVersion'; +import CONST from '@github/libs/CONST'; +import GitUtils from '@github/libs/GitUtils'; +import * as VersionUpdater from '@github/libs/versionUpdater'; +import type {SemverLevel} from '@github/libs/versionUpdater'; +import asMutable from '@src/types/utils/asMutable'; +import * as Log from '../../scripts/utils/Logger'; + +const DUMMY_DIR = path.resolve(os.homedir(), 'DumDumRepo'); +const GIT_REMOTE = path.resolve(os.homedir(), 'dummyGitRemotes/DumDumRepo'); + +const mockGetInput = jest.fn(); + +type ExecSyncError = {stderr: Buffer}; + +function exec(command: string) { + try { + execSync(command, {stdio: 'inherit'}); + } catch (error) { + if ((error as ExecSyncError).stderr) { + Log.error((error as ExecSyncError).stderr.toString()); + } else { + Log.error('Error:', error); + } + throw new Error(error as string); + } +} + +function setupGitAsHuman() { + Log.info('Switching to human git user'); + exec('git config --local user.name test'); + exec('git config --local user.email test@test.com'); +} + +function setupGitAsOSBotify() { + Log.info('Switching to OSBotify git user'); + exec(`git config --local user.name ${CONST.OS_BOTIFY}`); + exec('git config --local user.email infra+osbotify@expensify.com'); +} + +function getVersion() { + return JSON.parse(fs.readFileSync('package.json', {encoding: 'utf-8'})).version as string; +} + +function initGitServer() { + Log.info('Initializing git server...'); + if (fs.existsSync(GIT_REMOTE)) { + fs.rmSync(GIT_REMOTE, {recursive: true}); + } + fs.mkdirSync(GIT_REMOTE, {recursive: true}); + process.chdir(GIT_REMOTE); + exec('git init -b main'); + setupGitAsHuman(); + exec('npm init -y'); + exec('npm version --no-git-tag-version 1.0.0-0'); + fs.appendFileSync('.gitignore', 'node_modules/\n'); + exec('git add -A'); + exec('git commit -m "Initial commit"'); + exec('git switch -c staging'); + exec(`git tag ${getVersion()}`); + exec('git branch production'); + exec('git config --local receive.denyCurrentBranch ignore'); + Log.success(`Initialized git server in ${GIT_REMOTE}`); +} + +function checkoutRepo() { + if (fs.existsSync(DUMMY_DIR)) { + Log.warn(`Found existing directory at ${DUMMY_DIR}, deleting it to simulate a fresh checkout...`); + fs.rmSync(DUMMY_DIR, {recursive: true}); + } + fs.mkdirSync(DUMMY_DIR); + process.chdir(DUMMY_DIR); + exec('git init'); + exec(`git remote add origin ${GIT_REMOTE}`); + exec('git fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +refs/heads/main:refs/remotes/origin/main'); + exec('git checkout --progress --force -B main refs/remotes/origin/main'); + Log.success('Checked out repo at $DUMMY_DIR!'); +} + +function bumpVersion(level: SemverLevel) { + Log.info('Bumping version...'); + setupGitAsOSBotify(); + exec('git switch main'); + const nextVersion = VersionUpdater.incrementVersion(getVersion(), level); + exec(`npm --no-git-tag-version version ${nextVersion}`); + exec('git add package.json'); + exec(`git commit -m "Update version to ${nextVersion}"`); + exec('git push origin main'); + Log.success(`Version bumped to ${nextVersion} on main`); +} + +function updateStagingFromMain() { + Log.info('Recreating staging from main...'); + exec('git switch main'); + try { + execSync('git rev-parse --verify staging', {stdio: 'ignore'}); + exec('git branch -D staging'); + // eslint-disable-next-line no-empty + } catch (e) {} + exec('git switch -c staging'); + exec('git push --force origin staging'); + Log.success('Recreated staging from main!'); +} + +function updateProductionFromStaging() { + Log.info('Recreating production from staging...'); + + try { + execSync('git rev-parse --verify staging', {stdio: 'ignore'}); + } catch (e) { + exec('git fetch origin staging --depth=1'); + } + + exec('git switch staging'); + + try { + execSync('git rev-parse --verify production', {stdio: 'ignore'}); + exec('git branch -D production'); + // eslint-disable-next-line no-empty + } catch (e) {} + + exec('git switch -c production'); + exec('git push --force origin production'); + Log.success('Recreated production from staging!'); +} + +function createBasicPR(num: number) { + const branchName = `pr-${num}`; + const content = `Changes from PR #${num}`; + const filePath = path.resolve(process.cwd(), `PR${num}.txt`); + + Log.info(`Creating PR #${num}`); + checkoutRepo(); + setupGitAsHuman(); + exec('git pull'); + exec(`git switch -c ${branchName}`); + fs.appendFileSync(filePath, content); + exec(`git add ${filePath}`); + exec(`git commit -m "${content}"`); + Log.success(`Created PR #${num} in branch ${branchName}`); +} + +function mergePR(num: number) { + const branchName = `pr-${num}`; + + Log.info(`Merging PR #${num} to main`); + exec('git switch main'); + exec(`git merge ${branchName} --no-ff -m "Merge pull request #${num} from Expensify/${branchName}"`); + exec('git push origin main'); + exec(`git branch -d ${branchName}`); + Log.success(`Merged PR #${num} to main`); +} + +function cherryPickPR(num: number, resolveVersionBumpConflicts: () => void = () => {}, resolveMergeCommitConflicts: () => void = () => {}) { + Log.info(`Cherry-picking PR ${num} to staging...`); + mergePR(num); + const prMergeCommit = execSync('git rev-parse HEAD', {encoding: 'utf-8'}).trim(); + bumpVersion(VersionUpdater.SEMANTIC_VERSION_LEVELS.BUILD); + const versionBumpCommit = execSync('git rev-parse HEAD', {encoding: 'utf-8'}).trim(); + checkoutRepo(); + setupGitAsOSBotify(); + + mockGetInput.mockReturnValue(VersionUpdater.SEMANTIC_VERSION_LEVELS.PATCH); + const previousPatchVersion = getPreviousVersion(); + exec(`git fetch origin main staging --no-tags --shallow-exclude="${previousPatchVersion}"`); + + exec('git switch staging'); + exec('git switch -c cherry-pick-staging'); + + try { + exec(`git cherry-pick -x --mainline 1 ${versionBumpCommit}`); + } catch (e) { + resolveVersionBumpConflicts(); + } + + setupGitAsHuman(); + + try { + exec(`git cherry-pick -x --mainline 1 --strategy=recursive -Xtheirs ${prMergeCommit}`); + } catch (e) { + resolveMergeCommitConflicts(); + } + + setupGitAsOSBotify(); + exec('git switch staging'); + exec(`git merge cherry-pick-staging --no-ff -m "Merge pull request #${num + 1} from Expensify/cherry-pick-staging"`); + exec('git branch -d cherry-pick-staging'); + exec('git push origin staging'); + Log.info(`Merged PR #${num + 1} into staging`); + tagStaging(); + Log.success(`Successfully cherry-picked PR #${num} to staging!`); +} + +function tagStaging() { + Log.info('Tagging new version from the staging branch...'); + checkoutRepo(); + setupGitAsOSBotify(); + try { + execSync('git rev-parse --verify staging', {stdio: 'ignore'}); + } catch (e) { + exec('git fetch origin staging --depth=1'); + } + exec('git switch staging'); + exec(`git tag ${getVersion()}`); + exec('git push --tags'); + Log.success(`Created new tag ${getVersion()}`); +} + +function deployStaging() { + Log.info('Deploying staging...'); + checkoutRepo(); + bumpVersion(VersionUpdater.SEMANTIC_VERSION_LEVELS.BUILD); + updateStagingFromMain(); + tagStaging(); + Log.success(`Deployed ${getVersion()} to staging!`); +} + +function deployProduction() { + Log.info('Checklist closed, deploying production and staging...'); + + Log.info('Deploying production...'); + updateProductionFromStaging(); + Log.success(`Deployed v${getVersion()} to production!`); + + Log.info('Deploying staging...'); + bumpVersion(VersionUpdater.SEMANTIC_VERSION_LEVELS.PATCH); + updateStagingFromMain(); + tagStaging(); + Log.success(`Deployed v${getVersion()} to staging!`); +} + +async function assertPRsMergedBetween(from: string, to: string, expected: number[]) { + checkoutRepo(); + const PRs = await GitUtils.getPullRequestsMergedBetween(from, to); + expect(PRs).toStrictEqual(expected); +} + +describe('CIGitLogic', () => { + beforeAll(() => { + Log.info('Starting setup'); + initGitServer(); + checkoutRepo(); + Log.success('Setup complete!'); + + // Mock core module + asMutable(core).getInput = mockGetInput; + }); + + afterAll(() => { + fs.rmSync(DUMMY_DIR, {recursive: true, force: true}); + fs.rmSync(path.resolve(GIT_REMOTE, '..'), {recursive: true, force: true}); + }); + + test('Merge a pull request while the checklist is unlocked', () => { + createBasicPR(1); + mergePR(1); + deployStaging(); + + // Verify output for checklist and deploy comment + assertPRsMergedBetween('1.0.0-0', '1.0.0-1', [1]); + }); + + test("Merge a pull request with the checklist locked, but don't CP it", () => { + createBasicPR(2); + mergePR(2); + }); + + test('Merge a pull request with the checklist locked and CP it to staging', () => { + createBasicPR(3); + cherryPickPR(3); + + // Verify output for checklist + assertPRsMergedBetween('1.0.0-0', '1.0.0-2', [1, 3]); + + // Verify output for deploy comment + assertPRsMergedBetween('1.0.0-1', '1.0.0-2', [3]); + }); + + test('Close the checklist', () => { + deployProduction(); + + // Verify output for release body and production deploy comments + assertPRsMergedBetween('1.0.0-0', '1.0.0-2', [1, 3]); + + // Verify output for new checklist and staging deploy comments + assertPRsMergedBetween('1.0.0-2', '1.0.1-0', [2]); + }); + + test('Merging another pull request when the checklist is unlocked', () => { + createBasicPR(5); + mergePR(5); + deployStaging(); + + // Verify output for checklist + assertPRsMergedBetween('1.0.0-2', '1.0.1-1', [2, 5]); + + // Verify output for deploy comment + assertPRsMergedBetween('1.0.1-0', '1.0.1-1', [5]); + }); + + test('Deploying a PR, then CPing a revert, then adding the same code back again before the next production deploy results in the correct code on staging and production', () => { + Log.info('Creating myFile.txt in PR #6'); + setupGitAsHuman(); + exec('git switch main'); + exec('git switch -c pr-6'); + const initialFileContent = 'Changes from PR #6'; + fs.appendFileSync('myFile.txt', 'Changes from PR #6'); + exec('git add myFile.txt'); + exec('git commit -m "Add myFile.txt in PR #6"'); + + mergePR(6); + deployStaging(); + + // Verify output for checklist + assertPRsMergedBetween('1.0.0-2', '1.0.1-2', [2, 5, 6]); + + // Verify output for deploy comment + assertPRsMergedBetween('1.0.1-1', '1.0.1-2', [6]); + + Log.info('Appending and prepending content to myFile.txt in PR #7'); + setupGitAsHuman(); + exec('git switch main'); + exec('git switch -c pr-7'); + const newFileContent = ` +Prepended content +${initialFileContent} +Appended content +`; + fs.writeFileSync('myFile.txt', newFileContent, {encoding: 'utf-8'}); + exec('git add myFile.txt'); + exec('git commit -m "Append and prepend content in myFile.txt"'); + mergePR(7); + deployStaging(); + + // Verify output for checklist + assertPRsMergedBetween('1.0.0-2', '1.0.1-3', [2, 5, 6, 7]); + + // Verify output for deploy comment + assertPRsMergedBetween('1.0.1-2', '1.0.1-3', [7]); + + Log.info('Making an unrelated change in PR #8'); + setupGitAsHuman(); + exec('git switch main'); + exec('git switch -c pr-8'); + fs.appendFileSync('anotherFile.txt', 'some content'); + exec('git add anotherFile.txt'); + exec('git commit -m "Create another file"'); + mergePR(8); + + Log.info('Reverting the append + prepend on main in PR #9'); + setupGitAsHuman(); + exec('git switch main'); + exec('git switch -c pr-9'); + console.log('RORY_DEBUG BEFORE:', fs.readFileSync('myFile.txt', {encoding: 'utf8'})); + fs.writeFileSync('myFile.txt', initialFileContent); + console.log('RORY_DEBUG AFTER:', fs.readFileSync('myFile.txt', {encoding: 'utf8'})); + exec('git add myFile.txt'); + exec('git commit -m "Revert append and prepend"'); + cherryPickPR(9); + + Log.info('Verifying that the revert is present on staging, but the unrelated change is not'); + expect(fs.readFileSync('myFile.txt', {encoding: 'utf8'})).toBe(initialFileContent); + expect(fs.existsSync('anotherFile.txt')).toBe(false); + + Log.info('Repeating previously reverted append + prepend on main in PR #10'); + setupGitAsHuman(); + exec('git switch main'); + exec('git switch -c pr-10'); + fs.writeFileSync('myFile.txt', newFileContent, {encoding: 'utf-8'}); + exec('git add myFile.txt'); + exec('git commit -m "Append and prepend content in myFile.txt"'); + + mergePR(10); + deployProduction(); + + // Verify production release list + assertPRsMergedBetween('1.0.0-2', '1.0.1-4', [2, 5, 6, 7, 9]); + + // Verify PR list for the new checklist + assertPRsMergedBetween('1.0.1-4', '1.0.2-0', [8, 10]); + }); + + test('Force-pushing to a branch after rebasing older commits', () => { + createBasicPR(11); + exec('git push origin pr-11'); + createBasicPR(12); + mergePR(12); + deployStaging(); + + // Verify PRs for checklist + assertPRsMergedBetween('1.0.1-4', '1.0.2-1', [8, 10, 12]); + + // Verify PRs for deploy comments + assertPRsMergedBetween('1.0.2-0', '1.0.2-1', [12]); + + checkoutRepo(); + setupGitAsHuman(); + exec('git fetch origin pr-11'); + exec('git switch pr-11'); + exec('git rebase main -Xours'); + exec('git push --force origin pr-11'); + mergePR(11); + + deployProduction(); + + // Verify PRs for deploy comments / release + assertPRsMergedBetween('1.0.1-4', '1.0.2-1', [8, 10, 12]); + + // Verify PRs for new checklist + assertPRsMergedBetween('1.0.2-1', '1.0.3-0', [11]); + }); + + test('Manual version bump', () => { + Log.info('Creating manual version bump in PR #13'); + checkoutRepo(); + setupGitAsHuman(); + exec('git pull'); + exec('git switch -c "pr-13"'); + for (let i = 0; i < 3; i++) { + exec(`npm --no-git-tag-version version ${VersionUpdater.incrementVersion(getVersion(), VersionUpdater.SEMANTIC_VERSION_LEVELS.MAJOR)}`); + } + exec('git add package.json'); + exec(`git commit -m "Manually bump version to ${getVersion()} in PR #13"`); + Log.success('Created manual version bump in PR #13 in branch pr-13'); + + mergePR(13); + Log.info('Deploying staging...'); + checkoutRepo(); + updateStagingFromMain(); + tagStaging(); + Log.success(`Deployed v${getVersion()} to staging!`); + + // Verify PRs for deploy comments / release and new checklist + assertPRsMergedBetween('1.0.3-0', '4.0.0-0', [13]); + + Log.info('Creating manual version bump in PR #14'); + checkoutRepo(); + setupGitAsHuman(); + exec('git pull'); + exec('git switch -c "pr-14"'); + for (let i = 0; i < 3; i++) { + exec(`npm --no-git-tag-version version ${VersionUpdater.incrementVersion(getVersion(), VersionUpdater.SEMANTIC_VERSION_LEVELS.MAJOR)}`); + } + exec('git add package.json'); + exec(`git commit -m "Manually bump version to ${getVersion()} in PR #14"`); + Log.success('Created manual version bump in PR #14 in branch pr-14'); + + const packageJSONBefore = fs.readFileSync('package.json', {encoding: 'utf-8'}); + cherryPickPR( + 14, + () => { + fs.writeFileSync('package.json', packageJSONBefore); + exec('git add package.json'); + exec('git cherry-pick --continue'); + }, + () => { + exec('git commit --no-edit --allow-empty'); + }, + ); + + // Verify PRs for deploy comments + assertPRsMergedBetween('4.0.0-0', '7.0.0-0', [14]); + + // Verify PRs for the deploy checklist + assertPRsMergedBetween('1.0.3-0', '7.0.0-0', [13, 14]); + }); +}); diff --git a/tests/utils/bumpVersion.ts b/tests/utils/bumpVersion.ts deleted file mode 100644 index e187de508dab..000000000000 --- a/tests/utils/bumpVersion.ts +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env node -import {incrementVersion} from '../../.github/libs/versionUpdater'; - -const version = process.argv[2]; -const level = process.argv[3]; - -/* eslint-disable no-console */ -const realConsoleLog = console.log; -console.log = () => {}; - -const output = incrementVersion(version, level); - -console.log = realConsoleLog; -console.log(output); diff --git a/tests/utils/getPreviousVersion.ts b/tests/utils/getPreviousVersion.ts deleted file mode 100644 index 1eecf2fe5180..000000000000 --- a/tests/utils/getPreviousVersion.ts +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env node -import {getPreviousVersion} from '../../.github/libs/versionUpdater'; - -const currentVersion = process.argv[2]; -const level = process.argv[3]; - -/* eslint-disable no-console */ -const realConsoleLog = console.log; -console.log = () => {}; - -const output = getPreviousVersion(currentVersion, level); - -console.log = realConsoleLog; -console.log(output); diff --git a/tests/utils/getPullRequestsMergedBetween.ts b/tests/utils/getPullRequestsMergedBetween.ts deleted file mode 100755 index 2bf5a0835710..000000000000 --- a/tests/utils/getPullRequestsMergedBetween.ts +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env node -import GitUtils from '../../.github/libs/GitUtils'; - -const fromRef = process.argv[2]; -const toRef = process.argv[3]; - -/* eslint-disable no-console */ -const realConsoleLog = console.log; -console.log = () => {}; - -async function main() { - const output = await GitUtils.getPullRequestsMergedBetween(fromRef, toRef); - - console.log = realConsoleLog; - console.log(output); -} -main(); diff --git a/workflow_tests/assertions/testAssertions.ts b/workflow_tests/assertions/testAssertions.ts index 06379403a834..90fc624a76be 100644 --- a/workflow_tests/assertions/testAssertions.ts +++ b/workflow_tests/assertions/testAssertions.ts @@ -38,20 +38,4 @@ function assertJestJobExecuted(workflowResult: Step[], didExecute = true, timesE }); } -function assertShellTestsJobExecuted(workflowResult: Step[], didExecute = true) { - const steps = [ - createStepAssertion('Checkout', true, null, 'SHELLTESTS', 'Checkout', [], []), - createStepAssertion('Setup Node', true, null, 'SHELLTESTS', 'Setup Node', [], []), - createStepAssertion('Test CI git logic', true, null, 'SHELLTESTS', 'Test CI git logic', [], []), - ] as const; - - steps.forEach((expectedStep) => { - if (didExecute) { - expect(workflowResult).toEqual(expect.arrayContaining([expectedStep])); - } else { - expect(workflowResult).not.toEqual(expect.arrayContaining([expectedStep])); - } - }); -} - -export default {assertJestJobExecuted, assertShellTestsJobExecuted}; +export default {assertJestJobExecuted}; diff --git a/workflow_tests/mocks/testMocks.ts b/workflow_tests/mocks/testMocks.ts index 2d9527957b91..96bec3f0422a 100644 --- a/workflow_tests/mocks/testMocks.ts +++ b/workflow_tests/mocks/testMocks.ts @@ -15,10 +15,4 @@ const TEST__JEST__STEP_MOCKS = [ TEST__JEST__JEST_TESTS__STEP_MOCK, ]; -// shelltests -const TEST__SHELLTESTS__CHECKOUT__STEP_MOCK = createMockStep('Checkout', 'Checkout', 'SHELLTESTS', [], []); -const TEST__SHELLTESTS__SETUP_NODE__STEP_MOCK = createMockStep('Setup Node', 'Setup Node', 'SHELLTESTS', [], []); -const TEST__SHELLTESTS__TEST_CI_GIT_LOGIC__STEP_MOCK = createMockStep('Test CI git logic', 'Test CI git logic', 'SHELLTESTS', [], []); -const TEST__SHELLTESTS__STEP_MOCKS = [TEST__SHELLTESTS__CHECKOUT__STEP_MOCK, TEST__SHELLTESTS__SETUP_NODE__STEP_MOCK, TEST__SHELLTESTS__TEST_CI_GIT_LOGIC__STEP_MOCK]; - -export default {TEST__JEST__STEP_MOCKS, TEST__SHELLTESTS__STEP_MOCKS}; +export default {TEST__JEST__STEP_MOCKS}; diff --git a/workflow_tests/test.test.ts b/workflow_tests/test.test.ts index 085a2b3902d6..54beb8235eb7 100644 --- a/workflow_tests/test.test.ts +++ b/workflow_tests/test.test.ts @@ -59,7 +59,6 @@ describe('test workflow test', () => { act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); const testMockSteps: MockStep = { jest: mocks.TEST__JEST__STEP_MOCKS, - shellTests: mocks.TEST__SHELLTESTS__STEP_MOCKS, }; const result = await act.runEvent(event, { workflowFile: path.join(repoPath, '.github', 'workflows', 'test.yml'), @@ -69,7 +68,6 @@ describe('test workflow test', () => { }); assertions.assertJestJobExecuted(result); - assertions.assertShellTestsJobExecuted(result); }); describe('actor is OSBotify', () => { it('does not run tests', async () => { @@ -79,7 +77,6 @@ describe('test workflow test', () => { act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); const testMockSteps: MockStep = { jest: mocks.TEST__JEST__STEP_MOCKS, - shellTests: mocks.TEST__SHELLTESTS__STEP_MOCKS, }; const result = await act.runEvent(event, { workflowFile: path.join(repoPath, '.github', 'workflows', 'test.yml'), @@ -89,7 +86,6 @@ describe('test workflow test', () => { }); assertions.assertJestJobExecuted(result, false); - assertions.assertShellTestsJobExecuted(result, false); }); }); }); @@ -106,7 +102,6 @@ describe('test workflow test', () => { act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); const testMockSteps: MockStep = { jest: mocks.TEST__JEST__STEP_MOCKS, - shellTests: mocks.TEST__SHELLTESTS__STEP_MOCKS, }; const result = await act.runEvent(event, { workflowFile: path.join(repoPath, '.github', 'workflows', 'test.yml'), @@ -116,7 +111,6 @@ describe('test workflow test', () => { }); assertions.assertJestJobExecuted(result); - assertions.assertShellTestsJobExecuted(result); }); describe('actor is OSBotify', () => { it('does not run tests', async () => { @@ -126,7 +120,6 @@ describe('test workflow test', () => { act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); const testMockSteps: MockStep = { jest: mocks.TEST__JEST__STEP_MOCKS, - shellTests: mocks.TEST__SHELLTESTS__STEP_MOCKS, }; const result = await act.runEvent(event, { workflowFile: path.join(repoPath, '.github', 'workflows', 'test.yml'), @@ -136,7 +129,6 @@ describe('test workflow test', () => { }); assertions.assertJestJobExecuted(result, false); - assertions.assertShellTestsJobExecuted(result, false); }); }); }); @@ -151,7 +143,6 @@ describe('test workflow test', () => { act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); const testMockSteps: MockStep = { jest: mocks.TEST__JEST__STEP_MOCKS, - shellTests: mocks.TEST__SHELLTESTS__STEP_MOCKS, }; const result = await act.runEvent(event, { workflowFile: path.join(repoPath, '.github', 'workflows', 'test.yml'), @@ -161,7 +152,6 @@ describe('test workflow test', () => { }); assertions.assertJestJobExecuted(result); - assertions.assertShellTestsJobExecuted(result); }); describe('actor is OSBotify', () => { it('runs all tests normally', async () => { @@ -171,7 +161,6 @@ describe('test workflow test', () => { act = utils.setUpActParams(act, event, eventOptions, {}, githubToken); const testMockSteps: MockStep = { jest: mocks.TEST__JEST__STEP_MOCKS, - shellTests: mocks.TEST__SHELLTESTS__STEP_MOCKS, }; const result = await act.runEvent(event, { workflowFile: path.join(repoPath, '.github', 'workflows', 'test.yml'), @@ -181,7 +170,6 @@ describe('test workflow test', () => { }); assertions.assertJestJobExecuted(result); - assertions.assertShellTestsJobExecuted(result); }); }); });