From c15ccbff5182df181c691c0f3ebf4eccea0f07b1 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Thu, 3 Dec 2020 09:12:14 +0100 Subject: [PATCH 1/5] Create new GitHub release when a new tag is created --- .github/workflows/create-github-release.yml | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/create-github-release.yml diff --git a/.github/workflows/create-github-release.yml b/.github/workflows/create-github-release.yml new file mode 100644 index 0000000000..e3c6a4e80b --- /dev/null +++ b/.github/workflows/create-github-release.yml @@ -0,0 +1,25 @@ +# New tags and releases are created by changeset https://github.com/atlassian/changesets +name: Create a new release on GitHub when a new tag is created + +on: + push: + tags: + - 'v*' # Push events to matching v*, i.e. v0.4.0, v1.1.0 + +jobs: + build: + name: Create a new release on GitHub when a new tag is created + runs-on: ubuntu-latest + 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 }} + with: + tag_name: ${{ github.ref }} + release_name: ${{ github.ref }} + body: 'Changelog https://github.com/backstage/backstage/blob/master/CHANGELOG.md' + draft: false + prerelease: false From da852bdec31c0ef65dd668bdcdee966cb5e8d424 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 18 Dec 2020 20:19:17 +0100 Subject: [PATCH 2/5] 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); +}); From f7a79effaa78124e859bca39030a1883ba698afc Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Fri, 18 Dec 2020 21:08:04 +0100 Subject: [PATCH 3/5] release: Use draft releases to test the create-github-release script --- .github/workflows/create-github-release.yml | 6 +++-- scripts/create-github-release.js | 26 ++++++++++++--------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/.github/workflows/create-github-release.yml b/.github/workflows/create-github-release.yml index e3e71b7626..630d1a112a 100644 --- a/.github/workflows/create-github-release.yml +++ b/.github/workflows/create-github-release.yml @@ -20,7 +20,7 @@ jobs: node-version: '12.x' - name: Install node dependencies - run: yarn install @octokit/rest + run: npm install @octokit/rest # GITHUB_REF is of the format refs/tags/vA.B.C # This step extracts vA.B.C from GITHUB_REF @@ -28,5 +28,7 @@ jobs: id: get_version run: echo "::set-output name=TAG_NAME::${GITHUB_REF#refs/tags/}" + # TODO/Note: This will only create a Draft release, which the maintainer can see and publish. + # If the Draft release looks good, modify the step to go ahead publish the release. (By adding the third CLI argument.) - name: Create release on GitHub - run: node scripts/create-github-release.js ${{ steps.get_version.outputs.TAG_NAME }} ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} 1 + run: node scripts/create-github-release.js ${{ steps.get_version.outputs.TAG_NAME }} ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} diff --git a/scripts/create-github-release.js b/scripts/create-github-release.js index 35101056da..dd0deb1f2f 100644 --- a/scripts/create-github-release.js +++ b/scripts/create-github-release.js @@ -24,7 +24,8 @@ * * Example: * - * (Dry Run mode, will not create release) + * (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 @@ -34,7 +35,7 @@ * (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. + * This will do the same steps as above, and will publish the Release with the description. */ const { Octokit } = require('@octokit/rest'); @@ -44,7 +45,7 @@ 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.', + '\nRunning script in Dry Run mode. It will output details, wil create a draft release but will NOT publish it.', ); } @@ -146,19 +147,26 @@ async function prepareReleaseDescription(prDescription) { // Create Release on GitHub. async function createRelease(releaseDescription) { - // Create the release + // 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: false, + draft: boolCreateDraft, prerelease: false, }); if (releaseResponse.status === 201) { - console.log('Created release!'); + 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); @@ -171,11 +179,7 @@ async function main() { 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.'); - } + await createRelease(releaseDescription); } main().catch(error => { From 55fb92acc7e754d345e45a96cc68bdf8c6d82918 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Tue, 22 Dec 2020 10:58:35 +0100 Subject: [PATCH 4/5] Update scripts/create-github-release.js Co-authored-by: Patrik Oldsberg --- scripts/create-github-release.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/create-github-release.js b/scripts/create-github-release.js index dd0deb1f2f..4dfe8f9dc3 100644 --- a/scripts/create-github-release.js +++ b/scripts/create-github-release.js @@ -45,7 +45,7 @@ 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, wil create a draft release but will NOT publish it.', + '\nRunning script in Dry Run mode. It will output details, will create a draft release but will NOT publish it.', ); } From 086ab2047732a1fa16e53e52799e8a49ace33e55 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Tue, 22 Dec 2020 11:07:30 +0100 Subject: [PATCH 5/5] create-release-script: Use env variable for github token --- .github/workflows/create-github-release.yml | 4 +++- scripts/create-github-release.js | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/create-github-release.yml b/.github/workflows/create-github-release.yml index 630d1a112a..8ef3f10f6a 100644 --- a/.github/workflows/create-github-release.yml +++ b/.github/workflows/create-github-release.yml @@ -31,4 +31,6 @@ jobs: # TODO/Note: This will only create a Draft release, which the maintainer can see and publish. # If the Draft release looks good, modify the step to go ahead publish the release. (By adding the third CLI argument.) - name: Create release on GitHub - run: node scripts/create-github-release.js ${{ steps.get_version.outputs.TAG_NAME }} ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} + run: node scripts/create-github-release.js ${{ steps.get_version.outputs.TAG_NAME }} + env: + GITHUB_TOKEN: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} diff --git a/scripts/create-github-release.js b/scripts/create-github-release.js index 4dfe8f9dc3..a9bfb9f5b3 100644 --- a/scripts/create-github-release.js +++ b/scripts/create-github-release.js @@ -24,16 +24,18 @@ * * 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 + * $ 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 + * $ 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. */ @@ -41,7 +43,7 @@ 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); +const [TAG_NAME, BOOL_CREATE_RELEASE] = process.argv.slice(2); if (!BOOL_CREATE_RELEASE) { console.log( @@ -55,7 +57,7 @@ const EXPECTED_COMMIT_MESSAGE = /^Merge pull request #(?[0-9]+) from b // Initialize a GitHub client const octokit = new Octokit({ - auth: GITHUB_TOKEN, + auth: process.env.GITHUB_TOKEN, }); // Get the message of the commit responsible for a tag