tutanota/buildSrc/bump-version.js

256 lines
8.6 KiB
JavaScript
Executable file

#!/usr/bin/env node
// @ts-check
import { Option, program } from "commander"
import fs from "node:fs"
import path from "node:path"
import { $ } from "zx"
import { calculateClientVersions } from "./versionUtils.js"
await program
.addOption(new Option("-p, --platform <platform>", "version for which platform to bump").choices(["all", "webdesktop", "android", "ios"]).default("all"))
.action(run)
.parseAsync(process.argv)
/**
* @typedef {"major" | "minor" | "patch"} Which
* @typedef {{name: string, directory: string}} Workspace
*/
/**
* @param params {object}
* @param params.platform {undefined | "webdesktop" | "android" | "ios" | "all"}
* @return {Promise<void>}
*/
async function run({ platform }) {
console.log(`bumping version for ${platform ?? "all"}`)
const { currentVersion, currentVersionString, newVersionString } = await calculateClientVersions()
await bumpVersionInCargoWorkspace(newVersionString)
if (platform === "all" || platform === "webdesktop") {
await bumpWorkspaces(newVersionString)
await $`npm version --no-git-tag-version ${newVersionString}`
// Need to clean and re-install to make sure that all packages
// are installed with the correct version. otherwise, npm list
// from the tutanota-3 postinstall script will complain about
// invalid installed versions after npm i.
await fs.promises.rm("./node_modules", { recursive: true })
await $`npm i`
}
if (platform === "all" || platform === "ios") {
await bumpIosVersion(newVersionString)
}
if (platform === "all" || platform === "android") {
await bumpAndroidVersion("app-android/app/build.gradle.kts")
await bumpAndroidVersion("app-android/calendar/build.gradle.kts")
await bumpAndroidVersionName(currentVersion, newVersionString, "app-android/app/build.gradle.kts")
await bumpAndroidVersionName(currentVersion, newVersionString, "app-android/calendar/build.gradle.kts")
}
console.log(`Bumped version ${currentVersionString} -> ${newVersionString}`)
}
/**
* @param newVersionString {string}
* @return {Promise<void>}
*/
async function bumpVersionInCargoWorkspace(newVersionString) {
const workspaceFilePath = "Cargo.toml"
const versionRegex = /\[workspace\.package]\nversion = ".*"/
const contents = await fs.promises.readFile(workspaceFilePath, "utf8")
let found = 0
const newContents = contents.replace(versionRegex, (_, __, ___, ____) => {
found += 1
return `[workspace.package]\nversion = "${newVersionString}"`
})
if (found !== 1) {
console.warn(`${workspaceFilePath} had an unexpected format and couldn't be updated. Is it corrupted?`)
} else {
console.log(`rust: Updated ${workspaceFilePath} package.version to ${newVersionString}`)
await fs.promises.writeFile(workspaceFilePath, newContents)
// regenerate the lockfile
await $`cargo update tuta-sdk tutao_node-mimimi && cargo check --all`
}
}
/**
* @param newVersionString {string}
*/
async function bumpIosVersion(newVersionString) {
const plists = [
"app-ios/calendar/Info.plist",
"app-ios/tutanota/Info.plist",
"app-ios/TutanotaNotificationExtension/Info.plist",
"app-ios/tutanotaTests/Info.plist",
]
for (const plist of plists) {
await replaceCfBundleVersion(plist, newVersionString)
}
}
/**
* @param {string} filePath
*/
async function replaceCfBundleVersion(filePath, newVersionString) {
const infoPlistContents = await fs.promises.readFile(filePath, "utf8")
let found = 0
const newInfoPlistContents = infoPlistContents.replaceAll(
/<key>CFBundle(Short)?Version(String)?<\/key>\s+<string>(\d+\.\d+\.\d+)<\/string>/g,
(match, _, __, version) => {
found += 1
return match.replace(version, newVersionString)
},
)
if (found !== 2) {
console.warn(`${filePath} had an unexpected format and couldn't be updated. Is it corrupted?`)
} else {
console.log(`iOS: Updated ${filePath} to ${newVersionString}`)
await fs.promises.writeFile(filePath, newInfoPlistContents)
}
}
/**
* @param buildGradlePath {string}
* @return {Promise<void>}
*/
async function bumpAndroidVersion(buildGradlePath) {
const buildGradleString = await fs.promises.readFile(buildGradlePath, "utf8")
const kotlinRegex = /versionCode = (\d+)/
const groovyRegex = /versionCode (\d+)/
const versionRegex = new RegExp(buildGradlePath.endsWith("kts") ? kotlinRegex : groovyRegex)
const oldVersionCodeMatch = buildGradleString.match(versionRegex)
if (oldVersionCodeMatch == null) {
throw new Error(`Android: Could not find versionCode in ${buildGradlePath}! Is it corrupted?`)
}
const oldVersionCodeString = oldVersionCodeMatch[1]
const oldVersionCode = parseInt(oldVersionCodeString, 10)
if (Number.isNaN(oldVersionCode)) {
throw new Error(`Android: Detected version code as ${oldVersionCodeMatch[1]} but it is not a number! Is it corrupted`)
}
const newVersionCode = oldVersionCode + 1
const newVersionCodeString = String(newVersionCode)
const versionCodeToWrite = `versionCode ${buildGradlePath.endsWith("kts") ? "= " : ""}${newVersionCodeString}`
const newBuildGradleString = buildGradleString.replace(new RegExp(versionRegex), versionCodeToWrite)
console.log(`Bumped Android versionCode: ${oldVersionCodeString} -> ${newVersionCodeString} (${buildGradlePath})`)
await fs.promises.writeFile(buildGradlePath, newBuildGradleString)
}
/**
* @param currentVersion {number[]}
* @param newVersionString {string}
* @param buildGradlePath {string}
* @return {Promise<void>}
*/
async function bumpAndroidVersionName(currentVersion, newVersionString, buildGradlePath) {
const buildGradleString = await fs.promises.readFile(buildGradlePath, "utf8")
const newBuildGradleString = buildGradleString.replace(new RegExp(currentVersion.join("\\.")), newVersionString)
console.log(`Bumped Android versionName: ${currentVersion.join("\\.")} -> ${newVersionString} (${buildGradlePath})`)
await fs.promises.writeFile(buildGradlePath, newBuildGradleString)
}
/**
* @return {string[]}
*/
function getWorkspaceDirs() {
const packagesDir = path.resolve("./packages")
const relativePaths = fs.readdirSync(packagesDir)
return relativePaths.map((relativePath) => path.join(packagesDir, relativePath))
}
/**
* @return {Promise<Workspace[]>}
*/
async function getWorkspaces() {
const workspaces = []
const workspaceDirs = getWorkspaceDirs()
for (let workspaceDir of workspaceDirs) {
const packageJson = await readPackageJsonFromDir(workspaceDir)
workspaces.push({
name: packageJson.name,
directory: workspaceDir,
})
}
return workspaces
}
/**
* @param version {string}
* @return {Promise<void>}
*/
async function bumpWorkspaces(version) {
// if we don't first bump versions of packages and _then_
// update the inter-package dependencies we will get an error
// in the middle of the process because npm can't resolve something
const workspaces = await getWorkspaces()
for (const workspace of workspaces) {
await bumpWorkspaceVersion(version, workspace)
}
for (const workspace of workspaces) {
const dependency = workspace.name
await updateDependencyForWorkspaces(version, dependency, workspaces)
}
}
/**
* @param version {string}
* @param workspace {Workspace}
* @return {Promise<void>}
*/
async function bumpWorkspaceVersion(version, workspace) {
// set --workspaces-update to false to not run install (it's slow, doesn't work sometimes and we don't need it)
await $`npm version --workspaces-update false --no-git-tag-version --workspace=${workspace.name} ${version}`
}
/**
* @param directory {string}
* @return {Promise<any>}
*/
async function readPackageJsonFromDir(directory) {
const packageJsonPath = path.join(directory, "package.json")
const packageJsonContents = await fs.promises.readFile(packageJsonPath, { encoding: "utf8" })
return JSON.parse(packageJsonContents)
}
/**
* @param version {string}
* @param dependency {string}
* @param workspaces {Workspace[]}
* @return {Promise<void>}
*/
async function updateDependencyForWorkspaces(version, dependency, workspaces) {
await updateDependency({ version, dependency, directory: "." })
for (const workspace of workspaces) {
const directory = workspace.directory
await updateDependency({ version, dependency, directory })
}
}
/**
* @param {{version: string, dependency: string, directory: string}} params
* @return {Promise<void>}
*/
async function updateDependency({ version, dependency, directory }) {
const packageJson = await readPackageJsonFromDir(directory)
if (packageJson.dependencies && dependency in packageJson.dependencies) {
packageJson.dependencies[dependency] = version
}
if (packageJson.devDependencies && dependency in packageJson.devDependencies) {
packageJson.devDependencies[dependency] = version
}
await fs.promises.writeFile(path.join(directory, "package.json"), JSON.stringify(packageJson, null, "\t"), { encoding: "utf8" })
}