Skip to content
Merged
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
12 changes: 10 additions & 2 deletions AssignReviewers/assign.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,24 @@ function humanLogins(users) {
//
// When a whole stack is submitted at once, every PR's run starts together. So if a ready PR below
// has no assignees yet, wait for its own run to assign them before looking further down.
//
// The PR below is found by matching head branches across the repo's open PRs rather than with the
// list endpoint's `head: owner:branch` filter. In a repo that is a fork of another repo in the same
// org (goldengate-bms2000-string is a fork of stafl-bms2000-template), that filter finds nothing.
async function inheritedAssignees(github, owner, repo, pr, author, { maxDepth, parentWaitMs, pollMs, sleep }) {
let baseRef = pr.base.ref;
const seen = new Set([pr.number]);
const assigneesOf = async (number) => {
const { data } = await github.rest.pulls.get({ owner, repo, pull_number: number });
return humanLogins(data.assignees).filter((login) => login !== author && login !== data.user.login);
};
// PRs from other repos (forks) can share a branch name with this repo's stack, so they're left out.
const thisRepo = `${owner}/${repo}`.toLowerCase();
const open = (await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', per_page: 100 })).filter(
(p) => p.head.repo?.full_name.toLowerCase() === thisRepo,
);
for (let depth = 0; depth < maxDepth; depth++) {
const parents = await github.rest.pulls.list({ owner, repo, state: 'open', head: `${owner}:${baseRef}` });
const parent = parents.data.find((p) => !seen.has(p.number));
const parent = open.find((p) => p.head.ref === baseRef && !seen.has(p.number));
if (!parent) return null;
seen.add(parent.number);
let assignees = await assigneesOf(parent.number);
Expand Down
40 changes: 36 additions & 4 deletions AssignReviewers/assign.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ const STAFF = ['staffA', 'staffB', 'staffC'];
const EVERYONE = [...STAFF, 'devD', 'devE', 'devF'];

// A fake GitHub: open PRs keyed by number, team membership, per-user load, and when each person
// was last assigned (`assigned`, login to ISO time).
function fakeGithub({ prs, load = {}, assigned = {} }) {
// was last assigned (`assigned`, login to ISO time). A PR's `headRepo` is the repo its branch is in
// (this one by default). `fork` makes the list endpoint's head filter find nothing, as GitHub's
// does in a repo that is a fork of another repo in the same org.
function fakeGithub({ prs, load = {}, assigned = {}, fork = false }) {
const calls = { assign: [], unassign: [], review: [], graphql: 0 };
const byNumber = new Map(prs.map((p) => [p.number, { assignees: [], requested: [], teams: [], reviews: [], ...p }]));
const teams = { embeddedreviewersstaff: STAFF, embeddedreviewers: EVERYONE };
Expand Down Expand Up @@ -66,8 +68,13 @@ function fakeGithub({ prs, load = {}, assigned = {} }) {
},
list: async ({ head }) => ({
data: [...byNumber.values()]
.filter((p) => `StaflSystems:${p.head}` === head)
.map((p) => ({ number: p.number, draft: Boolean(p.draft), base: { ref: p.base } })),
.filter((p) => !head || (!fork && !p.headRepo && `StaflSystems:${p.head}` === head))
.map((p) => ({
number: p.number,
draft: Boolean(p.draft),
base: { ref: p.base },
head: { ref: p.head, repo: { full_name: p.headRepo ?? 'StaflSystems/StaflLib' } },
})),
}),
listReviews: async ({ pull_number }) => ({
data: byNumber.get(pull_number).reviews.map(([login, state]) => ({ user: user(login), state })),
Expand Down Expand Up @@ -194,6 +201,31 @@ test('skips a draft parent with no assignees and inherits from further down', as
assert.deepStrictEqual(fake.pr(32).assignees.sort(), ['devF', 'staffB']);
});

test('mid-stack in a repo that is a fork: still finds the PR below', async () => {
const fake = fakeGithub({
fork: true,
prs: [
{ number: 25, author: 'devE', head: 'a', base: 'main', assignees: ['staffC', 'devD'] },
{ number: 26, author: 'devE', head: 'b', base: 'a' },
],
});
await run(fake, 26);
assert.deepStrictEqual(fake.pr(26).assignees.sort(), ['devD', 'staffC']);
assert.strictEqual(fake.calls.graphql, 0);
});

test('a PR from a fork with the same branch name is not the PR below', async () => {
const fake = fakeGithub({
prs: [
{ number: 27, author: 'devF', head: 'a', headRepo: 'someone/StaflLib', base: 'main', assignees: ['staffA', 'devD'] },
{ number: 28, author: 'devE', head: 'b', base: 'a' },
],
load: { staffA: 3, staffB: 0, staffC: 3, devD: 3, devF: 0 },
});
await run(fake, 28);
assert.deepStrictEqual(fake.pr(28).assignees, ['staffB', 'devF']);
});

test('waits for a ready parent submitted in the same stack to get its assignees', async () => {
const fake = fakeGithub({
prs: [
Expand Down
Loading