Skip to content
This repository has been archived by the owner on Jan 19, 2024. It is now read-only.

Commit

Permalink
feat(triage): adding triage PR plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
ocombe committed Oct 16, 2018
1 parent 282c831 commit c89e411
Show file tree
Hide file tree
Showing 5 changed files with 204 additions and 23 deletions.
24 changes: 24 additions & 0 deletions functions/src/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Please help to unblock it by resolving these conflicts. Thanks!`,
If you can't get the PR to a green state due to flakes or broken master, please try rebasing to master and/or restarting the CI job. If that fails and you believe that the issue is not due to your change, please contact the caretaker and ask for help.`
},

// triage for issues
triage: {
// set to true to disable
disabled: true,
Expand All @@ -95,12 +96,27 @@ If you can't get the PR to a green state due to flakes or broken master, please
l1TriageLabels: [["comp: *"]],
// arrays of labels that determine if an issue has been fully triaged
l2TriageLabels: [["type: bug/fix", "severity*", "freq*", "comp: *"], ["type: feature", "comp: *"], ["type: refactor", "comp: *"], ["type: RFC / Discussion / question", "comp: *"]]
},

// triage for PRs
triagePR: {
// set to true to disable
disabled: true,
// number of the milestone to apply when the PR has not been triaged yet
needsTriageMilestone: 83,
// number of the milestone to apply when the PR is triaged
defaultMilestone: 82,
// arrays of labels that determine if a PR has been triaged by the caretaker
l1TriageLabels: [["comp: *"]],
// arrays of labels that determine if a PR has been fully triaged
l2TriageLabels: [["type: *", "effort*", "risk*", "comp: *"]]
}
};

export interface AppConfig {
merge: MergeConfig;
triage: TriageConfig;
triagePR: TriagePRConfig;
size: SizeConfig;
}

Expand Down Expand Up @@ -143,6 +159,14 @@ export interface TriageConfig {
triagedLabels?: string[][];
}

export interface TriagePRConfig {
disabled: boolean;
needsTriageMilestone: number;
defaultMilestone: number;
l1TriageLabels: string[][];
l2TriageLabels: string[][];
}

