Path: blob/master/scripts/create-github-release.js
15270 views
#!/usr/bin/env node1/* eslint-disable @backstage/no-undeclared-imports */2/*3* Copyright 2020 The Backstage Authors4*5* Licensed under the Apache License, Version 2.0 (the "License");6* you may not use this file except in compliance with the License.7* You may obtain a copy of the License at8*9* http://www.apache.org/licenses/LICENSE-2.010*11* Unless required by applicable law or agreed to in writing, software12* distributed under the License is distributed on an "AS IS" BASIS,13* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.14* See the License for the specific language governing permissions and15* limitations under the License.16*/1718/**19* This script creates a release on GitHub for the Backstage repository.20* Given a git tag, it identifies the PR created by changesets which is responsible for creating21* the git tag. It then uses the PR description consisting of changelogs for packages as the22* release description.23*24* Example:25*26* Set GITHUB_TOKEN environment variable.27*28* (Dry Run mode, will create a DRAFT release, but will not publish it.)29* (Draft releases are visible to maintainers and do not notify users.)30* $ node scripts/get-release-description v0.4.131*32* This will open the git tree at this tag https://github.com/backstage/backstage/tree/v0.4.133* It will identify https://github.com/backstage/backstage/pull/3668 as the responsible changeset PR.34* And will use everything in the PR description under "Releases" section.35*36* (Production or GitHub Actions Mode)37* $ node scripts/get-release-description v0.4.1 true38*39* This will do the same steps as above, and will publish the Release with the description.40*/4142const { Octokit } = require('@octokit/rest');43const semver = require('semver');4445// See Examples above to learn about these command line arguments.46const [TAG_NAME, BOOL_CREATE_RELEASE] = process.argv.slice(2);4748if (!BOOL_CREATE_RELEASE) {49console.log(50'\nRunning script in Dry Run mode. It will output details, will create a draft release but will NOT publish it.',51);52}5354const GH_OWNER = 'backstage';55const GH_REPO = 'backstage';56const EXPECTED_COMMIT_MESSAGE = /^Merge pull request #(?<prNumber>[0-9]+) from/;57const CHANGESET_RELEASE_BRANCH = 'backstage/changeset-release/master';5859// Initialize a GitHub client60const octokit = new Octokit({61auth: process.env.GITHUB_TOKEN,62});6364// Get the message of the commit responsible for a tag65async function getCommitUsingTagName(tagName) {66// Get the tag SHA using the provided tag name67const refData = await octokit.git.getRef({68owner: GH_OWNER,69repo: GH_REPO,70ref: `tags/${tagName}`,71});72if (refData.status !== 200) {73console.error('refData:');74console.error(refData);75throw new Error(76'Something went wrong when getting the tag SHA using tag name',77);78}79const tagSha = refData.data.object.sha;80console.log(`SHA for the tag ${TAG_NAME} is ${tagSha}`);8182// Get the commit SHA using the tag SHA83const tagData = await octokit.git.getTag({84owner: GH_REPO,85repo: GH_REPO,86tag_sha: tagSha,87});88if (tagData.status !== 200) {89console.error('tagData:');90console.error(tagData);91throw new Error(92'Something went wrong when getting the commit SHA using tag SHA',93);94}95const commitSha = tagData.data.object.sha;96console.log(97`The commit for the tag is https://github.com/backstage/backstage/commit/${commitSha}`,98);99100// Get the commit message using the commit SHA101const commitData = await octokit.git.getCommit({102owner: GH_OWNER,103repo: GH_REPO,104commit_sha: commitSha,105});106if (commitData.status !== 200) {107console.error('commitData:');108console.error(commitData);109throw new Error(110'Something went wrong when getting the commit message using commit SHA',111);112}113114// Example Commit Message115// Merge pull request #3555 from backstage/changeset-release/master Version Packages116return { sha: commitSha, message: commitData.data.message };117}118119// There is a PR number in our expected commit message. Get the description of that PR.120async function getReleaseDescriptionFromCommit(commit) {121let pullRequestBody = undefined;122123const { data: pullRequests } =124await octokit.repos.listPullRequestsAssociatedWithCommit({125owner: GH_OWNER,126repo: GH_REPO,127commit_sha: commit.sha,128});129if (pullRequests.length === 1) {130pullRequestBody = pullRequests[0].body;131} else {132console.warn(133`Found ${pullRequests.length} pull requests for commit ${commit.sha}, falling back to parsing commit message`,134);135136// It should exactly match the pattern of changeset commit message, or else will abort.137const expectedMessage = RegExp(EXPECTED_COMMIT_MESSAGE);138if (!expectedMessage.test(commit.message)) {139throw new Error(140`Expected regex did not match commit message: ${commit.message}`,141);142}143144// Get the PR description from the commit message145const prNumber = commit.message.match(expectedMessage).groups.prNumber;146console.log(147`Identified the changeset Pull request - https://github.com/backstage/backstage/pull/${prNumber}`,148);149150const { data } = await octokit.pulls.get({151owner: GH_OWNER,152repo: GH_REPO,153pull_number: prNumber,154});155156pullRequestBody = data.body;157}158159// Use the PR description to prepare for the release description160const isChangesetRelease = commit.message.includes(CHANGESET_RELEASE_BRANCH);161if (isChangesetRelease) {162const lines = pullRequestBody.split('\n');163return lines.slice(lines.indexOf('# Releases') + 1).join('\n');164}165166return pullRequestBody;167}168169// Create Release on GitHub.170async function createRelease(releaseDescription) {171// Create draft release if BOOL_CREATE_RELEASE is undefined172// Publish release if BOOL_CREATE_RELEASE is not undefined173const boolCreateDraft = !BOOL_CREATE_RELEASE;174175const releaseResponse = await octokit.repos.createRelease({176owner: GH_REPO,177repo: GH_REPO,178tag_name: TAG_NAME,179name: TAG_NAME,180body: releaseDescription,181draft: boolCreateDraft,182prerelease: Boolean(semver.prerelease(TAG_NAME)),183});184185if (releaseResponse.status === 201) {186if (boolCreateDraft) {187console.log('Created draft release! Click Publish to notify users.');188} else {189console.log('Published release!');190}191console.log(releaseResponse.data.html_url);192} else {193console.error(releaseResponse);194throw new Error('Something went wrong when creating the release.');195}196}197198async function main() {199const commit = await getCommitUsingTagName(TAG_NAME);200const releaseDescription = await getReleaseDescriptionFromCommit(commit);201await createRelease(releaseDescription);202}203204main().catch(error => {205console.error(error.stack);206process.exit(1);207});208209210