Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
backstage
GitHub Repository: backstage/backstage
Path: blob/master/scripts/create-github-release.js
15270 views
1
#!/usr/bin/env node
2
/* eslint-disable @backstage/no-undeclared-imports */
3
/*
4
* Copyright 2020 The Backstage Authors
5
*
6
* Licensed under the Apache License, Version 2.0 (the "License");
7
* you may not use this file except in compliance with the License.
8
* You may obtain a copy of the License at
9
*
10
* http://www.apache.org/licenses/LICENSE-2.0
11
*
12
* Unless required by applicable law or agreed to in writing, software
13
* distributed under the License is distributed on an "AS IS" BASIS,
14
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
* See the License for the specific language governing permissions and
16
* limitations under the License.
17
*/
18
19
/**
20
* This script creates a release on GitHub for the Backstage repository.
21
* Given a git tag, it identifies the PR created by changesets which is responsible for creating
22
* the git tag. It then uses the PR description consisting of changelogs for packages as the
23
* release description.
24
*
25
* Example:
26
*
27
* Set GITHUB_TOKEN environment variable.
28
*
29
* (Dry Run mode, will create a DRAFT release, but will not publish it.)
30
* (Draft releases are visible to maintainers and do not notify users.)
31
* $ node scripts/get-release-description v0.4.1
32
*
33
* This will open the git tree at this tag https://github.com/backstage/backstage/tree/v0.4.1
34
* It will identify https://github.com/backstage/backstage/pull/3668 as the responsible changeset PR.
35
* And will use everything in the PR description under "Releases" section.
36
*
37
* (Production or GitHub Actions Mode)
38
* $ node scripts/get-release-description v0.4.1 true
39
*
40
* This will do the same steps as above, and will publish the Release with the description.
41
*/
42
43
const { Octokit } = require('@octokit/rest');
44
const semver = require('semver');
45
46
// See Examples above to learn about these command line arguments.
47
const [TAG_NAME, BOOL_CREATE_RELEASE] = process.argv.slice(2);
48
49
if (!BOOL_CREATE_RELEASE) {
50
console.log(
51
'\nRunning script in Dry Run mode. It will output details, will create a draft release but will NOT publish it.',
52
);
53
}
54
55
const GH_OWNER = 'backstage';
56
const GH_REPO = 'backstage';
57
const EXPECTED_COMMIT_MESSAGE = /^Merge pull request #(?<prNumber>[0-9]+) from/;
58
const CHANGESET_RELEASE_BRANCH = 'backstage/changeset-release/master';
59
60
// Initialize a GitHub client
61
const octokit = new Octokit({
62
auth: process.env.GITHUB_TOKEN,
63
});
64
65
// Get the message of the commit responsible for a tag
66
async function getCommitUsingTagName(tagName) {
67
// Get the tag SHA using the provided tag name
68
const refData = await octokit.git.getRef({
69
owner: GH_OWNER,
70
repo: GH_REPO,
71
ref: `tags/${tagName}`,
72
});
73
if (refData.status !== 200) {
74
console.error('refData:');
75
console.error(refData);
76
throw new Error(
77
'Something went wrong when getting the tag SHA using tag name',
78
);
79
}
80
const tagSha = refData.data.object.sha;
81
console.log(`SHA for the tag ${TAG_NAME} is ${tagSha}`);
82
83
// Get the commit SHA using the tag SHA
84
const tagData = await octokit.git.getTag({
85
owner: GH_REPO,
86
repo: GH_REPO,
87
tag_sha: tagSha,
88
});
89
if (tagData.status !== 200) {
90
console.error('tagData:');
91
console.error(tagData);
92
throw new Error(
93
'Something went wrong when getting the commit SHA using tag SHA',
94
);
95
}
96
const commitSha = tagData.data.object.sha;
97
console.log(
98
`The commit for the tag is https://github.com/backstage/backstage/commit/${commitSha}`,
99
);
100
101
// Get the commit message using the commit SHA
102
const commitData = await octokit.git.getCommit({
103
owner: GH_OWNER,
104
repo: GH_REPO,
105
commit_sha: commitSha,
106
});
107
if (commitData.status !== 200) {
108
console.error('commitData:');
109
console.error(commitData);
110
throw new Error(
111
'Something went wrong when getting the commit message using commit SHA',
112
);
113
}
114
115
// Example Commit Message
116
// Merge pull request #3555 from backstage/changeset-release/master Version Packages
117
return { sha: commitSha, message: commitData.data.message };
118
}
119
120
// There is a PR number in our expected commit message. Get the description of that PR.
121
async function getReleaseDescriptionFromCommit(commit) {
122
let pullRequestBody = undefined;
123
124
const { data: pullRequests } =
125
await octokit.repos.listPullRequestsAssociatedWithCommit({
126
owner: GH_OWNER,
127
repo: GH_REPO,
128
commit_sha: commit.sha,
129
});
130
if (pullRequests.length === 1) {
131
pullRequestBody = pullRequests[0].body;
132
} else {
133
console.warn(
134
`Found ${pullRequests.length} pull requests for commit ${commit.sha}, falling back to parsing commit message`,
135
);
136
137
// It should exactly match the pattern of changeset commit message, or else will abort.
138
const expectedMessage = RegExp(EXPECTED_COMMIT_MESSAGE);
139
if (!expectedMessage.test(commit.message)) {
140
throw new Error(
141
`Expected regex did not match commit message: ${commit.message}`,
142
);
143
}
144
145
// Get the PR description from the commit message
146
const prNumber = commit.message.match(expectedMessage).groups.prNumber;
147
console.log(
148
`Identified the changeset Pull request - https://github.com/backstage/backstage/pull/${prNumber}`,
149
);
150
151
const { data } = await octokit.pulls.get({
152
owner: GH_OWNER,
153
repo: GH_REPO,
154
pull_number: prNumber,
155
});
156
157
pullRequestBody = data.body;
158
}
159
160
// Use the PR description to prepare for the release description
161
const isChangesetRelease = commit.message.includes(CHANGESET_RELEASE_BRANCH);
162
if (isChangesetRelease) {
163
const lines = pullRequestBody.split('\n');
164
return lines.slice(lines.indexOf('# Releases') + 1).join('\n');
165
}
166
167
return pullRequestBody;
168
}
169
170
// Create Release on GitHub.
171
async function createRelease(releaseDescription) {
172
// Create draft release if BOOL_CREATE_RELEASE is undefined
173
// Publish release if BOOL_CREATE_RELEASE is not undefined
174
const boolCreateDraft = !BOOL_CREATE_RELEASE;
175
176
const releaseResponse = await octokit.repos.createRelease({
177
owner: GH_REPO,
178
repo: GH_REPO,
179
tag_name: TAG_NAME,
180
name: TAG_NAME,
181
body: releaseDescription,
182
draft: boolCreateDraft,
183
prerelease: Boolean(semver.prerelease(TAG_NAME)),
184
});
185
186
if (releaseResponse.status === 201) {
187
if (boolCreateDraft) {
188
console.log('Created draft release! Click Publish to notify users.');
189
} else {
190
console.log('Published release!');
191
}
192
console.log(releaseResponse.data.html_url);
193
} else {
194
console.error(releaseResponse);
195
throw new Error('Something went wrong when creating the release.');
196
}
197
}
198
199
async function main() {
200
const commit = await getCommitUsingTagName(TAG_NAME);
201
const releaseDescription = await getReleaseDescriptionFromCommit(commit);
202
await createRelease(releaseDescription);
203
}
204
205
main().catch(error => {
206
console.error(error.stack);
207
process.exit(1);
208
});
209
210