Merge pull request #9838 from backstage/rugvip/auto-bump
scripts/prepare-release: update to detect patch versions from patch branches
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+137
-37
@@ -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 WITH a 'v' prefix
|
||||
// For example, `patch/v1.2.0`
|
||||
const PATCH_BRANCH_PREFIX = 'patch/';
|
||||
|
||||
const DEPENDENCY_TYPES = [
|
||||
'dependencies',
|
||||
'devDependencies',
|
||||
@@ -35,25 +39,84 @@ const DEPENDENCY_TYPES = [
|
||||
'peerDependencies',
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns the most recent release version on the main branch that is not a pre-release.
|
||||
*/
|
||||
async function getPreviousReleaseVersion(repo) {
|
||||
// TODO(Rugvip): Figure out which field to sort by to avoid manual sort after
|
||||
const { stdout: tagsStr } = await execFile(
|
||||
'git',
|
||||
['tag', '--list', 'v*', '--merged=HEAD'],
|
||||
{ shell: true, cwd: repo.root.dir },
|
||||
);
|
||||
const tags = tagsStr.trim().split(/\r\n|\n/);
|
||||
const [latestTag] = semver.rsort(tags).filter(t => !semver.prerelease(t));
|
||||
return latestTag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <ref> 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',
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +144,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 +180,44 @@ 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 previousRelease = await getPreviousReleaseVersion(repo);
|
||||
console.log(`Found release version: ${previousRelease}`);
|
||||
|
||||
const patchRef = await findTipOfPatchBranch(repo, previousRelease);
|
||||
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 +226,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 +261,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 +269,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 +279,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 => {
|
||||
|
||||
Reference in New Issue
Block a user