diff --git a/.changeset/patched.json b/.changeset/patched.json deleted file mode 100644 index dd63e86169..0000000000 --- a/.changeset/patched.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "currentReleaseVersion": { - "@backstage/backend-common": "0.12.1", - "@backstage/catalog-model": "0.12.1", - "@backstage/cli": "0.15.1", - "@backstage/plugin-catalog-backend": "0.23.1", - "@backstage/plugin-catalog-common": "0.2.1", - "@backstage/plugin-catalog-react": "0.8.1" - } -} diff --git a/docs/publishing.md b/docs/publishing.md index fbbef93493..a0d67b7d24 100644 --- a/docs/publishing.md +++ b/docs/publishing.md @@ -67,12 +67,3 @@ process is used to release an emergency fix as version `6.5.1` in the patch rele - [ ] The fix, which you can likely cherry-pick from your patch branch: `git cherry-pick origin/patch/v1.18.0^` - [ ] An updated `CHANGELOG.md` of all patched packages from the tip of the patch branch, `git checkout origin/patch/v1.18.0 -- {packages,plugins}/*/CHANGELOG.md`. - [ ] A changeset with the message "Applied the fix from version `6.5.1` of this package, which is part of the `v1.18.1` release of Backstage." - - [ ] An entry in `.changeset/patched.json` that sets the current release version to `6.5.1`: - - ```json - { - "currentReleaseVersion": { - "@backstage/plugin-foo": "6.5.1" - } - } - ``` diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index be82e19344..bc0a58b966 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -28,6 +28,10 @@ const execFile = promisify(execFileCb); // All of these are considered to be main-line release branches const MAIN_BRANCHES = ['master', 'origin/master', 'changeset-release/master']; +// This prefix is used for patch branches, followed by the release version +// For example, `patch/v1.2.0` +const PATCH_BRANCH_PREFIX = 'patch/v'; + const DEPENDENCY_TYPES = [ 'dependencies', 'devDependencies', @@ -35,25 +39,79 @@ const DEPENDENCY_TYPES = [ 'peerDependencies', ]; +/** + * Finds the tip of the patch branch of a given release version. + * Returns undefined if no patch branch exists. + */ +async function findTipOfPatchBranch(repo, release) { + try { + await execFile('git', ['fetch', 'origin', PATCH_BRANCH_PREFIX + release], { + shell: true, + cwd: repo.root.dir, + }); + } catch (error) { + if (error.stderr?.match(/fatal: couldn't find remote ref/i)) { + return undefined; + } + throw error; + } + const { stdout: refStr } = await execFile('git', ['rev-parse', 'FETCH_HEAD']); + return refStr.trim(); +} + +/** + * Returns a map of packages to their versions for any package version + * in that does not match the current version in the working directory. + */ +async function detectPatchVersionsForRef(repo, ref) { + const patchVersions = new Map(); + + for (const pkg of repo.packages) { + const pkgJsonPath = path.join( + path.relative(repo.root.dir, pkg.dir), + 'package.json', + ); + try { + const { stdout: pkgJsonStr } = await execFile('git', [ + 'show', + `${ref}:${pkgJsonPath}`, + ]); + if (pkgJsonStr) { + const releasePkgJson = JSON.parse(pkgJsonStr); + const pkgJson = pkg.packageJson; + if (releasePkgJson.name !== pkgJson.name) { + throw new Error( + `Mismatched package name at ${pkg.dir}, ${releasePkgJson.name} !== ${pkgJson.name}`, + ); + } + if (releasePkgJson.version !== pkgJson.version) { + patchVersions.set(pkgJson.name, releasePkgJson.version); + } + } + } catch (error) { + if ( + error.stderr?.match(/^fatal: Path .* exists on disk, but not in .*$/m) + ) { + console.log(`Skipping new package ${pkg.packageJson.name}`); + continue; + } + throw error; + } + } + + return patchVersions; +} + /** * 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('.')); - +async function applyPatchVersions(repo, patchVersions) { const pendingVersionBumps = new Map(); - for (const [name, version] of Object.entries(currentReleaseVersion)) { - const pkg = packages.find(p => p.packageJson.name === name); + for (const [name, version] of patchVersions) { + const pkg = repo.packages.find(p => p.packageJson.name === name); if (!pkg) { throw new Error(`Package ${name} not found`); } @@ -81,7 +139,7 @@ async function updatePatchVersions() { }); } - for (const { dir, packageJson } of packages) { + for (const { dir, packageJson } of [repo.root, ...repo.packages]) { let hasChanges = false; if (pendingVersionBumps.has(packageJson.name)) { @@ -117,20 +175,45 @@ async function updatePatchVersions() { }); } } +} - await fs.writeJSON( - patchedJsonPath, - { currentReleaseVersion: {} }, - { spaces: 2, encoding: 'utf8' }, - ); +/** + * Detects any patched packages version since the most recent release on + * the main branch, and then bumps all packages in the repo accordingly. + */ +async function updatePackageVersions(repo) { + const rootPkgPath = path.resolve(repo.root.dir, 'package.json'); + const { version: currentRelease } = await fs.readJson(rootPkgPath); + console.log(`Current release version: ${currentRelease}`); + + const patchRef = await findTipOfPatchBranch(repo, currentRelease); + if (patchRef) { + console.log(`Tip of the patch branch: ${patchRef}`); + + const patchVersions = await detectPatchVersionsForRef(repo, patchRef); + if (patchVersions.size > 0) { + console.log( + `Found ${patchVersions.size} packages that were patched since the last release`, + ); + for (const [name, version] of patchVersions) { + console.log(` ${name}: ${version}`); + } + + await applyPatchVersions(repo, patchVersions); + } else { + console.log('No packages were patched since the last release'); + } + } else { + console.log('No patch branch found'); + } } /** * 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'); +async function getPreInfo(repo) { + const pre = path.join(repo.root.dir, '.changeset', 'pre.json'); if (!(await fs.pathExists(pre))) { return { mode: undefined, tag: undefined }; } @@ -139,26 +222,30 @@ async function getPreInfo(rootPath) { return { mode, tag }; } +/** + * Returns the name of the current git branch + */ +async function getCurrentBranch(repo) { + const { stdout } = await execFile( + 'git', + ['rev-parse', '--abbrev-ref', 'HEAD'], + { cwd: repo.root.dir, shell: true }, + ); + return stdout.trim(); +} + /** * 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); +async function updateBackstageReleaseVersion(repo, type) { + const { mode: preMode, tag: preTag } = await getPreInfo(repo); - const packagePath = path.join(rootPath, 'package.json'); - const package = await fs.readJson(packagePath); - const { version: currentVersion } = package; + const { version: currentVersion } = repo.root.packageJson; let nextVersion; - if (MAIN_BRANCHES.includes(branchName)) { + if (type === 'minor') { if (preMode === 'pre') { if (semver.prerelease(currentVersion)) { nextVersion = semver.inc(currentVersion, 'pre', preTag); @@ -170,7 +257,7 @@ async function updateBackstageReleaseVersion() { } else { nextVersion = semver.inc(currentVersion, 'minor'); } - } else { + } else if (type === 'patch') { if (preMode) { throw new Error(`Unexpected pre mode ${preMode} on branch ${branchName}`); } @@ -178,9 +265,9 @@ async function updateBackstageReleaseVersion() { } await fs.writeJson( - packagePath, + path.join(repo.root.dir, 'package.json'), { - ...package, + ...repo.root.packageJson, version: nextVersion, }, { spaces: 2, encoding: 'utf8' }, @@ -188,8 +275,17 @@ async function updateBackstageReleaseVersion() { } async function main() { - await updatePatchVersions(); - await updateBackstageReleaseVersion(); + const repo = await getPackages(__dirname); + const branchName = await getCurrentBranch(repo); + const isMainBranch = MAIN_BRANCHES.includes(branchName); + + console.log(`Current branch: ${branchName}`); + if (isMainBranch) { + console.log('Main release, updating package versions'); + await updatePackageVersions(repo); + } + + await updateBackstageReleaseVersion(repo, isMainBranch ? 'minor' : 'patch'); } main().catch(error => {