Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ci: Automatically notify issues after release #13808

Merged
merged 16 commits into from
Sep 26, 2024
34 changes: 34 additions & 0 deletions .github/workflows/release-comment-issues.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: "Automation: Notify issues for release"
on:
release:
types:
- published
workflow_dispatch:
inputs:
version:
description: Which version to notify issues for
required: false

# This workflow is triggered when a release is published
jobs:
release-comment-issues:
runs-on: ubuntu-20.04
name: 'Notify issues'

steps:
- name: Check out code
uses: actions/checkout@v4

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Get version
id: get_version
run: echo "version=${{ github.event.inputs.version || github.event.release.tag_name }}" >> $GITHUB_OUTPUT

- name: Comment on linked issues that are mentioned in release
if: steps.get_version.outputs.version != ''
uses: ./dev-packages/release-comment-issues-gh-action
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
version: ${{ steps.get_version.outputs.version }}
14 changes: 14 additions & 0 deletions dev-packages/release-comment-issues-gh-action/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
extends: ['../../.eslintrc.js'],
parserOptions: {
sourceType: 'module',
ecmaVersion: 'latest',
},

overrides: [
{
files: ['*.mjs'],
extends: ['@sentry-internal/sdk/src/base'],
},
],
};
12 changes: 12 additions & 0 deletions dev-packages/release-comment-issues-gh-action/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
name: 'release-comment-issues-gh-action'
description: 'An internal Github Action to comment on related issues when a release is published.'
inputs:
github_token:
required: true
description: 'a github access token'
version:
required: true
description: 'Which version was released'
runs:
using: 'node20'
main: 'index.mjs'
137 changes: 137 additions & 0 deletions dev-packages/release-comment-issues-gh-action/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import * as core from '@actions/core';
import { context, getOctokit } from '@actions/github';

const RELEASE_COMMENT_HEADING = '## A PR closing this issue has just been released 🚀';

async function run() {
const { getInput } = core;

const githubToken = getInput('github_token');
const version = getInput('version');

if (!githubToken || !version) {
core.debug('Skipping because github_token or version are empty.');
return;
}

const { owner, repo } = context.repo;

const octokit = getOctokit(githubToken);

const release = await octokit.request('GET /repos/{owner}/{repo}/releases/tags/{tag}', {
owner,
repo,
tag: version,
headers: {
'X-GitHub-Api-Version': '2022-11-28',
},
});

const prNumbers = extractPrsFromReleaseBody(release.data.body);

if (!prNumbers.length) {
core.debug('No PRs found in release body.');
return;
}

core.debug(`Found PRs in release body: ${prNumbers.join(', ')}`);

const linkedIssues = await Promise.all(
prNumbers.map(prNumber => getLinkedIssuesForPr(octokit, { repo, owner, prNumber })),
);

for (const pr of linkedIssues) {
if (!pr.issues.length) {
core.debug(`No linked issues found for PR #${pr.prNumber}`);
continue;
}

core.debug(`Linked issues for PR #${pr.prNumber}: ${pr.issues.map(issue => issue.number).join(',')}`);

for (const issue of pr.issues) {
if (await hasExistingComment(octokit, { repo, owner, issueNumber: issue.number })) {
core.debug(`Comment already exists for issue #${issue.number}`);
continue;
}

const body = `${RELEASE_COMMENT_HEADING}\n\nThis issue was referenced by PR #${pr.prNumber}, which was included in the [${version} release](https://github.com/${owner}/${repo}/releases/tag/${version}).`;

core.debug(`Creating comment for issue #${issue.number}`);

await octokit.rest.issues.createComment({
repo,
owner,
issue_number: issue.number,
body,
});
}
}
}

/**
*
* @param {string} body
* @returns {number[]}
*/
function extractPrsFromReleaseBody(body) {
const regex = /\[#(\d+)\]\(https:\/\/github\.com\/getsentry\/sentry-javascript\/pull\/(?:\d+)\)/gm;
const prNumbers = Array.from(new Set([...body.matchAll(regex)].map(match => parseInt(match[1]))));

return prNumbers.filter(number => !!number && !Number.isNaN(number));
}

/**
*
* @param {ReturnType<import('@actions/github').getOctokit>} octokit
* @param {{ repo: string, owner: string, prNumber: number}} options
* @returns {Promise<{ prNumber: number, issues: {id: string, number: number}[] }>}
*/
async function getLinkedIssuesForPr(octokit, { repo, owner, prNumber }) {
const res = await octokit.graphql(
`
query issuesForPr($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
id
closingIssuesReferences (first: 50) {
edges {
node {
id
number
}
}
}
}
}
}`,
{
prNumber,
owner,
repo,
},
);

const issues = res.repository?.pullRequest?.closingIssuesReferences.edges.map(edge => edge.node);
return {
prNumber,
issues,
};
}

/**
*
* @param {ReturnType<import('@actions/github').getOctokit>} octokit
* @param {{ repo: string, owner: string, issueNumber: number}} options
* @returns {Promise<boolean>}
*/
async function hasExistingComment(octokit, { repo, owner, issueNumber }) {
const { data: commentList } = await octokit.rest.issues.listComments({
repo,
owner,
issue_number: issueNumber,
});

return commentList.some(comment => comment.body.startsWith(RELEASE_COMMENT_HEADING));
}

run();
23 changes: 23 additions & 0 deletions dev-packages/release-comment-issues-gh-action/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@sentry-internal/release-comment-issues-gh-action",
"description": "An internal Github Action to comment on related issues when a release is published.",
"version": "8.31.0",
"license": "MIT",
"engines": {
"node": ">=18"
},
"private": true,
"main": "index.mjs",
"type": "module",
"scripts": {
"lint": "eslint . --format stylish",
"fix": "eslint . --format stylish --fix"
},
"dependencies": {
"@actions/core": "1.10.1",
"@actions/github": "6.0.0"
},
"volta": {
"extends": "../../package.json"
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@
"dev-packages/size-limit-gh-action",
"dev-packages/clear-cache-gh-action",
"dev-packages/external-contributor-gh-action",
"dev-packages/release-comment-issues-gh-action",
"dev-packages/rollup-utils"
],
"devDependencies": {
Expand Down
Loading
Loading