scripts/bumpVersion.ts
Rama main · réplica de lectura del repositorio original.
import * as versionUpdater from '@github/libs/versionUpdater';
import type {SemverLevel} from '@github/libs/versionUpdater';
import type {SemVer} from 'semver';
import type {PackageJson} from 'type-fest';
import {execSync, exec as originalExec} from 'child_process';
import {promises as fs} from 'fs';
import path from 'path';
import getMajorVersion from 'semver/functions/major';
import getMinorVersion from 'semver/functions/minor';
import getPatchVersion from 'semver/functions/patch';
// Disabling lint on the next two imports due to a bug in @dword-design/import-alias/prefer-alias
import getBuildVersion from 'semver/functions/prerelease';
import {promisify} from 'util';
const exec = promisify(originalExec);
type ConfigJSON = {
meta: {
version: string;
};
};
// PlistBuddy executable path
const PLIST_BUDDY = '/usr/libexec/PlistBuddy';
/**
* This is a utility function to get the repo root.
* It's a helpful alternative to __dirname, which doesn't work with bundled/compiled scripts.
* __dirname doesn't work, because:
* - if it's evaluated at compile time it will include an absolute path in the computer in which the file was compiled
* - if it's evaluated at runtime, it won't refer to the directory of the imported module, because the code will have moved to wherever it's bundled
*/
function getRepoRoot(): string {
return execSync('git rev-parse --show-toplevel', {
encoding: 'utf8',
}).trim();
}
// Filepath constants
const ROOT_DIR = getRepoRoot();
const PACKAGE_JSON_PATH = path.resolve(ROOT_DIR, 'package.json');
const BUILD_GRADLE_PATH = path.resolve(ROOT_DIR, 'android/app/build.gradle');
const PLIST_PATH = path.resolve(ROOT_DIR, 'ios/NewExpensify/Info.plist');
const PLIST_PATH_NSE = path.resolve(ROOT_DIR, 'ios/NotificationServiceExtension/Info.plist');
const PLIST_PATH_SHARE = path.resolve(ROOT_DIR, 'ios/ShareViewController/Info.plist');
// Filepath constants (submodule)
const MOBILE_EXPENSIFY_DIR = path.resolve(ROOT_DIR, 'Mobile-Expensify');
const MOBILE_EXPENSIFY_CONFIG_JSON_PATH = path.resolve(MOBILE_EXPENSIFY_DIR, 'app/config/config.json');
const MOBILE_EXPENSIFY_ANDROID_MANIFEST_PATH = path.resolve(MOBILE_EXPENSIFY_DIR, 'Android/AndroidManifest.xml');
const MOBILE_EXPENSIFY_PLIST_PATH = path.resolve(MOBILE_EXPENSIFY_DIR, 'iOS/Expensify/Expensify-Info.plist');
const MOBILE_EXPENSIFY_PLIST_PATH_NSE = path.resolve(MOBILE_EXPENSIFY_DIR, 'iOS/NotificationServiceExtension/Info.plist');
const MOBILE_EXPENSIFY_PLIST_PATH_SS = path.resolve(MOBILE_EXPENSIFY_DIR, 'iOS/SmartScanExtension/Info.plist');
/**
* Pad a number to be two digits (with leading zeros if necessary).
*/
function padToTwoDigits(value: number): string {
if (value >= 10) {
return value.toString();
}
return `0${value.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.
*/
function generateAndroidVersionCode(npmVersion: string | SemVer, prefix: string): string {
return ''.concat(
prefix,
padToTwoDigits(getMajorVersion(npmVersion) ?? 0),
padToTwoDigits(getMinorVersion(npmVersion) ?? 0),
padToTwoDigits(getPatchVersion(npmVersion) ?? 0),
padToTwoDigits(Number(getBuildVersion(npmVersion)) ?? 0),
);
}
/**
* Update the Android native versions in E/App and the Mobile-Expensify submodule.
*/
async function updateAndroid(version: string) {
console.log(`Updating Android versions to ${version}`);
try {
const versionNamePattern = '([0-9.-]*)';
const versionCodePattern = '([0-9]*)';
const updateBuildGradle = async () => {
// build.gradle versions will be prefixed with '10' due to previous versioning
const versionCode = generateAndroidVersionCode(version, '10');
console.log(`Updating ${BUILD_GRADLE_PATH}:`, {version, versionCode});
const fileContent = await fs.readFile(BUILD_GRADLE_PATH, {encoding: 'utf8'});
const updatedContent = fileContent
.replace(new RegExp(`versionName "${versionNamePattern}"`), `versionName "${version}"`)
.replace(new RegExp(`versionCode ${versionCodePattern}`), `versionCode ${versionCode}`);
await fs.writeFile(BUILD_GRADLE_PATH, updatedContent, {encoding: 'utf8'});
console.log(`Updated ${BUILD_GRADLE_PATH}`);
};
const updateAndroidManifest = async () => {
// AndroidManifest.xml versions will be prefixed with '05' due to previous versioning
const versionCode = generateAndroidVersionCode(version, '05');
console.log(`Updating ${MOBILE_EXPENSIFY_ANDROID_MANIFEST_PATH}:`, {version, versionCode});
const fileContent = await fs.readFile(MOBILE_EXPENSIFY_ANDROID_MANIFEST_PATH, {encoding: 'utf8'});
const updatedContent = fileContent
.replace(new RegExp(`android:versionName="${versionNamePattern}"`), `android:versionName="${version}"`)
.replace(new RegExp(`android:versionCode="${versionCodePattern}"`), `android:versionCode="${versionCode}"`);
await fs.writeFile(MOBILE_EXPENSIFY_ANDROID_MANIFEST_PATH, updatedContent, {encoding: 'utf8'});
console.log(`Updated ${MOBILE_EXPENSIFY_ANDROID_MANIFEST_PATH}`);
};
await Promise.all([updateBuildGradle(), updateAndroidManifest()]);
console.log('Successfully updated Android');
} catch (err) {
console.error('Error updating Android');
throw new Error('Error updating Android');
}
}
/**
* Update the iOS native versions in E/App and the Mobile-Expensify submodule.
*/
async function updateIOS(version: string) {
console.log(`Updating native versions to ${version}`);
try {
const PLIST_KEYS = {
CF_BUNDLE_SHORT_VERSION: 'CFBundleShortVersionString',
CF_BUNDLE_VERSION: 'CFBundleVersion',
};
const shortVersion = version.split('-').at(0);
const cfVersion = version.includes('-') ? version.replace('-', '.') : `${version}.0`;
console.log('Updating iOS', {
[PLIST_KEYS.CF_BUNDLE_SHORT_VERSION]: shortVersion,
[PLIST_KEYS.CF_BUNDLE_VERSION]: cfVersion,
});
// Update plist
await Promise.all(
[PLIST_PATH, PLIST_PATH_NSE, PLIST_PATH_SHARE, MOBILE_EXPENSIFY_PLIST_PATH, MOBILE_EXPENSIFY_PLIST_PATH_NSE, MOBILE_EXPENSIFY_PLIST_PATH_SS].map(async (file) => {
console.log(`Updating ${file}`);
await exec(`${PLIST_BUDDY} -c "Set :${PLIST_KEYS.CF_BUNDLE_SHORT_VERSION} ${shortVersion}" ${file}`);
await exec(`${PLIST_BUDDY} -c "Set :${PLIST_KEYS.CF_BUNDLE_VERSION} ${cfVersion}" ${file}`);
console.log(`Updated ${file}`);
}),
);
console.log('Successfully updated iOS');
} catch (err) {
console.error('Error updating iOS');
throw new Error('Error updating iOS');
}
}
/**
* Update package.json and package-lock.json
*/
async function updateNPM(version: string) {
console.log(`Setting npm version to ${version}`);
try {
const {stdout} = await exec(`npm --no-git-tag-version version ${version} -m "Update version to ${version}"`);
// NPM and native versions successfully updated, output new version
console.log(stdout);
} catch (err) {
// Log errors and fail gracefully
if (err instanceof Error) {
console.error('Error:', err.message);
}
throw new Error('Error updating npm version');
}
}
/**
* Update Mobile-Expensify config.json.
*/
async function updateConfigJSON(version: string) {
try {
console.log(`Updating ${MOBILE_EXPENSIFY_CONFIG_JSON_PATH} to ${version}`);
const fileContent = JSON.parse(await fs.readFile(MOBILE_EXPENSIFY_CONFIG_JSON_PATH, {encoding: 'utf8'})) as ConfigJSON;
fileContent.meta.version = version;
await fs.writeFile(MOBILE_EXPENSIFY_CONFIG_JSON_PATH, JSON.stringify(fileContent, null, 4), {encoding: 'utf8'});
console.log(`Updated ${MOBILE_EXPENSIFY_CONFIG_JSON_PATH}`);
} catch (err) {
// Log errors and fail gracefully
if (err instanceof Error) {
console.error('Error:', err.message);
}
throw new Error('Error updating Mobile-Expensify config.json');
}
}
async function run(semanticVersionLevel: SemverLevel) {
// Parse the current version from package.json
const {version: previousVersion} = JSON.parse(await fs.readFile(PACKAGE_JSON_PATH, {encoding: 'utf8'})) as PackageJson;
if (!previousVersion) {
throw new Error('Could not read package.json');
}
// Figure out the next version
const newVersion = versionUpdater.incrementVersion(previousVersion ?? '', semanticVersionLevel);
console.log(`Previous version: ${previousVersion}`, `New version: ${newVersion}`);
// Apply the version changes in Android, iOS, and JS config files (E/App and Mobile-Expensify)
await Promise.all([updateAndroid(newVersion), updateIOS(newVersion), updateNPM(newVersion), updateConfigJSON(newVersion)]);
return newVersion;
}
export default run;
export {updateAndroid, generateAndroidVersionCode};