diff --git a/.github/workflows/release-comment-issues.yml b/.github/workflows/release-comment-issues.yml new file mode 100644 index 000000000000..80cb04eb6b54 --- /dev/null +++ b/.github/workflows/release-comment-issues.yml @@ -0,0 +1,33 @@ +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 }} diff --git a/dev-packages/release-comment-issues-gh-action/.eslintrc.cjs b/dev-packages/release-comment-issues-gh-action/.eslintrc.cjs new file mode 100644 index 000000000000..8c67e0037908 --- /dev/null +++ b/dev-packages/release-comment-issues-gh-action/.eslintrc.cjs @@ -0,0 +1,14 @@ +module.exports = { + extends: ['../../.eslintrc.js'], + parserOptions: { + sourceType: 'module', + ecmaVersion: 'latest', + }, + + overrides: [ + { + files: ['*.mjs'], + extends: ['@sentry-internal/sdk/src/base'], + }, + ], +}; diff --git a/dev-packages/release-comment-issues-gh-action/action.yml b/dev-packages/release-comment-issues-gh-action/action.yml new file mode 100644 index 000000000000..fcfb0bf00f2b --- /dev/null +++ b/dev-packages/release-comment-issues-gh-action/action.yml @@ -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' diff --git a/dev-packages/release-comment-issues-gh-action/index.mjs b/dev-packages/release-comment-issues-gh-action/index.mjs new file mode 100644 index 000000000000..c49ad6575ef7 --- /dev/null +++ b/dev-packages/release-comment-issues-gh-action/index.mjs @@ -0,0 +1,138 @@ +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, { repo, owner }); + + 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 + * @param {{ repo: string, owner: string}} options + * @returns {number[]} + */ +function extractPrsFromReleaseBody(body, { repo, owner }) { + const regex = new RegExp(`\\[#(\\d+)\\]\\(https:\\/\\/github\\.com\\/${owner}\\/${repo}\\/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} 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} octokit + * @param {{ repo: string, owner: string, issueNumber: number}} options + * @returns {Promise} + */ +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(); diff --git a/dev-packages/release-comment-issues-gh-action/package.json b/dev-packages/release-comment-issues-gh-action/package.json new file mode 100644 index 000000000000..49c8f2ad5caa --- /dev/null +++ b/dev-packages/release-comment-issues-gh-action/package.json @@ -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" + } +} diff --git a/package.json b/package.json index 365e1eb13922..b17092f04a80 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/yarn.lock b/yarn.lock index a32337e3835f..91c55bb33d69 100644 --- a/yarn.lock +++ b/yarn.lock @@ -27,6 +27,16 @@ dependencies: "@actions/io" "^1.0.1" +"@actions/github@6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@actions/github/-/github-6.0.0.tgz#65883433f9d81521b782a64cc1fd45eef2191ea7" + integrity sha512-alScpSVnYmjNEXboZjarjukQEzgCRmjMv6Xj47fsdnqGS73bjJNDpiiXmp8jr0UZLdUB6d9jW63IcmddUP+l0g== + dependencies: + "@actions/http-client" "^2.2.0" + "@octokit/core" "^5.0.1" + "@octokit/plugin-paginate-rest" "^9.0.0" + "@octokit/plugin-rest-endpoint-methods" "^10.0.0" + "@actions/github@^5.0.0": version "5.1.1" resolved "https://registry.yarnpkg.com/@actions/github/-/github-5.1.1.tgz#40b9b9e1323a5efcf4ff7dadd33d8ea51651bbcb" @@ -53,6 +63,14 @@ tunnel "^0.0.6" undici "^5.25.4" +"@actions/http-client@^2.2.0": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@actions/http-client/-/http-client-2.2.3.tgz#31fc0b25c0e665754ed39a9f19a8611fc6dab674" + integrity sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA== + dependencies: + tunnel "^0.0.6" + undici "^5.25.4" + "@actions/io@1.1.3", "@actions/io@^1.0.1": version "1.1.3" resolved "https://registry.yarnpkg.com/@actions/io/-/io-1.1.3.tgz#4cdb6254da7962b07473ff5c335f3da485d94d71" @@ -6747,6 +6765,11 @@ dependencies: "@octokit/types" "^8.0.0" +"@octokit/auth-token@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-4.0.0.tgz#40d203ea827b9f17f42a29c6afb93b7745ef80c7" + integrity sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA== + "@octokit/core@^3.6.0": version "3.6.0" resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.6.0.tgz#3376cb9f3008d9b3d110370d90e0a1fcd5fe6085" @@ -6786,6 +6809,19 @@ before-after-hook "^2.2.0" universal-user-agent "^6.0.0" +"@octokit/core@^5.0.1": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-5.2.0.tgz#ddbeaefc6b44a39834e1bb2e58a49a117672a7ea" + integrity sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg== + dependencies: + "@octokit/auth-token" "^4.0.0" + "@octokit/graphql" "^7.1.0" + "@octokit/request" "^8.3.1" + "@octokit/request-error" "^5.1.0" + "@octokit/types" "^13.0.0" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + "@octokit/endpoint@^6.0.1": version "6.0.12" resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.12.tgz#3b4d47a4b0e79b1027fb8d75d4221928b2d05658" @@ -6804,6 +6840,14 @@ is-plain-object "^5.0.0" universal-user-agent "^6.0.0" +"@octokit/endpoint@^9.0.1": + version "9.0.5" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-9.0.5.tgz#e6c0ee684e307614c02fc6ac12274c50da465c44" + integrity sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw== + dependencies: + "@octokit/types" "^13.1.0" + universal-user-agent "^6.0.0" + "@octokit/graphql@^4.5.8": version "4.8.0" resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.8.0.tgz#664d9b11c0e12112cbf78e10f49a05959aa22cc3" @@ -6822,6 +6866,15 @@ "@octokit/types" "^8.0.0" universal-user-agent "^6.0.0" +"@octokit/graphql@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-7.1.0.tgz#9bc1c5de92f026648131f04101cab949eeffe4e0" + integrity sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ== + dependencies: + "@octokit/request" "^8.3.0" + "@octokit/types" "^13.0.0" + universal-user-agent "^6.0.0" + "@octokit/openapi-types@^12.11.0": version "12.11.0" resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-12.11.0.tgz#da5638d64f2b919bca89ce6602d059f1b52d3ef0" @@ -6842,6 +6895,16 @@ resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-18.0.0.tgz#f43d765b3c7533fd6fb88f3f25df079c24fccf69" integrity sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw== +"@octokit/openapi-types@^20.0.0": + version "20.0.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-20.0.0.tgz#9ec2daa0090eeb865ee147636e0c00f73790c6e5" + integrity sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA== + +"@octokit/openapi-types@^22.2.0": + version "22.2.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-22.2.0.tgz#75aa7dcd440821d99def6a60b5f014207ae4968e" + integrity sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg== + "@octokit/plugin-enterprise-rest@6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" @@ -6869,11 +6932,25 @@ "@octokit/tsconfig" "^1.0.2" "@octokit/types" "^9.2.3" +"@octokit/plugin-paginate-rest@^9.0.0": + version "9.2.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz#2e2a2f0f52c9a4b1da1a3aa17dabe3c459b9e401" + integrity sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw== + dependencies: + "@octokit/types" "^12.6.0" + "@octokit/plugin-request-log@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz#5e50ed7083a613816b1e4a28aeec5fb7f1462e85" integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== +"@octokit/plugin-rest-endpoint-methods@^10.0.0": + version "10.4.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.4.1.tgz#41ba478a558b9f554793075b2e20cd2ef973be17" + integrity sha512-xV1b+ceKV9KytQe3zCVqjg+8GTGfDYwaT1ATU5isiUyVtlVAO3HNdzpS4sr4GBx4hxQ46s7ITtZrAsxG22+rVg== + dependencies: + "@octokit/types" "^12.6.0" + "@octokit/plugin-rest-endpoint-methods@^5.13.0": version "5.16.2" resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz#7ee8bf586df97dd6868cf68f641354e908c25342" @@ -6915,6 +6992,15 @@ deprecation "^2.0.0" once "^1.4.0" +"@octokit/request-error@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-5.1.0.tgz#ee4138538d08c81a60be3f320cd71063064a3b30" + integrity sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q== + dependencies: + "@octokit/types" "^13.1.0" + deprecation "^2.0.0" + once "^1.4.0" + "@octokit/request@^5.6.0", "@octokit/request@^5.6.3": version "5.6.3" resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.6.3.tgz#19a022515a5bba965ac06c9d1334514eb50c48b0" @@ -6939,6 +7025,16 @@ node-fetch "^2.6.7" universal-user-agent "^6.0.0" +"@octokit/request@^8.3.0", "@octokit/request@^8.3.1": + version "8.4.0" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-8.4.0.tgz#7f4b7b1daa3d1f48c0977ad8fffa2c18adef8974" + integrity sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw== + dependencies: + "@octokit/endpoint" "^9.0.1" + "@octokit/request-error" "^5.1.0" + "@octokit/types" "^13.1.0" + universal-user-agent "^6.0.0" + "@octokit/rest@19.0.11": version "19.0.11" resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-19.0.11.tgz#2ae01634fed4bd1fca5b642767205ed3fd36177c" @@ -6971,6 +7067,20 @@ dependencies: "@octokit/openapi-types" "^18.0.0" +"@octokit/types@^12.6.0": + version "12.6.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-12.6.0.tgz#8100fb9eeedfe083aae66473bd97b15b62aedcb2" + integrity sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw== + dependencies: + "@octokit/openapi-types" "^20.0.0" + +"@octokit/types@^13.0.0", "@octokit/types@^13.1.0": + version "13.5.1" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-13.5.1.tgz#5685a91f295195ddfff39723b093b0df9609ce6e" + integrity sha512-F41lGiWBKPIWPBgjSvaDXTTQptBujnozENAK3S//nj7xsFdYdirImKlBB/hTjr+Vii68SM+8jG3UJWRa6DMuDA== + dependencies: + "@octokit/openapi-types" "^22.2.0" + "@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.39.0", "@octokit/types@^6.40.0": version "6.41.0" resolved "https://registry.yarnpkg.com/@octokit/types/-/types-6.41.0.tgz#e58ef78d78596d2fb7df9c6259802464b5f84a04"