export interface SizeConfig {
disabled: boolean;
maxSizeIncrease: number | string;
Expand Down
24 changes: 22 additions & 2 deletions functions/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,9 @@ exports.init = https.onRequest(async (request: Request, response: Response) => {
});

/**
* Manually trigger init for triage issues, you shouldn't need to use that unless you clean the database
* Manually trigger init to triage issues, you shouldn't need to use that unless you clean the database
*/
exports.initIssues = https.onRequest(async (request: Request, response: Response) => {
exports.initTriage = https.onRequest(async (request: Request, response: Response) => {
try {
await tasks.triageTask.manualInit().catch(err => {
console.error(err);
Expand All @@ -102,6 +102,26 @@ exports.initIssues = https.onRequest(async (request: Request, response: Response
}
});

/**
* Manually trigger init to triage PR, you shouldn't need to use that unless you clean the database
*/
exports.initTriagePR = https.onRequest(async (request: Request, response: Response) => {
try {
await tasks.triagePRTask.manualInit().catch(err => {
console.error(err);
});
response.send({
statusCode: 200,
body: JSON.stringify({
message: 'Init triage PRs function started'
})
});
} catch(err) {
console.error(err);
response.sendStatus(500);
}
});

/**
* Init the PRs of a repository, triggered by an insertion in the "repositories" table
*/
Expand Down
45 changes: 24 additions & 21 deletions functions/src/plugins/triage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class TriageTask extends Task {
this.dispatch([
'issues.labeled',
'issues.unlabeled',
'issues.demilestoned',
'issues.milestoned',
'issues.opened'
], this.checkTriage.bind(this));
Expand Down Expand Up @@ -71,29 +72,31 @@ export class TriageTask extends Task {
}

async checkTriage(context: Context): Promise<void> {
const issue: Github.IssuesGetResponse = context.payload.issue;
const config = await this.getConfig(context);
if(config.disabled) {
return;
}
const {owner, repo} = context.repo();
// getting labels from Github because we might be adding multiple labels at once
const labels = await getGhLabels(context.github, owner, repo, issue.number);
const isL1Triaged = this.isTriaged(config.l1TriageLabels, getLabelsNames(labels));
if(!isL1Triaged) {
if(issue.milestone) {
await this.setMilestone(null, context.github, owner, repo, issue);
if(!context.payload.pull_request && !context.payload.issue.pull_request) {
const issue: Github.IssuesGetResponse = context.payload.issue;
const config = await this.getConfig(context);
if(config.disabled) {
return;
}
} else if(!issue.milestone || issue.milestone.number === config.defaultMilestone || issue.milestone.number === config.needsTriageMilestone) {
const isL2Triaged = this.isTriaged(config.l2TriageLabels || config.triagedLabels, getLabelsNames(labels));
if(isL2Triaged) {
if(!issue.milestone || issue.milestone.number !== config.defaultMilestone) {
await this.setMilestone(config.defaultMilestone, context.github, owner, repo, issue);
const {owner, repo} = context.repo();
// getting labels from Github because we might be adding multiple labels at once
const labels = await getGhLabels(context.github, owner, repo, issue.number);
const isL1Triaged = this.isTriaged(config.l1TriageLabels, getLabelsNames(labels));
if(!isL1Triaged) {
if(issue.milestone) {
await this.setMilestone(null, context.github, owner, repo, issue);
}
} else {
// if it's not triaged, set the "needsTriage" milestone
if(!issue.milestone || issue.milestone.number !== config.needsTriageMilestone) {
await this.setMilestone(config.needsTriageMilestone, context.github, owner, repo, issue);
} else if(!issue.milestone || issue.milestone.number === config.defaultMilestone || issue.milestone.number === config.needsTriageMilestone) {
const isL2Triaged = this.isTriaged(config.l2TriageLabels || config.triagedLabels, getLabelsNames(labels));
if(isL2Triaged) {
if(!issue.milestone || issue.milestone.number !== config.defaultMilestone) {
await this.setMilestone(config.defaultMilestone, context.github, owner, repo, issue);
}
} else {
// if it's not triaged, set the "needsTriage" milestone
if(!issue.milestone || issue.milestone.number !== config.needsTriageMilestone) {
await this.setMilestone(config.needsTriageMilestone, context.github, owner, repo, issue);
}
}
}
}
Expand Down
131 changes: 131 additions & 0 deletions functions/src/plugins/triagePR.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import {Application, Context} from "probot";
import {Task} from "./task";
import {CONFIG_FILE} from "./merge";
import {AdminConfig, AppConfig, appConfig, TriageConfig} from "../default";
import {getGhLabels, getLabelsNames, matchAllOfAny} from "./common";
import Github from '@octokit/rest';

export class TriagePRTask extends Task {
constructor(robot: Application, db: FirebaseFirestore.Firestore) {
super(robot, db);

// PRs are issues for github
this.dispatch([
'pull_request.labeled',
'pull_request.unlabeled',
'issues.demilestoned',
'issues.milestoned',
'issues.opened'
], this.checkTriage.bind(this));
}

async manualInit(): Promise<void> {
this.log('init triage PR');
const adminConfig = await this.admin.doc('config').get();
if(adminConfig.exists && (<AdminConfig>adminConfig.data()).allowInit) {
const github = await this.robot.auth();
const installations = await github.paginate(github.apps.getInstallations({}), pages => pages.data);
await Promise.all(installations.map(async installation => {
const authGithub = await this.robot.auth(installation.id);
const repositories = await authGithub.apps.getInstallationRepositories({});
await Promise.all(repositories.data.repositories.map(async (repository: Github.AppsGetInstallationRepositoriesResponseRepositoriesItem) => {
const context = new Context({payload: {repository}}, authGithub, this.robot.log);
const config = await this.getConfig(context);
if(config.disabled) {
return;
}
const {owner, repo} = context.repo();
const issues = await authGithub.paginate(authGithub.issues.getForRepo({
owner,
repo,
state: 'open',
per_page: 100
}), page => page.data);

issues.forEach(async (issue: Github.IssuesGetForRepoResponseItem) => {
// We only want the PRs, not the issues
if(issue.pull_request) {
const isL1Triaged = this.isTriaged(config.l1TriageLabels, issue.labels.map((label: Github.IssuesGetForRepoResponseItemLabelsItem) => label.name));
if(!isL1Triaged) {
if(issue.milestone) {
await this.setMilestone(null, context.github, owner, repo, issue);
}
} else if(!issue.milestone || issue.milestone.number === config.defaultMilestone || issue.milestone.number === config.needsTriageMilestone) {
const isL2Triaged = this.isTriaged(config.l2TriageLabels || config.triagedLabels, issue.labels.map((label: Github.IssuesGetForRepoResponseItemLabelsItem) => label.name));
if(isL2Triaged) {
if(!issue.milestone || issue.milestone.number !== config.defaultMilestone) {
await this.setMilestone(config.defaultMilestone, context.github, owner, repo, issue);
}
} else {
// if it's not triaged, set the "needsTriage" milestone
if(!issue.milestone || issue.milestone.number !== config.needsTriageMilestone) {
await this.setMilestone(config.needsTriageMilestone, context.github, owner, repo, issue);
}
}
}
}
});
}));
}));
} else {
this.logError(`Manual init is disabled: the value of allowInit is set to false in the admin config database`);
}
}

async checkTriage(context: Context): Promise<void> {
if((context.payload.issue && context.payload.issue.pull_request) || context.payload.pull_request) {
const PR: Github.PullRequestsGetResponse | Github.IssuesGetResponse = context.payload.pull_request || context.payload.issue;
const config = await this.getConfig(context);
if(config.disabled) {
return;
}
const {owner, repo} = context.repo();
// getting labels from Github because we might be adding multiple labels at once
const labels = await getGhLabels(context.github, owner, repo, PR.number);
const isL1Triaged = this.isTriaged(config.l1TriageLabels, getLabelsNames(labels));
if(!isL1Triaged) {
if(PR.milestone) {
await this.setMilestone(null, context.github, owner, repo, PR);
}
} else if(!PR.milestone || PR.milestone.number === config.defaultMilestone || PR.milestone.number === config.needsTriageMilestone) {
const isL2Triaged = this.isTriaged(config.l2TriageLabels || config.triagedLabels, getLabelsNames(labels));
if(isL2Triaged) {
if(!PR.milestone || PR.milestone.number !== config.defaultMilestone) {
await this.setMilestone(config.defaultMilestone, context.github, owner, repo, PR);
}
} else {
// if it's not triaged, set the "needsTriage" milestone
if(!PR.milestone || PR.milestone.number !== config.needsTriageMilestone) {
await this.setMilestone(config.needsTriageMilestone, context.github, owner, repo, PR);
}
}
}
}
}

setMilestone(milestoneNumber: number | null, github: Github, owner: string, repo: string, PR: Github.PullRequestsGetResponse|Github.IssuesGetForRepoResponseItem): Promise<Github.Response<Github.IssuesEditResponse>> {
if(milestoneNumber) {
this.log(`Adding milestone ${milestoneNumber} to PR ${PR.html_url}`);
} else {
this.log(`Removing milestone from PR ${PR.html_url}`);
}
return github.issues.edit({owner, repo, number: PR.number, milestone: milestoneNumber}).catch(err => {
throw err;
});
}

isTriaged(triagedLabels: string[][], currentLabels: string[]): boolean {
return matchAllOfAny(currentLabels, triagedLabels);
}

/**
* Gets the config for the merge plugin from Github or uses default if necessary
*/
async getConfig(context: Context): Promise<TriageConfig> {
const repositoryConfig = await context.config<AppConfig>(CONFIG_FILE, appConfig);
const config = repositoryConfig.triagePR;
config.defaultMilestone = parseInt(config.defaultMilestone, 10);
config.needsTriageMilestone = parseInt(config.needsTriageMilestone, 10);
return config;
}
}
3 changes: 3 additions & 0 deletions functions/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {CommonTask} from "./plugins/common";
import {MergeTask} from "./plugins/merge";
import {TriageTask} from "./plugins/triage";
import {SizeTask} from "./plugins/size";
import {TriagePRTask} from "./plugins/triagePR";

class Stream {
constructor(private store: FirebaseFirestore.Firestore) {
Expand Down Expand Up @@ -93,6 +94,7 @@ export interface Tasks {
commonTask: CommonTask;
mergeTask: MergeTask;
triageTask: TriageTask;
triagePRTask: TriagePRTask;
sizeTask: SizeTask;
}

Expand All @@ -102,6 +104,7 @@ export function registerTasks(robot: Application, store: FirebaseFirestore.Fires
commonTask: new CommonTask(robot, store),
mergeTask: new MergeTask(robot, store),
triageTask: new TriageTask(robot, store),
triagePRTask: new TriagePRTask(robot, store),
sizeTask: new SizeTask(robot, store),
};
}
Expand Down

0 comments on commit c89e411

Please sign in to comment.