#!/usr/bin/env node /* * Copyright 2021 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 path = require('path'); const semver = require('semver'); const { Octokit } = require('@octokit/rest'); const { execFile: execFileCb } = require('child_process'); const { promisify } = require('util'); const execFile = promisify(execFileCb); const owner = 'backstage'; const repo = 'backstage'; const rootDir = path.resolve(__dirname, '..'); const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN, }); async function run(command, ...args) { const { stdout, stderr } = await execFile(command, args, { cwd: rootDir, }); if (stderr) { console.error(stderr); } return stdout.trim(); } /** * Finds the current stable release version of the repo, looking at * the current commit and backwards, finding the first commit were a * stable version is present. */ async function findCurrentReleaseVersion() { const rootPkgPath = path.resolve(rootDir, 'package.json'); const pkg = await fs.readJson(rootPkgPath); if (!semver.prerelease(pkg.version)) { return pkg.version; } const { stdout: revListStr } = await execFile('git', [ 'rev-list', 'HEAD', '--', 'package.json', ]); const revList = revListStr.trim().split(/\r?\n/); for (const rev of revList) { const { stdout: pkgJsonStr } = await execFile('git', [ 'show', `${rev}:package.json`, ]); if (pkgJsonStr) { const pkgJson = JSON.parse(pkgJsonStr); if (!semver.prerelease(pkgJson.version)) { return pkgJson.version; } } } throw new Error('No stable release found'); } async function main(args) { const prNumbers = args.map(s => { const num = parseInt(s, 10); if (!Number.isInteger(num)) { throw new Error(`Must provide valid PR number arguments, got ${s}`); } return num; }); console.log(`PR number(s): ${prNumbers.join(', ')}`); if (await run('git', 'status', '--porcelain')) { throw new Error('Cannot run with a dirty working tree'); } const release = await findCurrentReleaseVersion(); console.log(`Patching release ${release}`); await run('git', 'fetch'); const patchBranch = `patch/v${release}`; try { await run('git', 'checkout', `origin/${patchBranch}`); } catch { await run('git', 'checkout', '-b', patchBranch, `v${release}`); await run('git', 'push', 'origin', '-u', patchBranch); } // Create new branch, apply changes from all commits on PR branch, commit, push const branchName = `patch-release-pr-${prNumbers.join('-')}`; await run('git', 'checkout', '-b', branchName); for (const prNumber of prNumbers) { const { data } = await octokit.pulls.get({ owner, repo, pull_number: prNumber, }); const headSha = data.head.sha; if (!headSha) { throw new Error('head sha not available'); } const baseSha = data.base.sha; if (!baseSha) { throw new Error('base sha not available'); } const mergeBaseSha = await run('git', 'merge-base', headSha, baseSha); const logLines = await run( 'git', 'log', `${mergeBaseSha}...${headSha}`, '--reverse', '--pretty=%H', ); for (const logSha of logLines.split(/\r?\n/)) { await run('git', 'cherry-pick', '-n', logSha); } await run( 'git', 'commit', '--signoff', '--no-verify', '-m', `Patch from PR #${prNumber}`, ); } console.log('Running "yarn install" ...'); await run('yarn', 'install'); console.log('Running "yarn release" ...'); await run('yarn', 'release'); await run('git', 'add', '.'); await run( 'git', 'commit', '--signoff', '--no-verify', '-m', 'Generate Release', ); await run('git', 'push', 'origin', '-u', branchName); const params = new URLSearchParams({ expand: 1, body: 'This release fixes an issue where', title: `Patch release of ${prNumbers.map(nr => `#${nr}`).join(', ')}`, }); const url = `https://github.com/backstage/backstage/compare/${patchBranch}...${branchName}?${params}`; console.log(`Opening ${url} ...`); await run('open', url); } main(process.argv.slice(2)).catch(error => { console.error(error.stack || error); process.exit(1); });