Skip to content

Commit 9383d99

Browse files
authored
Merge branch 'master' into master
2 parents f159606 + b92867b commit 9383d99

4 files changed

Lines changed: 255 additions & 0 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
name: Assign PR reviewers
2+
on:
3+
pull_request_target:
4+
types: [opened, synchronize, reopened]
5+
6+
jobs:
7+
assign_reviewers:
8+
name: Assign reviewers
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Generate app token
13+
id: token
14+
uses: actions/create-github-app-token@v2
15+
with:
16+
app-id: ${{ vars.PR_BOT_ID }}
17+
private-key: ${{ secrets.PR_BOT_PEM }}
18+
19+
- name: Checkout
20+
uses: actions/checkout@v5
21+
with:
22+
ref: refs/pull/${{ github.event.pull_request.number }}/head
23+
24+
- name: Install dependencies
25+
uses: ./.github/actions/npm-ci
26+
- name: Build
27+
run: |
28+
npx gulp build
29+
- name: Install s3 client
30+
run: |
31+
npm install @aws-sdk/client-s3
32+
- name: Get PR properties
33+
id: get-props
34+
uses: actions/github-script@v8
35+
env:
36+
AWS_ACCESS_KEY_ID: ${{ vars.PR_BOT_AWS_AK }}
37+
AWS_SECRET_ACCESS_KEY: ${{ secrets.PR_BOT_AWS_SAK }}
38+
with:
39+
github-token: ${{ steps.token.outputs.token }}
40+
script: |
41+
const getProps = require('./.github/workflows/scripts/getPRProperties.js')
42+
const props = await getProps({
43+
github,
44+
context,
45+
prNo: ${{ github.event.pull_request.number }},
46+
reviewerTeam: '${{ vars.REVIEWER_TEAM }}',
47+
engTeam: '${{ vars.ENG_TEAM }}'
48+
});
49+
console.log('PR properties:', JSON.stringify(props, null, 2));
50+
return props;
51+
- name: Assign reviewers
52+
if: ${{ !fromJSON(steps.get-props.outputs.result).review.ok }}
53+
uses: actions/github-script@v8
54+
with:
55+
github-token: ${{ steps.token.outputs.token }}
56+
script: |
57+
const assignReviewers = require('./.github/workflows/scripts/assignReviewers.js')
58+
const reviewers = await assignReviewers({github, context, prData: ${{ steps.get-props.outputs.result }} });
59+
console.log('Assigned reviewers:', JSON.stringify(reviewers, null, 2));
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const ghRequester = require('./ghRequest.js');
2+
3+
function pickFrom(candidates, exclude, no) {
4+
exclude = exclude.slice();
5+
const winners = [];
6+
while (winners.length < no) {
7+
const candidate = candidates[Math.floor(Math.random() * candidates.length)];
8+
if (!exclude.includes(candidate)) {
9+
winners.push(candidate);
10+
exclude.push(candidate);
11+
}
12+
}
13+
return winners;
14+
}
15+
16+
async function assignReviewers({github, context, prData}) {
17+
const reviewers = prData.review.reviewers.map(rv => rv.login);
18+
const missingPrebidEng = prData.review.requires.prebidEngineers - prData.review.prebidEngineers;
19+
const missingPrebidReviewers = prData.review.requires.prebidReviewers - prData.review.prebidReviewers - missingPrebidEng;
20+
21+
if (missingPrebidEng > 0) {
22+
reviewers.push(...pickFrom(prData.prebidEngineers, [...reviewers, prData.author.login], missingPrebidEng))
23+
}
24+
if (missingPrebidReviewers > 0) {
25+
reviewers.push(...pickFrom(prData.prebidReviewers, [...reviewers, prData.author.login], missingPrebidReviewers))
26+
}
27+
28+
const request = ghRequester(github);
29+
await request('POST /repos/{owner}/{repo}/pulls/{prNo}/requested_reviewers', {
30+
owner: context.repo.owner,
31+
repo: context.repo.repo,
32+
prNo: prData.pr,
33+
reviewers
34+
})
35+
return reviewers;
36+
}
37+
38+
module.exports = assignReviewers;
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
const ghRequester = require('./ghRequest.js');
2+
const AWS = require("@aws-sdk/client-s3");
3+
4+
const MODULE_PATTERNS = [
5+
/^modules\/([^\/]+)BidAdapter(\.(\w+)|\/)/,
6+
/^modules\/([^\/]+)AnalyticsAdapter(\.(\w+)|\/)/,
7+
/^modules\/([^\/]+)RtdProvider(\.(\w+)|\/)/,
8+
/^modules\/([^\/]+)IdSystem(\.(\w+)|\/)/
9+
]
10+
11+
const EXCLUDE_PATTERNS = [
12+
/^test\//,
13+
/^integrationExamples\//
14+
]
15+
const LIBRARY_PATTERN = /^libraries\/([^\/]+)\//;
16+
17+
function extractVendor(chunkName) {
18+
for (const pat of MODULE_PATTERNS) {
19+
const match = pat.exec(`modules/${chunkName}`);
20+
if (match != null) {
21+
return match[1];
22+
}
23+
}
24+
return chunkName;
25+
}
26+
27+
const getLibraryRefs = (() => {
28+
const deps = require('../../../build/dist/dependencies.json');
29+
const refs = {};
30+
return function (libraryName) {
31+
if (!refs.hasOwnProperty(libraryName)) {
32+
refs[libraryName] = new Set();
33+
Object.entries(deps)
34+
.filter(([name, deps]) => deps.includes(`${libraryName}.js`))
35+
.forEach(([name]) => refs[libraryName].add(extractVendor(name)))
36+
}
37+
return refs[libraryName];
38+
}
39+
})();
40+
41+
function isCoreFile(path) {
42+
if (EXCLUDE_PATTERNS.find(pat => pat.test(path))) {
43+
return false;
44+
}
45+
if (MODULE_PATTERNS.find(pat => pat.test(path)) ) {
46+
return false;
47+
}
48+
const lib = LIBRARY_PATTERN.exec(path);
49+
if (lib != null) {
50+
// a library is "core" if it's used by more than one vendor
51+
return getLibraryRefs(lib[1]).size > 1;
52+
}
53+
return true;
54+
}
55+
56+
async function isPrebidMember(ghHandle) {
57+
const client = new AWS.S3({region: 'us-east-2'});
58+
const res = await client.getObject({
59+
Bucket: 'repo-dashboard-files-891377123989',
60+
Key: 'memberMapping.json'
61+
});
62+
const members = JSON.parse(await res.Body.transformToString());
63+
return members.includes(ghHandle);
64+
}
65+
66+
67+
async function getPRProperties({github, context, prNo, reviewerTeam, engTeam}) {
68+
const request = ghRequester(github);
69+
let [files, pr, prebidReviewers, prebidEngineers] = await Promise.all([
70+
request('GET /repos/{owner}/{repo}/pulls/{prNo}/files', {
71+
owner: context.repo.owner,
72+
repo: context.repo.repo,
73+
prNo,
74+
}),
75+
request('GET /repos/{owner}/{repo}/pulls/{prNo}', {
76+
owner: context.repo.owner,
77+
repo: context.repo.repo,
78+
prNo,
79+
}),
80+
...[reviewerTeam, engTeam].map(team => request('GET /orgs/{org}/teams/{team}/members', {
81+
org: context.repo.owner,
82+
team,
83+
}))
84+
]);
85+
prebidReviewers = prebidReviewers.data.map(datum => datum.login);
86+
prebidEngineers = prebidEngineers.data.map(datum=> datum.login);
87+
let isCoreChange = false;
88+
files = files.data.map(datum => datum.filename).map(file => {
89+
const core = isCoreFile(file);
90+
if (core) isCoreChange = true;
91+
return {
92+
file,
93+
core
94+
}
95+
});
96+
const review = {
97+
prebidEngineers: 0,
98+
prebidReviewers: 0,
99+
reviewers: []
100+
};
101+
const author = pr.data.user.login;
102+
pr.data.requested_reviewers
103+
.map(rv => rv.login)
104+
.forEach(reviewer => {
105+
if (reviewer === author) return;
106+
const isPrebidEngineer = prebidEngineers.includes(reviewer);
107+
const isPrebidReviewer = isPrebidEngineer || prebidReviewers.includes(reviewer);
108+
if (isPrebidEngineer) {
109+
review.prebidEngineers += 1;
110+
}
111+
if (isPrebidReviewer) {
112+
review.prebidReviewers += 1
113+
}
114+
review.reviewers.push({
115+
login: reviewer,
116+
isPrebidEngineer,
117+
isPrebidReviewer,
118+
})
119+
});
120+
const data = {
121+
pr: prNo,
122+
author: {
123+
login: author,
124+
isPrebidMember: await isPrebidMember(author)
125+
},
126+
isCoreChange,
127+
files,
128+
prebidReviewers,
129+
prebidEngineers,
130+
review,
131+
}
132+
data.review.requires = reviewRequirements(data);
133+
data.review.ok = satisfiesReviewRequirements(data.review);
134+
return data;
135+
}
136+
137+
function reviewRequirements(prData) {
138+
return {
139+
prebidEngineers: prData.author.isPrebidMember ? 1 : 0,
140+
prebidReviewers: prData.isCoreChange ? 2 : 1
141+
}
142+
}
143+
144+
function satisfiesReviewRequirements({requires, prebidEngineers, prebidReviewers}) {
145+
return prebidEngineers >= requires.prebidEngineers && prebidReviewers >= requires.prebidReviewers
146+
}
147+
148+
149+
module.exports = getPRProperties;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module.exports = function githubRequester(github) {
2+
return function (verb, params) {
3+
return github.request(verb, Object.assign({
4+
headers: {
5+
'X-GitHub-Api-Version': '2022-11-28'
6+
}
7+
}, params))
8+
}
9+
}

0 commit comments

Comments
 (0)