Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/workflows/ping-code-owners.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Ping Code Owners

on:
pull_request_target:
types: [opened]

permissions:
pull-requests: write
contents: read

jobs:
ping-owners:
if: github.repository == 'nodejs/node'
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false

- name: Use Node.js lts/*
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: lts/*

- name: Install codeowners-utils
run: npm install --no-save codeowners-utils

- name: Ping code owners
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
run: node tools/actions/ping-owners.mjs
72 changes: 72 additions & 0 deletions tools/actions/ping-owners.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { matchPattern, parse } from 'codeowners-utils';
import { readFileSync } from 'node:fs';

const { GITHUB_TOKEN, PR_NUMBER, REPO_OWNER, REPO_NAME } = process.env;

async function githubRequest(path, options = {}) {
const response = await fetch(`https://api.github.com${path}`, {
...options,
headers: {
'Authorization': `Bearer ${GITHUB_TOKEN}`,
'Accept': 'application/vnd.github.v3+json',
'X-GitHub-Api-Version': '2022-11-28',
...options.headers,
},
});
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
}
return response.json();
}

async function getChangedFiles() {
const files = [];
let page = 1;
while (true) {
const data = await githubRequest(
`/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${PR_NUMBER}/files?per_page=100&page=${page}`,
);
files.push(...data.map((f) => f.filename));
if (data.length < 100) break;
page++;
}
return files;
}

export function getOwnersForPaths(codeownersContent, changedFiles) {
const definitions = parse(codeownersContent);
let ownersForPaths = [];

for (const { pattern, owners } of definitions) {
for (const file of changedFiles) {
if (matchPattern(file, pattern)) {
ownersForPaths = ownersForPaths.concat(owners);
}
}
}

return ownersForPaths.filter((v, i) => ownersForPaths.indexOf(v) === i).sort();
}

export function getCommentBody(owners) {
return `Review requested:\n\n${owners.map((i) => `- [ ] ${i}`).join('\n')}`;
}

async function pingOwners() {
const changedFiles = await getChangedFiles();
const codeownersContent = readFileSync('.github/CODEOWNERS', 'utf8');
const owners = getOwnersForPaths(codeownersContent, changedFiles);
if (owners.length === 0) return;
await githubRequest(
`/repos/${REPO_OWNER}/${REPO_NAME}/issues/${PR_NUMBER}/comments`,
{
method: 'POST',
body: JSON.stringify({ body: getCommentBody(owners) }),
},
);
}

pingOwners().catch((err) => {
console.error(err);
process.exit(1);
});
Loading