Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(approvals-satisfied): add users support #581

Merged
merged 18 commits into from
Apr 2, 2024
Merged
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ Each of the following helpers are defined in a file of the same name in `src/hel

### [approvals-satisfied](.github/workflows/approvals-satisfied.yml)

- Returns `true` if the PR has been approved by the specified GitHub team(s) and `false` otherwise
- If GitHub teams are omitted, uses `CODEOWNERS.md` to determine teams to use
- Returns `true` if the PR has been approved by the specified GitHub team(s) or user(s) and `false` otherwise
- If GitHub teams are omitted, uses `CODEOWNERS.md` to determine teams and/or users to use
- Note: If you are providing teams in input, full team name is NOT needed. i.e. `team-name` works and `org/team-name` is NOT needed

### [approve-pr](.github/workflows/approve-pr.yml)

Expand Down
73 changes: 58 additions & 15 deletions dist/431.index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/431.index.js.map

Large diffs are not rendered by default.

73 changes: 58 additions & 15 deletions dist/676.index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/676.index.js.map

Large diffs are not rendered by default.

74 changes: 59 additions & 15 deletions src/helpers/approvals-satisfied.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,37 +23,45 @@ import { paginateAllReviews } from '../utils/paginate-all-reviews';

export class ApprovalsSatisfied extends HelperInputs {
teams?: string;
users?: string;
number_of_reviewers?: string;
pull_number?: string;
}

export const approvalsSatisfied = async ({ teams, number_of_reviewers = '1', pull_number }: ApprovalsSatisfied = {}) => {
export const approvalsSatisfied = async ({ teams, users, number_of_reviewers = '1', pull_number }: ApprovalsSatisfied = {}) => {
const prNumber = pull_number ? Number(pull_number) : context.issue.number;

const teamsList = updateTeamsList(teams?.split('\n'));
if (!validateTeamsList(teamsList)) {
core.setFailed('If teams input is in the format "org/team", then the org must be the same as the repository org');
return false;
}
const usersList = users?.split('\n');

const reviews = await paginateAllReviews(prNumber);
const approverLogins = reviews
.filter(({ state }) => state === 'APPROVED')
.map(({ user }) => user?.login)
.filter(Boolean);
core.debug(`PR already approved by: ${approverLogins.toString()}`);

const teamsList = teams?.split('\n');
const requiredCodeOwnersEntries = teamsList ? createArtificialCodeOwnersEntry(teamsList) : await getRequiredCodeOwnersEntries(prNumber);
const requiredCodeOwnersEntries =
teamsList || usersList
? createArtificialCodeOwnersEntry({ teams: teamsList, users: usersList })
: await getRequiredCodeOwnersEntries(prNumber);
const requiredCodeOwnersEntriesWithOwners = requiredCodeOwnersEntries.filter(({ owners }) => owners.length);

const codeOwnersEntrySatisfiesApprovals = async (entry: Pick<CodeOwnersEntry, 'owners'>) => {
const teamsAndLoginsLists = await map(entry.owners, async team => {
const { data } = await octokit.teams.listMembersInOrg({
org: context.repo.owner,
team_slug: convertToTeamSlug(team),
per_page: 100
});
return data.map(({ login }) => ({ team, login }));
const loginsLists = await map(entry.owners, async teamOrUser => {
if (isTeam(teamOrUser)) {
return await fetchTeamLogins(teamOrUser);
} else {
return [teamOrUser];
}
});
const codeOwnerLogins = teamsAndLoginsLists.flat().map(({ login }) => login);
const codeOwnerLogins = distinct(loginsLists.flat());

const numberOfCollectiveApprovalsAcrossTeams = approverLogins.filter(login => codeOwnerLogins.includes(login)).length;
const numberOfApprovalsForSingleTeam = codeOwnerLogins.filter(login => approverLogins.includes(login)).length;
const numberOfApprovals = entry.owners.length > 1 ? numberOfCollectiveApprovalsAcrossTeams : numberOfApprovalsForSingleTeam;
const numberOfApprovals = approverLogins.filter(login => codeOwnerLogins.includes(login)).length;

core.debug(`Current number of approvals satisfied for ${entry.owners}: ${numberOfApprovals}`);

Expand All @@ -65,4 +73,40 @@ export const approvalsSatisfied = async ({ teams, number_of_reviewers = '1', pul
return booleans.every(Boolean);
};

const createArtificialCodeOwnersEntry = (teams: string[]) => [{ owners: teams }];
const createArtificialCodeOwnersEntry = ({ teams = [], users = [] }: { teams?: string[]; users?: string[] }) => [
{ owners: teams.concat(users) }
];
const distinct = (arrayWithDuplicates: string[]) => arrayWithDuplicates.filter((n, i) => arrayWithDuplicates.indexOf(n) === i);
const isTeam = (teamOrUser: string) => teamOrUser.includes('/');
vsingal-p marked this conversation as resolved.
Show resolved Hide resolved
const fetchTeamLogins = async (team: string) => {
const { data } = await octokit.teams.listMembersInOrg({
org: context.repo.owner,
team_slug: convertToTeamSlug(team),
per_page: 100
});
return data.map(({ login }) => login);
};
const updateTeamsList = (teamsList?: string[]) => {
if (teamsList) {
return teamsList.map(team => {
if (!team.includes('/')) {
return `${context.repo.owner}/${team}`;
} else {
return team;
}
});
} else {
return undefined;
}
danadajian marked this conversation as resolved.
Show resolved Hide resolved
};

const validateTeamsList = (teamsList?: string[]) => {
if (teamsList) {
return teamsList.every(team => {
const inputOrg = team.split('/')[0];
return inputOrg === context.repo.owner;
});
} else {
return true;
}
danadajian marked this conversation as resolved.
Show resolved Hide resolved
};
Loading
Loading