diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e7644f6b1979fd3..86abef0c2a01848 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,12 @@ jobs: key: static - run: npm ci + + - run: npx tsx bin/post-codeowners-comment/index.ts + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - run: npm run check - uses: reviewdog/action-eslint@v1 diff --git a/bin/post-codeowners-comment/constants.ts b/bin/post-codeowners-comment/constants.ts new file mode 100644 index 000000000000000..e5cd61eaad403d2 --- /dev/null +++ b/bin/post-codeowners-comment/constants.ts @@ -0,0 +1,3 @@ +export const GITHUB_ACTIONS_BOT_ID = 41898282; +export const COMMENT_PREFIX = + "This pull request requires reviews from **CODEOWNERS** as it changes files that match the following patterns:"; diff --git a/bin/post-codeowners-comment/index.ts b/bin/post-codeowners-comment/index.ts new file mode 100644 index 000000000000000..976c54fc2d911d3 --- /dev/null +++ b/bin/post-codeowners-comment/index.ts @@ -0,0 +1,95 @@ +import * as core from "@actions/core"; +import * as github from "@actions/github"; +import type { PullRequestEvent } from "@octokit/webhooks-types"; + +import { loadOwners, matchFile } from "codeowners-utils"; +import { GITHUB_ACTIONS_BOT_ID, COMMENT_PREFIX } from "./constants.ts"; + +async function run(): Promise { + try { + if (!process.env.GITHUB_TOKEN) { + core.setFailed(`Could not find GITHUB_TOKEN in env`); + process.exit(); + } + + const octokit = github.getOctokit(process.env.GITHUB_TOKEN); + const payload = github.context.payload as PullRequestEvent; + + const { owner, repo } = github.context.repo; + const pullRequestNumber = payload.number; + + const files = await octokit.paginate(octokit.rest.pulls.listFiles, { + owner, + repo, + pull_number: pullRequestNumber, + }); + + const codeowners = await loadOwners(process.cwd()); + + if (!codeowners) { + throw new Error("Unable to load CODEOWNERS file."); + } + + const matchedPatterns = [ + ...new Set( + files.flatMap((file) => matchFile(file.filename, codeowners) ?? []), + ), + ]; + + const { data: comments } = await octokit.rest.issues.listComments({ + owner, + repo, + issue_number: pullRequestNumber, + per_page: 100, + }); + + const existingComment = comments.find( + (comment) => + comment.user?.id === GITHUB_ACTIONS_BOT_ID && + comment.body?.includes(COMMENT_PREFIX), + ); + + if (existingComment) { + core.info(`Found existing comment with ID ${existingComment.id}`); + } else { + core.info(`No existing comment found`); + } + + const comment = [ + COMMENT_PREFIX, + "| Pattern | Owners |", + "| ------- | ------ |", + ...matchedPatterns.map( + (pattern) => + `| \`${pattern.pattern}\` | ${pattern.owners.map((owner) => `\`${owner}\``).join(", ")} |`, + ), + ].join("\n"); + + if (existingComment) { + core.info( + `Updating ${existingComment.id} with ${JSON.stringify(comment)}`, + ); + await octokit.rest.issues.updateComment({ + owner, + repo, + comment_id: existingComment.id, + body: comment, + }); + } else { + core.info(`Creating new comment with ${JSON.stringify(comment)}`); + await octokit.rest.issues.createComment({ + owner, + repo, + issue_number: pullRequestNumber, + body: comment, + }); + } + } catch (error) { + if (error instanceof Error) { + core.setFailed(error.message); + } + process.exit(); + } +} + +run();