#!/usr/bin/env node /* eslint-disable import/no-extraneous-dependencies */ /* * Copyright 2022 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const fs = require('fs-extra'); const semver = require('semver'); const { getPackages } = require('@manypkg/get-packages'); const path = require('path'); const { execFile: execFileCb } = require('child_process'); const { promisify } = require('util'); const execFile = promisify(execFileCb); // All of these are considered to be main-line release branches const MAIN_BRANCHES = ['master', 'origin/master', 'changeset-release/master']; const DEPENDENCY_TYPES = [ 'dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies', ]; /** * Bumps up the versions of packages to account for * the base versions that are set in .changeset/patched.json. * This may be needed when we have made emergency releases. */ async function updatePatchVersions() { const patchedJsonPath = path.resolve('.changeset', 'patched.json'); const { currentReleaseVersion } = await fs.readJson(patchedJsonPath); if (Object.keys(currentReleaseVersion).length === 0) { console.log('No currentReleaseVersion overrides found, skipping.'); return; } const { packages } = await getPackages(path.resolve('.')); const pendingVersionBumps = new Map(); for (const [name, version] of Object.entries(currentReleaseVersion)) { const pkg = packages.find(p => p.packageJson.name === name); if (!pkg) { throw new Error(`Package ${name} not found`); } if (!semver.valid(version)) { throw new Error(`Invalid base version ${version} for package ${name}`); } let targetVersion = version; // If we're currently in a pre-release we need to manually execute the // patch bump up to the next version. And we also need to make sure we // resume the releases at the same pre-release tag. const currentPrerelease = semver.prerelease(pkg.packageJson.version); if (currentPrerelease) { const parsed = targetVersion.parse(version); parsed.inc('patch'); parsed.prerelease = currentPrerelease; targetVersion = parsed.format(); } pendingVersionBumps.set(name, { targetVersion, targetRange: `^${targetVersion}`, }); } for (const { dir, packageJson } of packages) { let hasChanges = false; if (pendingVersionBumps.has(packageJson.name)) { packageJson.version = pendingVersionBumps.get( packageJson.name, ).targetVersion; hasChanges = true; } for (const depType of DEPENDENCY_TYPES) { const deps = packageJson[depType]; for (const depName of Object.keys(deps ?? {})) { const currentRange = deps[depName]; if (currentRange === '*' || currentRange === '') { continue; } if (pendingVersionBumps.has(depName)) { const pendingBump = pendingVersionBumps.get(depName); console.log( `Replacing ${depName} ${currentRange} with ${pendingBump.targetRange} in ${depType} of ${packageJson.name}`, ); deps[depName] = pendingBump.targetRange; hasChanges = true; } } } if (hasChanges) { await fs.writeJson(path.resolve(dir, 'package.json'), packageJson, { spaces: 2, encoding: 'utf8', }); } } await fs.writeJSON( patchedJsonPath, { currentReleaseVersion: {} }, { spaces: 2, encoding: 'utf8' }, ); } /** * Returns the mode and tag that is currently set * in the .changeset/pre.json file */ async function getPreInfo(rootPath) { const pre = path.join(rootPath, '.changeset', 'pre.json'); if (!(await fs.pathExists(pre))) { return { mode: undefined, tag: undefined }; } const { mode, tag } = await fs.readJson(pre); return { mode, tag }; } /** * Bumps the release version in the root package.json. * * This takes into account whether we're in pre-release mode or on a patch branch. */ async function updateBackstageReleaseVersion() { const rootPath = path.resolve(__dirname, '..'); const branchName = await execFile( 'git', ['rev-parse', '--abbrev-ref', 'HEAD'], { shell: true }, ).then(({ stdout }) => stdout.trim()); const { mode: preMode, tag: preTag } = await getPreInfo(rootPath); const packagePath = path.join(rootPath, 'package.json'); const package = await fs.readJson(packagePath); const { version: currentVersion } = package; let nextVersion; if (MAIN_BRANCHES.includes(branchName)) { if (preMode === 'pre') { if (semver.prerelease(currentVersion)) { nextVersion = semver.inc(currentVersion, 'pre', preTag); } else { nextVersion = semver.inc(currentVersion, 'preminor', preTag); } } else if (preMode === 'exit') { nextVersion = semver.inc(currentVersion, 'patch'); } else { nextVersion = semver.inc(currentVersion, 'minor'); } } else { if (preMode) { throw new Error(`Unexpected pre mode ${preMode} on branch ${branchName}`); } nextVersion = semver.inc(currentVersion, 'patch'); } await fs.writeJson( packagePath, { ...package, version: nextVersion, }, { spaces: 2, encoding: 'utf8' }, ); } async function main() { await updatePatchVersions(); await updateBackstageReleaseVersion(); } main().catch(error => { console.error(error.stack); process.exit(1); });