-
Notifications
You must be signed in to change notification settings - Fork 6.1k
/
create-github-release.js
executable file
·208 lines (185 loc) · 6.7 KB
/
create-github-release.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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/usr/bin/env node
/* eslint-disable @backstage/no-undeclared-imports */
/*
* Copyright 2020 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.
*/
/**
* This script creates a release on GitHub for the Backstage repository.
* Given a git tag, it identifies the PR created by changesets which is responsible for creating
* the git tag. It then uses the PR description consisting of changelogs for packages as the
* release description.
*
* Example:
*
* Set GITHUB_TOKEN environment variable.
*
* (Dry Run mode, will create a DRAFT release, but will not publish it.)
* (Draft releases are visible to maintainers and do not notify users.)
* $ node scripts/get-release-description v0.4.1
*
* This will open the git tree at this tag https://github.com/backstage/backstage/tree/v0.4.1
* It will identify https://github.com/backstage/backstage/pull/3668 as the responsible changeset PR.
* And will use everything in the PR description under "Releases" section.
*
* (Production or GitHub Actions Mode)
* $ node scripts/get-release-description v0.4.1 true
*
* This will do the same steps as above, and will publish the Release with the description.
*/
const { Octokit } = require('@octokit/rest');
const semver = require('semver');
// See Examples above to learn about these command line arguments.
const [TAG_NAME, BOOL_CREATE_RELEASE] = process.argv.slice(2);
if (!BOOL_CREATE_RELEASE) {
console.log(
'\nRunning script in Dry Run mode. It will output details, will create a draft release but will NOT publish it.',
);
}
const GH_OWNER = 'backstage';
const GH_REPO = 'backstage';
const EXPECTED_COMMIT_MESSAGE = /^Merge pull request #(?<prNumber>[0-9]+) from/;
const CHANGESET_RELEASE_BRANCH = 'backstage/changeset-release/master';
// Initialize a GitHub client
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
// Get the message of the commit responsible for a tag
async function getCommitUsingTagName(tagName) {
// Get the tag SHA using the provided tag name
const refData = await octokit.git.getRef({
owner: GH_OWNER,
repo: GH_REPO,
ref: `tags/${tagName}`,
});
if (refData.status !== 200) {
console.error('refData:');
console.error(refData);
throw new Error(
'Something went wrong when getting the tag SHA using tag name',
);
}
const tagSha = refData.data.object.sha;
console.log(`SHA for the tag ${TAG_NAME} is ${tagSha}`);
// Get the commit SHA using the tag SHA
const tagData = await octokit.git.getTag({
owner: GH_REPO,
repo: GH_REPO,
tag_sha: tagSha,
});
if (tagData.status !== 200) {
console.error('tagData:');
console.error(tagData);
throw new Error(
'Something went wrong when getting the commit SHA using tag SHA',
);
}
const commitSha = tagData.data.object.sha;
console.log(
`The commit for the tag is https://github.com/backstage/backstage/commit/${commitSha}`,
);
// Get the commit message using the commit SHA
const commitData = await octokit.git.getCommit({
owner: GH_OWNER,
repo: GH_REPO,
commit_sha: commitSha,
});
if (commitData.status !== 200) {
console.error('commitData:');
console.error(commitData);
throw new Error(
'Something went wrong when getting the commit message using commit SHA',
);
}
// Example Commit Message
// Merge pull request #3555 from backstage/changeset-release/master Version Packages
return { sha: commitSha, message: commitData.data.message };
}
// There is a PR number in our expected commit message. Get the description of that PR.
async function getReleaseDescriptionFromCommit(commit) {
let pullRequestBody = undefined;
const { data: pullRequests } =
await octokit.repos.listPullRequestsAssociatedWithCommit({
owner: GH_OWNER,
repo: GH_REPO,
commit_sha: commit.sha,
});
if (pullRequests.length === 1) {
pullRequestBody = pullRequests[0].body;
} else {
console.warn(
`Found ${pullRequests.length} pull requests for commit ${commit.sha}, falling back to parsing commit message`,
);
// It should exactly match the pattern of changeset commit message, or else will abort.
const expectedMessage = RegExp(EXPECTED_COMMIT_MESSAGE);
if (!expectedMessage.test(commit.message)) {
throw new Error(
`Expected regex did not match commit message: ${commit.message}`,
);
}
// Get the PR description from the commit message
const prNumber = commit.message.match(expectedMessage).groups.prNumber;
console.log(
`Identified the changeset Pull request - https://github.com/backstage/backstage/pull/${prNumber}`,
);
const { data } = await octokit.pulls.get({
owner: GH_OWNER,
repo: GH_REPO,
pull_number: prNumber,
});
pullRequestBody = data.body;
}
// Use the PR description to prepare for the release description
const isChangesetRelease = commit.message.includes(CHANGESET_RELEASE_BRANCH);
if (isChangesetRelease) {
const lines = pullRequestBody.split('\n');
return lines.slice(lines.indexOf('# Releases') + 1).join('\n');
}
return pullRequestBody;
}
// Create Release on GitHub.
async function createRelease(releaseDescription) {
// Create draft release if BOOL_CREATE_RELEASE is undefined
// Publish release if BOOL_CREATE_RELEASE is not undefined
const boolCreateDraft = !BOOL_CREATE_RELEASE;
const releaseResponse = await octokit.repos.createRelease({
owner: GH_REPO,
repo: GH_REPO,
tag_name: TAG_NAME,
name: TAG_NAME,
body: releaseDescription,
draft: boolCreateDraft,
prerelease: Boolean(semver.prerelease(TAG_NAME)),
});
if (releaseResponse.status === 201) {
if (boolCreateDraft) {
console.log('Created draft release! Click Publish to notify users.');
} else {
console.log('Published release!');
}
console.log(releaseResponse.data.html_url);
} else {
console.error(releaseResponse);
throw new Error('Something went wrong when creating the release.');
}
}
async function main() {
const commit = await getCommitUsingTagName(TAG_NAME);
const releaseDescription = await getReleaseDescriptionFromCommit(commit);
await createRelease(releaseDescription);
}
main().catch(error => {
console.error(error.stack);
process.exit(1);
});