Files
backstage/scripts/create-github-release.js
T
2020-12-22 11:07:30 +01:00

191 lines
6.2 KiB
JavaScript

#!/usr/bin/env node
/* eslint-disable import/no-extraneous-dependencies */
/*
* Copyright 2020 Spotify AB
*
* 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');
// 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 backstage\/changeset-release\/master\n\nVersion Packages$/;
// 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 getCommitMessageUsingTagName(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 commitData.data.message;
}
// There is a PR number in our expected commit message. Get the description of that PR.
async function getPrDescriptionFromCommitMessage(commitMessage) {
// It should exactly match the pattern of changeset commit message, or else will abort.
const expectedMessage = RegExp(EXPECTED_COMMIT_MESSAGE);
if (!expectedMessage.test(commitMessage)) {
throw new Error(
`Expected regex did not match commit message: ${commitMessage}`,
);
}
// Get the PR description from the commit message
const prNumber = commitMessage.match(expectedMessage).groups.prNumber;
console.log(
`Identified the changeset Pull request - https://github.com/backstage/backstage/pull/${prNumber}`,
);
const prData = await octokit.pulls.get({
owner: GH_OWNER,
repo: GH_REPO,
pull_number: prNumber,
});
return prData.data.body;
}
// Use the PR description to prepare for the release description
async function prepareReleaseDescription(prDescription) {
// TODO: Refine prDescription to remove the lines containing "Update Dependencies"
// Remove everything in the beginning until changelogs.
return prDescription.split('\n').slice(3).join('\n');
}
// 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: false,
});
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 commitMessage = await getCommitMessageUsingTagName(TAG_NAME);
const prDescription = await getPrDescriptionFromCommitMessage(commitMessage);
const releaseDescription = await prepareReleaseDescription(prDescription);
await createRelease(releaseDescription);
}
main().catch(error => {
console.error(error.stack);
process.exit(1);
});