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

feat(triage): adding triage PR plugin #33

Merged
merged 1 commit into from
Oct 17, 2018
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
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
22 changes: 21 additions & 1 deletion test/fixtures/angular-robot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ merge:
\n
\nIf 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."

# options for the triage plugin
# options for the triage issues plugin
triage:
# set to true to disable
disabled: false
Expand Down Expand Up @@ -128,3 +128,23 @@ triage:
-
- "type: RFC / Discussion / question"
- "comp: *"

# options for the triage PR plugin
triagePR:
# set to true to disable
disabled: false
# 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: *"