-
Notifications
You must be signed in to change notification settings - Fork 6.1k
/
patch-release-for-pr.js
executable file
·185 lines (156 loc) · 4.75 KB
/
patch-release-for-pr.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#!/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);
});