From da852bdec31c0ef65dd668bdcdee966cb5e8d424 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 18 Dec 2020 20:19:17 +0100 Subject: [PATCH] release: Add a script to create releases on GitHub --- .github/workflows/create-github-release.yml | 25 ++- scripts/create-github-release.js | 184 ++++++++++++++++++++ 2 files changed, 200 insertions(+), 9 deletions(-) create mode 100644 scripts/create-github-release.js diff --git a/.github/workflows/create-github-release.yml b/.github/workflows/create-github-release.yml index e3c6a4e80b..e3e71b7626 100644 --- a/.github/workflows/create-github-release.yml +++ b/.github/workflows/create-github-release.yml @@ -13,13 +13,20 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - - name: Create release on GitHub - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + + - name: use node.js 12.x + uses: actions/setup-node@v1 with: - tag_name: ${{ github.ref }} - release_name: ${{ github.ref }} - body: 'Changelog https://github.com/backstage/backstage/blob/master/CHANGELOG.md' - draft: false - prerelease: false + node-version: '12.x' + + - name: Install node dependencies + run: yarn install @octokit/rest + + # GITHUB_REF is of the format refs/tags/vA.B.C + # This step extracts vA.B.C from GITHUB_REF + - name: Get the version + id: get_version + run: echo "::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}" + + - name: Create release on GitHub + run: node scripts/create-github-release.js ${{ steps.get_version.outputs.TAG_NAME }} ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} 1 diff --git a/scripts/create-github-release.js b/scripts/create-github-release.js new file mode 100644 index 0000000000..35101056da --- /dev/null +++ b/scripts/create-github-release.js @@ -0,0 +1,184 @@ +#!/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: + * + * (Dry Run mode, will not create release) + * $ 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 1 + * + * This will do the same steps as above, and will create the Release with the description. + */ + +const { Octokit } = require('@octokit/rest'); + +// See Examples above to learn about these command line arguments. +const [TAG_NAME, GITHUB_TOKEN, BOOL_CREATE_RELEASE] = process.argv.slice(2); + +if (!BOOL_CREATE_RELEASE) { + console.log( + '\nRunning script in Dry Run mode. It will output details, but will not create any Release.', + ); +} + +const GH_OWNER = 'backstage'; +const GH_REPO = 'backstage'; +const EXPECTED_COMMIT_MESSAGE = /^Merge pull request #(?[0-9]+) from backstage\/changeset-release\/master\n\nVersion Packages$/; + +// Initialize a GitHub client +const octokit = new Octokit({ + auth: 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 the release + const releaseResponse = await octokit.repos.createRelease({ + owner: GH_REPO, + repo: GH_REPO, + tag_name: TAG_NAME, + name: TAG_NAME, + body: releaseDescription, + draft: false, + prerelease: false, + }); + + if (releaseResponse.status === 201) { + console.log('Created 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); + + if (BOOL_CREATE_RELEASE) { + await createRelease(releaseDescription); + } else { + console.log('Skipping release since script running in Dry Run mode.'); + } +} + +main().catch(error => { + console.error(error.stack); + process.exit(1); +});