This commit is contained in:
Shelley Vohr 2020-05-21 15:42:22 -07:00 коммит произвёл GitHub
Родитель 517da59a45
Коммит 8bdd43fd97
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
21 изменённых файлов: 775 добавлений и 414 удалений

6
.prettierrc Normal file
Просмотреть файл

@ -0,0 +1,6 @@
{
"trailingComma": "all",
"tabWidth": 2,
"singleQuote": true,
"endOfLine": "lf"
}

Просмотреть файл

@ -9,7 +9,8 @@
"scripts": {
"build": "tsc",
"start": "probot run ./lib/index.js",
"lint": "tslint --project '.'",
"prettier:write": "prettier --write '**/*.ts'",
"lint": "prettier --check '**/*.ts'",
"test": "jest --testPathIgnorePatterns=/working/ --testPathIgnorePatterns=/node_modules/",
"postinstall": "tsc"
},
@ -34,6 +35,7 @@
"@types/node-fetch": "^2.5.4",
"@types/sinon": "^5.0.5",
"jest": "^23.6.0",
"prettier": "^2.0.5",
"sinon": "^7.5.0",
"smee-client": "^1.0.1",
"ts-jest": "^23.10.4",

Просмотреть файл

@ -3,7 +3,9 @@ import * as commands from '../src/constants';
describe('commands', () => {
it('should all be unique', () => {
const commandsRecord: Record<string, string> = commands as any;
const allCommands = Object.keys(commandsRecord).map(key => commandsRecord[key]).sort();
const allCommands = Object.keys(commandsRecord)
.map((key) => commandsRecord[key])
.sort();
const uniqueCommands = Array.from(new Set(allCommands)).sort();
expect(allCommands).toStrictEqual(uniqueCommands);
});

Просмотреть файл

@ -2,7 +2,10 @@ jest.mock('request');
import { Application } from 'probot';
import * as utils from '../src/utils';
import { backportToBranch, backportToLabel } from '../src/operations/backport-to-location';
import {
backportToBranch,
backportToLabel,
} from '../src/operations/backport-to-location';
import { updateManualBackport } from '../src/operations/update-manual-backport';
import { ProbotHandler } from '../src/index';
@ -37,64 +40,76 @@ describe('trop', () => {
github = {
repos: {
getContents: jest.fn().mockReturnValue(Promise.resolve({
data: { content: Buffer.from('watchedProject:\n name: Radar\nauthorizedUsers:\n - codebytere').toString('base64') },
})),
getContents: jest.fn().mockReturnValue(
Promise.resolve({
data: {
content: Buffer.from(
'watchedProject:\n name: Radar\nauthorizedUsers:\n - codebytere',
).toString('base64'),
},
}),
),
getBranch: jest.fn().mockReturnValue(Promise.resolve()),
listBranches: jest.fn().mockReturnValue(Promise.resolve({
data: [
{ name: '8-x-y' },
{ name: '7-1-x' },
],
})),
listBranches: jest.fn().mockReturnValue(
Promise.resolve({
data: [{ name: '8-x-y' }, { name: '7-1-x' }],
}),
),
},
git: {
deleteRef: jest.fn().mockReturnValue(Promise.resolve()),
},
pulls: {
get: jest.fn().mockReturnValue(Promise.resolve({
data: {
merged: true,
base: {
repo: {
name: 'test',
owner: {
login: 'codebytere',
get: jest.fn().mockReturnValue(
Promise.resolve({
data: {
merged: true,
base: {
repo: {
name: 'test',
owner: {
login: 'codebytere',
},
},
},
},
head: {
sha: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
},
labels: [
{
url: 'my_cool_url',
name: 'target/X-X-X',
color: 'fc2929',
head: {
sha: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
},
],
},
})),
labels: [
{
url: 'my_cool_url',
name: 'target/X-X-X',
color: 'fc2929',
},
],
},
}),
),
},
issues: {
addLabels: jest.fn().mockReturnValue(Promise.resolve({})),
removeLabel: jest.fn().mockReturnValue(Promise.resolve({})),
createLabel: jest.fn().mockReturnValue(Promise.resolve({})),
createComment: jest.fn().mockReturnValue(Promise.resolve({})),
listLabelsOnIssue: jest.fn().mockReturnValue(Promise.resolve({
data: [
{
id: 208045946,
url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug',
name: 'bug',
description: 'Something isn\'t working',
color: 'f29513',
},
],
})),
listLabelsOnIssue: jest.fn().mockReturnValue(
Promise.resolve({
data: [
{
id: 208045946,
url:
'https://api.github.com/repos/octocat/Hello-World/labels/bug',
name: 'bug',
description: "Something isn't working",
color: 'f29513',
},
],
}),
),
},
checks: {
listForRef: jest.fn().mockReturnValue(Promise.resolve({ data: { check_runs: [] } })),
listForRef: jest
.fn()
.mockReturnValue(Promise.resolve({ data: { check_runs: [] } })),
},
};
@ -119,7 +134,9 @@ describe('trop', () => {
});
it('does not trigger the backport on comment if the PR is not merged', async () => {
github.pulls.get = jest.fn().mockReturnValue(Promise.resolve({ data: { merged: false } }));
github.pulls.get = jest
.fn()
.mockReturnValue(Promise.resolve({ data: { merged: false } }));
await robot.receive(issueCommentBackportCreatedEvent);
@ -145,7 +162,9 @@ describe('trop', () => {
});
it('does not trigger the backport on comment to a targeted branch if the branch does not exist', async () => {
github.repos.getBranch = jest.fn().mockReturnValue(Promise.reject(new Error('404')));
github.repos.getBranch = jest
.fn()
.mockReturnValue(Promise.reject(new Error('404')));
await robot.receive(issueCommentBackportToCreatedEvent);
expect(github.pulls.get).toHaveBeenCalled();

Просмотреть файл

@ -25,19 +25,23 @@ describe('runner', () => {
describe('initRepo()', () => {
it('should clone a github repository', async () => {
const dir = saveDir(await initRepo({
slug: 'electron/trop',
accessToken: '',
}));
const dir = saveDir(
await initRepo({
slug: 'electron/trop',
accessToken: '',
}),
);
expect(await fs.pathExists(dir)).toBe(true);
expect(await fs.pathExists(path.resolve(dir, '.git'))).toBe(true);
});
it('should fail if the github repository does not exist', async () => {
await expect(initRepo({
slug: 'electron/this-is-not-trop',
accessToken: '',
})).rejects.toBeTruthy();
await expect(
initRepo({
slug: 'electron/this-is-not-trop',
accessToken: '',
}),
).rejects.toBeTruthy();
});
});
@ -59,29 +63,40 @@ describe('runner', () => {
it('should set new remotes correctly', async () => {
await setupRemotes({
dir,
remotes: [{
name: 'origin',
value: 'https://github.com/electron/clerk.git',
}, {
name: 'secondary',
value: 'https://github.com/electron/trop.git',
}],
remotes: [
{
name: 'origin',
value: 'https://github.com/electron/clerk.git',
},
{
name: 'secondary',
value: 'https://github.com/electron/trop.git',
},
],
});
const git = simpleGit(dir);
const remotes = await git.raw(['remote', '-v']);
const parsedRemotes = remotes.trim()
.replace(/ +/g, ' ').replace(/\t/g, ' ')
.replace(/ \(fetch\)/g, '').replace(/ \(push\)/g, '')
.split(/\r?\n/g).map(line => line.trim().split(' '));
const parsedRemotes = remotes
.trim()
.replace(/ +/g, ' ')
.replace(/\t/g, ' ')
.replace(/ \(fetch\)/g, '')
.replace(/ \(push\)/g, '')
.split(/\r?\n/g)
.map((line) => line.trim().split(' '));
expect(parsedRemotes.length).toBe(4);
for (const remote of parsedRemotes) {
expect(remote.length).toBe(2);
expect(['origin', 'secondary']).toContain(remote[0]);
if (remote[0] === 'origin') {
expect(remote[1].endsWith('github.com/electron/clerk.git')).toBeTruthy();
expect(
remote[1].endsWith('github.com/electron/clerk.git'),
).toBeTruthy();
} else {
expect(remote[1].endsWith('github.com/electron/trop.git')).toBeTruthy();
expect(
remote[1].endsWith('github.com/electron/trop.git'),
).toBeTruthy();
}
}
});

Просмотреть файл

@ -11,7 +11,11 @@ const waitForEvent = (emitter: EventEmitter, event: string) => {
});
};
const delayedEvent = async (emitter: EventEmitter, event: string, fn: () => Promise<void>) => {
const delayedEvent = async (
emitter: EventEmitter,
event: string,
fn: () => Promise<void>,
) => {
const waiter = waitForEvent(emitter, event);
await fn();
await waiter;
@ -22,7 +26,12 @@ const fakeTask = (name: string) => {
name,
taskRunner: sinon.stub().returns(Promise.resolve()),
errorHandler: sinon.stub().returns(Promise.resolve()),
args: () => [name, namedArgs.taskRunner, namedArgs.errorHandler] as [string, () => Promise<void>, () => Promise<void>],
args: () =>
[name, namedArgs.taskRunner, namedArgs.errorHandler] as [
string,
() => Promise<void>,
() => Promise<void>,
],
};
return namedArgs;
};
@ -74,7 +83,7 @@ describe('ExecutionQueue', () => {
expect(task2.taskRunner.callCount).toBe(1);
});
it('should run the next task if the current task fails and it\'s error handler fails', async () => {
it("should run the next task if the current task fails and it's error handler fails", async () => {
const q = new ExecutionQueue();
const task = fakeTask('test');

Просмотреть файл

@ -16,7 +16,11 @@ export class ExecutionQueue extends EventEmitter {
super();
}
public enterQueue = (identifier: string, fn: Executor, errorFn: ErrorExecutor) => {
public enterQueue = (
identifier: string,
fn: Executor,
errorFn: ErrorExecutor,
) => {
if (this.activeIdents.has(identifier)) return;
this.activeIdents.add(identifier);
@ -26,24 +30,30 @@ export class ExecutionQueue extends EventEmitter {
} else {
this.run([identifier, fn, errorFn]);
}
}
};
private run = (fns: [string, Executor, ErrorExecutor]) => {
this.active += 1;
fns[1]().then(() => this.runNext(fns[0])).catch((err: any) => {
if (!process.env.SPEC_RUNNING) {
console.error(err);
}
fns[2](err)
.catch((e) => {
if (!process.env.SPEC_RUNNING) console.error(e);
})
.then(() => this.runNext(fns[0]));
});
}
fns[1]()
.then(() => this.runNext(fns[0]))
.catch((err: any) => {
if (!process.env.SPEC_RUNNING) {
console.error(err);
}
fns[2](err)
.catch((e) => {
if (!process.env.SPEC_RUNNING) console.error(e);
})
.then(() => this.runNext(fns[0]));
});
};
private runNext = (lastIdent: string) => {
log('runNext', LogLevel.INFO, `Running queue item with identifier ${lastIdent}`);
log(
'runNext',
LogLevel.INFO,
`Running queue item with identifier ${lastIdent}`,
);
this.activeIdents.delete(lastIdent);
this.active -= 1;
@ -52,7 +62,7 @@ export class ExecutionQueue extends EventEmitter {
} else {
this.emit('empty');
}
}
};
}
export default new ExecutionQueue();

Просмотреть файл

@ -2,4 +2,5 @@ export const CHECK_PREFIX = 'Backportable? - ';
export const NUM_SUPPORTED_VERSIONS = 4;
export const SKIP_CHECK_LABEL = process.env.SKIP_CHECK_LABEL || 'backport-check-skip';
export const SKIP_CHECK_LABEL =
process.env.SKIP_CHECK_LABEL || 'backport-check-skip';

Просмотреть файл

@ -6,8 +6,14 @@ import { TropConfig } from './interfaces';
import { CHECK_PREFIX, SKIP_CHECK_LABEL } from './constants';
import { getEnvVar } from './utils/env-util';
import { PRChange, PRStatus, BackportPurpose, CheckRunStatus } from './enums';
import { ChecksListForRefResponseCheckRunsItem, PullsGetResponse } from '@octokit/rest';
import { backportToLabel, backportToBranch } from './operations/backport-to-location';
import {
ChecksListForRefResponseCheckRunsItem,
PullsGetResponse,
} from '@octokit/rest';
import {
backportToLabel,
backportToBranch,
} from './operations/backport-to-location';
import { updateManualBackport } from './operations/update-manual-backport';
import { getSupportedBranches, getBackportPattern } from './utils/branch-util';
import { updateBackportValidityCheck } from './utils/checks-util';
@ -15,7 +21,9 @@ import { updateBackportValidityCheck } from './utils/checks-util';
const probotHandler = async (robot: Application) => {
const labelMergedPRs = async (context: Context, pr: PullsGetResponse) => {
for (const label of pr.labels) {
const targetBranch = label.name.match(/^(\d)+-(?:(?:[0-9]+-x$)|(?:x+-y$))$/);
const targetBranch = label.name.match(
/^(\d)+-(?:(?:[0-9]+-x$)|(?:x+-y$))$/,
);
if (targetBranch && targetBranch[0]) {
await labelMergedPR(context, pr, label.name);
}
@ -30,49 +38,57 @@ const probotHandler = async (robot: Application) => {
};
const runCheck = async (context: Context, pr: PullsGetResponse) => {
const allChecks = await context.github.checks.listForRef(context.repo({
ref: pr.head.sha,
per_page: 100,
}));
const checkRuns = allChecks.data.check_runs.filter(run => run.name.startsWith(CHECK_PREFIX));
const allChecks = await context.github.checks.listForRef(
context.repo({
ref: pr.head.sha,
per_page: 100,
}),
);
const checkRuns = allChecks.data.check_runs.filter((run) =>
run.name.startsWith(CHECK_PREFIX),
);
for (const label of pr.labels) {
if (!label.name.startsWith(PRStatus.TARGET)) continue;
const targetBranch = labelToTargetBranch(label, PRStatus.TARGET);
const runName = `${CHECK_PREFIX}${targetBranch}`;
const existing = checkRuns.find(run => run.name === runName);
const existing = checkRuns.find((run) => run.name === runName);
if (existing) {
if (existing.conclusion !== 'neutral') continue;
await context.github.checks.update(context.repo({
name: existing.name,
check_run_id: existing.id,
status: 'queued' as 'queued',
}));
await context.github.checks.update(
context.repo({
name: existing.name,
check_run_id: existing.id,
status: 'queued' as 'queued',
}),
);
} else {
await context.github.checks.create(context.repo({
name: runName,
head_sha: pr.head.sha,
status: 'queued' as 'queued',
details_url: 'https://github.com/electron/trop',
}));
await context.github.checks.create(
context.repo({
name: runName,
head_sha: pr.head.sha,
status: 'queued' as 'queued',
details_url: 'https://github.com/electron/trop',
}),
);
}
await backportImpl(
robot,
context,
targetBranch,
BackportPurpose.Check,
);
await backportImpl(robot, context, targetBranch, BackportPurpose.Check);
}
for (const checkRun of checkRuns) {
if (!pr.labels.find(
label => label.name === `${PRStatus.TARGET}${checkRun.name.replace(CHECK_PREFIX, '')}`,
)) {
if (
!pr.labels.find(
(label) =>
label.name ===
`${PRStatus.TARGET}${checkRun.name.replace(CHECK_PREFIX, '')}`,
)
) {
await updateBackportValidityCheck(context, checkRun, {
title: 'Cancelled',
summary: 'This trop check was cancelled and can be ignored as this \
summary:
'This trop check was cancelled and can be ignored as this \
PR is no longer targeting this branch for a backport',
conclusion: CheckRunStatus.NEUTRAL,
});
@ -95,9 +111,11 @@ const probotHandler = async (robot: Application) => {
const backportPattern = getBackportPattern();
// Check if this PR is a manual backport of another PR.
let match: RegExpExecArray | null;
while (match = backportPattern.exec(pr.body)) {
while ((match = backportPattern.exec(pr.body))) {
// This might be the first or second capture group depending on if it's a link or not.
backportNumbers.push(match[1] ? parseInt(match[1], 10) : parseInt(match[2], 10));
backportNumbers.push(
match[1] ? parseInt(match[1], 10) : parseInt(match[2], 10),
);
}
}
@ -121,39 +139,55 @@ const probotHandler = async (robot: Application) => {
const pr = context.payload.pull_request;
// Only check for manual backports when a new PR is opened or if the PR body is edited.
if (oldPRNumbers.length > 0 && ['opened', 'edited'].includes(context.payload.action)) {
if (
oldPRNumbers.length > 0 &&
['opened', 'edited'].includes(context.payload.action)
) {
for (const oldPRNumber of oldPRNumbers) {
robot.log(`Updating original backport at ${oldPRNumber} for ${pr.number}`);
robot.log(
`Updating original backport at ${oldPRNumber} for ${pr.number}`,
);
await updateManualBackport(context, PRChange.OPEN, oldPRNumber);
}
}
// Check if the PR is going to master, if it's not check if it's correctly
// tagged as a backport of a PR that has already been merged into master.
const { data: allChecks } = await context.github.checks.listForRef(context.repo({
ref: pr.head.sha,
per_page: 100,
}));
let checkRun = allChecks.check_runs.find(run => run.name === VALID_BACKPORT_CHECK_NAME);
const { data: allChecks } = await context.github.checks.listForRef(
context.repo({
ref: pr.head.sha,
per_page: 100,
}),
);
let checkRun = allChecks.check_runs.find(
(run) => run.name === VALID_BACKPORT_CHECK_NAME,
);
if (pr.base.ref !== 'master') {
if (!checkRun) {
robot.log(`Queueing new check run for #${pr.number}`);
checkRun = (await context.github.checks.create(context.repo({
name: VALID_BACKPORT_CHECK_NAME,
head_sha: pr.head.sha,
status: 'queued' as 'queued',
details_url: 'https://github.com/electron/trop',
}))).data as any as ChecksListForRefResponseCheckRunsItem;
checkRun = ((
await context.github.checks.create(
context.repo({
name: VALID_BACKPORT_CHECK_NAME,
head_sha: pr.head.sha,
status: 'queued' as 'queued',
details_url: 'https://github.com/electron/trop',
}),
)
).data as any) as ChecksListForRefResponseCheckRunsItem;
}
// If a branch is targeting something that isn't master it might not be a backport;
// allow for a label to skip backport validity check for these branches.
if (await labelExistsOnPR(context, pr.number, SKIP_CHECK_LABEL)) {
robot.log(`#${pr.number} is labeled with ${SKIP_CHECK_LABEL} - skipping backport validation check`);
robot.log(
`#${pr.number} is labeled with ${SKIP_CHECK_LABEL} - skipping backport validation check`,
);
await updateBackportValidityCheck(context, checkRun, {
title: 'Backport Check Skipped',
summary: 'This PR is not a backport - skip backport validation check',
summary:
'This PR is not a backport - skip backport validation check',
conclusion: CheckRunStatus.NEUTRAL,
});
return;
@ -171,45 +205,63 @@ const probotHandler = async (robot: Application) => {
// There are several types of PRs which might not target master yet which are
// inherently valid; e.g roller-bot PRs. Check for and allow those here.
if (oldPRNumbers.length === 0) {
robot.log(`#${pr.number} does not have backport numbers - checking fast track status`);
robot.log(
`#${pr.number} does not have backport numbers - checking fast track status`,
);
if (
!FASTTRACK_PREFIXES.some(pre => pr.title.startsWith(pre)) &&
!FASTTRACK_USERS.some(user => pr.user.login === user) &&
!FASTTRACK_LABELS.some(label => pr.labels.some((prLabel: any) => prLabel.name === label))
!FASTTRACK_PREFIXES.some((pre) => pr.title.startsWith(pre)) &&
!FASTTRACK_USERS.some((user) => pr.user.login === user) &&
!FASTTRACK_LABELS.some((label) =>
pr.labels.some((prLabel: any) => prLabel.name === label),
)
) {
robot.log(`#${pr.number} is not a fast track PR - marking check run as failed`);
robot.log(
`#${pr.number} is not a fast track PR - marking check run as failed`,
);
await updateBackportValidityCheck(context, checkRun, {
title: 'Invalid Backport',
summary: 'This PR is targeting a branch that is not master but is missing a "Backport of #{N}" declaration. \
summary:
'This PR is targeting a branch that is not master but is missing a "Backport of #{N}" declaration. \
Check out the trop documentation linked below for more information.',
conclusion: CheckRunStatus.FAILURE,
});
} else {
robot.log(`#${pr.number} is a fast track PR - marking check run as succeeded`);
robot.log(
`#${pr.number} is a fast track PR - marking check run as succeeded`,
);
await updateBackportValidityCheck(context, checkRun, {
title: 'Valid Backport',
summary: 'This PR is targeting a branch that is not master but a designated fast-track backport which does \
summary:
'This PR is targeting a branch that is not master but a designated fast-track backport which does \
not require a manual backport number.',
conclusion: CheckRunStatus.SUCCESS,
});
}
} else {
robot.log(`#${pr.number} has backport numbers - checking their validity now`);
robot.log(
`#${pr.number} has backport numbers - checking their validity now`,
);
const supported = await getSupportedBranches(context);
for (const oldPRNumber of oldPRNumbers) {
robot.log(`Checking validity of #${oldPRNumber}`);
const oldPR = (await context.github.pulls.get(context.repo({
pull_number: oldPRNumber,
}))).data;
const oldPR = (
await context.github.pulls.get(
context.repo({
pull_number: oldPRNumber,
}),
)
).data;
// The current PR is only valid if the PR it is backporting
// was merged to master or to a supported release branch.
if (!['master', ...supported].includes(oldPR.base.ref)) {
const cause = 'the PR that it is backporting was not targeting the master branch.';
const cause =
'the PR that it is backporting was not targeting the master branch.';
failureMap.set(oldPRNumber, cause);
} else if (!oldPR.merged) {
const cause = 'the PR that this is backporting has not been merged yet.';
const cause =
'the PR that this is backporting has not been merged yet.';
failureMap.set(oldPRNumber, cause);
}
}
@ -217,10 +269,18 @@ const probotHandler = async (robot: Application) => {
for (const oldPRNumber of oldPRNumbers) {
if (failureMap.has(oldPRNumber)) {
robot.log(`#${pr.number} is targeting a branch that is not master - ${failureMap.get(oldPRNumber)}`);
robot.log(
`#${
pr.number
} is targeting a branch that is not master - ${failureMap.get(
oldPRNumber,
)}`,
);
await updateBackportValidityCheck(context, checkRun, {
title: 'Invalid Backport',
summary: `This PR is targeting a branch that is not master but ${failureMap.get(oldPRNumber)}`,
summary: `This PR is targeting a branch that is not master but ${failureMap.get(
oldPRNumber,
)}`,
conclusion: CheckRunStatus.FAILURE,
});
} else {
@ -235,7 +295,9 @@ const probotHandler = async (robot: Application) => {
} else if (checkRun) {
// If we're somehow targeting master and have a check run,
// we mark this check as cancelled.
robot.log(`#${pr.number} is targeting 'master' and is not a backport - marking as cancelled`);
robot.log(
`#${pr.number} is targeting 'master' and is not a backport - marking as cancelled`,
);
await updateBackportValidityCheck(context, checkRun, {
title: 'Cancelled',
summary: 'This PR is targeting `master` and is not a backport',
@ -245,7 +307,10 @@ const probotHandler = async (robot: Application) => {
// Only run the backportable checks on "opened" and "synchronize"
// an "edited" change can not impact backportability.
if (context.payload.action === 'edited' || context.payload.action === 'synchronize') {
if (
context.payload.action === 'edited' ||
context.payload.action === 'synchronize'
) {
maybeRunCheck(context);
}
},
@ -276,12 +341,16 @@ const probotHandler = async (robot: Application) => {
robot.log(`Deleting base branch: ${pr.base.ref}`);
try {
await context.github.git.deleteRef(context.repo({ ref: pr.base.ref }));
await context.github.git.deleteRef(
context.repo({ ref: pr.base.ref }),
);
} catch (e) {
robot.log('Failed to delete base branch: ', e);
}
} else {
robot.log(`Backporting #${pr.number} to all branches specified by labels`);
robot.log(
`Backporting #${pr.number} to all branches specified by labels`,
);
backportAllLabels(context, pr);
}
}
@ -292,13 +361,15 @@ const probotHandler = async (robot: Application) => {
// Manually trigger backporting process on trigger comment phrase.
robot.on('issue_comment.created', async (context: Context) => {
const payload = context.payload;
const config = await context.config<TropConfig>('config.yml') as TropConfig;
const config = (await context.config<TropConfig>(
'config.yml',
)) as TropConfig;
if (!config || !Array.isArray(config.authorizedUsers)) {
robot.log('missing or invalid config', config);
return;
}
const isPullRequest = (issue: { number: number, html_url: string }) =>
const isPullRequest = (issue: { number: number; html_url: string }) =>
issue.html_url.endsWith(`/pull/${issue.number}`);
if (!isPullRequest(payload.issue)) return;
@ -307,95 +378,127 @@ const probotHandler = async (robot: Application) => {
if (!cmd.startsWith(TROP_COMMAND_PREFIX)) return;
if (!config.authorizedUsers.includes(payload.comment.user.login)) {
robot.log(`@${payload.comment.user.login} is not authorized to run PR backports - stopping`);
await context.github.issues.createComment(context.repo({
issue_number: payload.issue.number,
body: `@${payload.comment.user.login} is not authorized to run PR backports.`,
}));
robot.log(
`@${payload.comment.user.login} is not authorized to run PR backports - stopping`,
);
await context.github.issues.createComment(
context.repo({
issue_number: payload.issue.number,
body: `@${payload.comment.user.login} is not authorized to run PR backports.`,
}),
);
return;
}
const actualCmd = cmd.substr(TROP_COMMAND_PREFIX.length);
const actions = [{
name: 'backport sanity checker',
command: /^run backport/,
execute: async () => {
const pr = (await context.github.pulls.get(
context.repo({ pull_number: payload.issue.number }))
).data;
if (!pr.merged) {
await context.github.issues.createComment(context.repo({
issue_number: payload.issue.number,
body: 'This PR has not been merged yet, and cannot be backported.',
}));
return false;
}
return true;
},
}, {
name: 'backport automatically',
command: /^run backport$/,
execute: async () => {
const pr = (await context.github.pulls.get(
context.repo({ pull_number: payload.issue.number }))
).data as any;
await context.github.issues.createComment(context.repo({
body: 'The backport process for this PR has been manually initiated, here we go! :D',
issue_number: payload.issue.number,
}));
backportAllLabels(context, pr);
return true;
},
}, {
name: 'backport to branch',
command: /^run backport-to ([^\s:]+)/,
execute: async (targetBranches: string) => {
const branches = targetBranches.split(',');
for (const branch of branches) {
robot.log(`Initiatating backport to ${branch} from 'backport-to' comment`);
if (!(branch.trim())) continue;
const pr = (await context.github.pulls.get(
context.repo({ pull_number: payload.issue.number }))
const actions = [
{
name: 'backport sanity checker',
command: /^run backport/,
execute: async () => {
const pr = (
await context.github.pulls.get(
context.repo({ pull_number: payload.issue.number }),
)
).data;
try {
(await context.github.repos.getBranch(context.repo({ branch })));
} catch (err) {
await context.github.issues.createComment(context.repo({
body: `The branch you provided "${branch}" does not appear to exist :cry:`,
issue_number: payload.issue.number,
}));
return true;
}
// Optionally disallow backports to EOL branches
const noEOLSupport = getEnvVar('NO_EOL_SUPPORT', '');
if (noEOLSupport) {
const supported = await getSupportedBranches(context);
if (!supported.includes(branch)) {
robot.log(`${branch} is no longer supported - no backport will be initiated`);
await context.github.issues.createComment(context.repo({
body: `${branch} is no longer supported - no backport will be initiated.`,
if (!pr.merged) {
await context.github.issues.createComment(
context.repo({
issue_number: payload.issue.number,
}));
return false;
}
body:
'This PR has not been merged yet, and cannot be backported.',
}),
);
return false;
}
robot.log(`Initiating manual backport process for #${payload.issue.number} to ${branch}`);
await context.github.issues.createComment(context.repo({
body: `The backport process for this PR has been manually initiated -
sending your commits to "${branch}"!`,
issue_number: payload.issue.number,
}));
context.payload.pull_request = context.payload.pull_request || pr;
backportToBranch(robot, context, branch);
}
return true;
return true;
},
},
}];
{
name: 'backport automatically',
command: /^run backport$/,
execute: async () => {
const pr = (
await context.github.pulls.get(
context.repo({ pull_number: payload.issue.number }),
)
).data as any;
await context.github.issues.createComment(
context.repo({
body:
'The backport process for this PR has been manually initiated, here we go! :D',
issue_number: payload.issue.number,
}),
);
backportAllLabels(context, pr);
return true;
},
},
{
name: 'backport to branch',
command: /^run backport-to ([^\s:]+)/,
execute: async (targetBranches: string) => {
const branches = targetBranches.split(',');
for (const branch of branches) {
robot.log(
`Initiatating backport to ${branch} from 'backport-to' comment`,
);
if (!branch.trim()) continue;
const pr = (
await context.github.pulls.get(
context.repo({ pull_number: payload.issue.number }),
)
).data;
try {
await context.github.repos.getBranch(context.repo({ branch }));
} catch (err) {
await context.github.issues.createComment(
context.repo({
body: `The branch you provided "${branch}" does not appear to exist :cry:`,
issue_number: payload.issue.number,
}),
);
return true;
}
// Optionally disallow backports to EOL branches
const noEOLSupport = getEnvVar('NO_EOL_SUPPORT', '');
if (noEOLSupport) {
const supported = await getSupportedBranches(context);
if (!supported.includes(branch)) {
robot.log(
`${branch} is no longer supported - no backport will be initiated`,
);
await context.github.issues.createComment(
context.repo({
body: `${branch} is no longer supported - no backport will be initiated.`,
issue_number: payload.issue.number,
}),
);
return false;
}
}
robot.log(
`Initiating manual backport process for #${payload.issue.number} to ${branch}`,
);
await context.github.issues.createComment(
context.repo({
body: `The backport process for this PR has been manually initiated -
sending your commits to "${branch}"!`,
issue_number: payload.issue.number,
}),
);
context.payload.pull_request = context.payload.pull_request || pr;
backportToBranch(robot, context, branch);
}
return true;
},
},
];
for (const action of actions) {
const match = actualCmd.match(action.command);
@ -404,7 +507,7 @@ sending your commits to "${branch}"!`,
robot.log(`running action: ${action.name} for comment`);
// @ts-ignore (false positive on next line arg count)
if (!await action.execute(...match.slice(1))) {
if (!(await action.execute(...match.slice(1)))) {
robot.log(`${action.name} failed, stopping responder chain`);
break;
}

Просмотреть файл

@ -9,8 +9,8 @@ export interface TropConfig {
export interface RemotesOptions {
dir: string;
remotes: {
name: string,
value: string,
name: string;
value: string;
}[];
}

Просмотреть файл

@ -15,14 +15,21 @@ import { LogLevel } from '../enums';
* @returns {Object} - an object containing the repo initialization directory
*/
export const backportCommitsToBranch = async (options: BackportOptions) => {
log('backportCommitsToBranch', LogLevel.INFO, `Backporting ${options.patches.length} commits to ${options.targetBranch}`);
log(
'backportCommitsToBranch',
LogLevel.INFO,
`Backporting ${options.patches.length} commits to ${options.targetBranch}`,
);
const git = simpleGit(options.dir);
// Create branch to cherry-pick the commits to.
await git.checkout(`target_repo/${options.targetBranch}`);
await git.pull('target_repo', options.targetBranch);
await git.checkoutBranch(options.tempBranch, `target_repo/${options.targetBranch}`);
await git.checkoutBranch(
options.tempBranch,
`target_repo/${options.targetBranch}`,
);
// Cherry pick the commits to be backported.
const patchPath = `${options.dir}.patch`;

Просмотреть файл

@ -17,16 +17,28 @@ export const backportToLabel = async (
context: Context,
label: PullsGetResponseLabelsItem,
) => {
log('backportToLabel', LogLevel.INFO, `Executing backport to branch from label ${label}`);
log(
'backportToLabel',
LogLevel.INFO,
`Executing backport to branch from label ${label}`,
);
if (!label.name.startsWith(PRStatus.TARGET)) {
log('backportToLabel', LogLevel.ERROR, `Label '${label.name}' does not begin with '${PRStatus.TARGET}'`);
log(
'backportToLabel',
LogLevel.ERROR,
`Label '${label.name}' does not begin with '${PRStatus.TARGET}'`,
);
return;
}
const targetBranch = labelUtils.labelToTargetBranch(label, PRStatus.TARGET);
if (!targetBranch) {
log('backportToLabel', LogLevel.WARN, 'No target branch specified - aborting backport process');
log(
'backportToLabel',
LogLevel.WARN,
'No target branch specified - aborting backport process',
);
return;
}
@ -54,7 +66,11 @@ export const backportToBranch = async (
context: Context,
targetBranch: string,
) => {
log('backportToLabel', LogLevel.INFO, `Executing backport to branch '${targetBranch}'`);
log(
'backportToLabel',
LogLevel.INFO,
`Executing backport to branch '${targetBranch}'`,
);
const labelToRemove = undefined;
const labelToAdd = PRStatus.IN_FLIGHT + targetBranch;

Просмотреть файл

@ -21,15 +21,25 @@ export const updateManualBackport = async (
let labelToRemove;
let labelToAdd;
log('updateManualBackport', LogLevel.INFO, `Updating backport of ${oldPRNumber} to ${pr.base.ref}`);
log(
'updateManualBackport',
LogLevel.INFO,
`Updating backport of ${oldPRNumber} to ${pr.base.ref}`,
);
if (type === PRChange.OPEN) {
log('updateManualBackport', LogLevel.INFO, `New manual backport opened at #${pr.number}`);
log(
'updateManualBackport',
LogLevel.INFO,
`New manual backport opened at #${pr.number}`,
);
labelToAdd = PRStatus.IN_FLIGHT + pr.base.ref;
labelToRemove = PRStatus.NEEDS_MANUAL + pr.base.ref;
if (!await labelUtils.labelExistsOnPR(context, oldPRNumber, labelToRemove)) {
if (
!(await labelUtils.labelExistsOnPR(context, oldPRNumber, labelToRemove))
) {
labelToRemove = PRStatus.TARGET + pr.base.ref;
}
@ -39,23 +49,33 @@ please check out #${pr.number}`;
// TODO(codebytere): Once probot updates to @octokit/rest@16 we can use .paginate to
// get all the comments properly, for now 100 should do
const { data: existingComments } = await context.github.issues.listComments(context.repo({
issue_number: oldPRNumber,
per_page: 100,
}));
const { data: existingComments } = await context.github.issues.listComments(
context.repo({
issue_number: oldPRNumber,
per_page: 100,
}),
);
// We should only comment if there is not a previous existing comment
const shouldComment = !existingComments.some(comment => comment.body === commentBody);
const shouldComment = !existingComments.some(
(comment) => comment.body === commentBody,
);
if (shouldComment) {
// Comment on the original PR with the manual backport link
await context.github.issues.createComment(context.repo({
issue_number: oldPRNumber,
body: commentBody,
}));
await context.github.issues.createComment(
context.repo({
issue_number: oldPRNumber,
body: commentBody,
}),
);
}
} else {
log('updateManualBackport', LogLevel.INFO, `Backport of ${oldPRNumber} at #${pr.number} merged to ${pr.base.ref}`);
log(
'updateManualBackport',
LogLevel.INFO,
`Backport of ${oldPRNumber} at #${pr.number} merged to ${pr.base.ref}`,
);
labelToRemove = PRStatus.IN_FLIGHT + pr.base.ref;
labelToAdd = PRStatus.MERGED + pr.base.ref;

Просмотреть файл

@ -27,15 +27,25 @@ import { log } from './utils/log-util';
const makeQueue: IQueue = require('queue');
const { parse: parseDiff } = require('what-the-diff');
export const labelMergedPR = async (context: Context, pr: PullsGetResponse, targetBranch: String) => {
log('labelMergedPR', LogLevel.INFO, `Labeling original PRs for PR at #${pr.number}`);
export const labelMergedPR = async (
context: Context,
pr: PullsGetResponse,
targetBranch: String,
) => {
log(
'labelMergedPR',
LogLevel.INFO,
`Labeling original PRs for PR at #${pr.number}`,
);
const backportNumbers: number[] = [];
let match: RegExpExecArray | null;
const backportPattern = getBackportPattern();
while (match = backportPattern.exec(pr.body)) {
while ((match = backportPattern.exec(pr.body))) {
// This might be the first or second capture group depending on if it's a link or not.
backportNumbers.push(match[1] ? parseInt(match[1], 10) : parseInt(match[2], 10));
backportNumbers.push(
match[1] ? parseInt(match[1], 10) : parseInt(match[2], 10),
);
}
for (const prNumber of backportNumbers) {
@ -48,10 +58,16 @@ export const labelMergedPR = async (context: Context, pr: PullsGetResponse, targ
};
const checkUserHasWriteAccess = async (context: Context, user: string) => {
log('checkUserHasWriteAccess', LogLevel.INFO, `Checking whether ${user} has write access`);
log(
'checkUserHasWriteAccess',
LogLevel.INFO,
`Checking whether ${user} has write access`,
);
const params = context.repo({ username: user });
const { data: userInfo } = await context.github.repos.getCollaboratorPermissionLevel(params);
const {
data: userInfo,
} = await context.github.repos.getCollaboratorPermissionLevel(params);
// Possible values for the permission key: 'admin', 'write', 'read', 'none'.
// In order for the user's review to count, they must be at least 'write'.
@ -59,13 +75,20 @@ const checkUserHasWriteAccess = async (context: Context, user: string) => {
};
const createBackportComment = (pr: PullsGetResponse) => {
log('createBackportComment', LogLevel.INFO, `Creating backport comment for #${pr.number}`);
log(
'createBackportComment',
LogLevel.INFO,
`Creating backport comment for #${pr.number}`,
);
let body = `Backport of #${pr.number}\n\nSee that PR for details.`;
const onelineMatch = pr.body.match(/(?:(?:\r?\n)|^)notes: (.+?)(?:(?:\r?\n)|$)/gi);
const multilineMatch =
pr.body.match(/(?:(?:\r?\n)Notes:(?:\r?\n)((?:\*.+(?:(?:\r?\n)|$))+))/gi);
const onelineMatch = pr.body.match(
/(?:(?:\r?\n)|^)notes: (.+?)(?:(?:\r?\n)|$)/gi,
);
const multilineMatch = pr.body.match(
/(?:(?:\r?\n)Notes:(?:\r?\n)((?:\*.+(?:(?:\r?\n)|$))+))/gi,
);
// attach release notes to backport PR body
if (onelineMatch && onelineMatch[0]) {
@ -79,23 +102,30 @@ const createBackportComment = (pr: PullsGetResponse) => {
return body;
};
export const backportImpl = async (robot: Application,
context: Context,
targetBranch: string,
purpose: BackportPurpose,
labelToRemove?: string,
labelToAdd?: string) => {
export const backportImpl = async (
robot: Application,
context: Context,
targetBranch: string,
purpose: BackportPurpose,
labelToRemove?: string,
labelToAdd?: string,
) => {
// Optionally disallow backports to EOL branches
const noEOLSupport = getEnvVar('NO_EOL_SUPPORT', '');
if (noEOLSupport) {
const supported = await getSupportedBranches(context);
if (!['master', ...supported].includes(targetBranch)) {
log('backportImpl', LogLevel.WARN, `${targetBranch} is no longer supported - no backport will be initiated.`);
await context.github.issues.createComment(context.repo({
body: `${targetBranch} is no longer supported - no backport will be initiated.`,
issue_number: context.payload.issue.number,
}));
log(
'backportImpl',
LogLevel.WARN,
`${targetBranch} is no longer supported - no backport will be initiated.`,
);
await context.github.issues.createComment(
context.repo({
body: `${targetBranch} is no longer supported - no backport will be initiated.`,
issue_number: context.payload.issue.number,
}),
);
return;
}
}
@ -106,14 +136,18 @@ export const backportImpl = async (robot: Application,
log('backportImpl', LogLevel.INFO, `Queuing ${bp} for "${slug}"`);
const getCheckRun = async () => {
const allChecks = await context.github.checks.listForRef(context.repo({
ref: context.payload.pull_request.head.sha,
per_page: 100,
}));
const allChecks = await context.github.checks.listForRef(
context.repo({
ref: context.payload.pull_request.head.sha,
per_page: 100,
}),
);
return allChecks.data.check_runs.find((run: ChecksListForRefResponseCheckRunsItem) => {
return run.name === `${CHECK_PREFIX}${targetBranch}`;
});
return allChecks.data.check_runs.find(
(run: ChecksListForRefResponseCheckRunsItem) => {
return run.name === `${CHECK_PREFIX}${targetBranch}`;
},
);
};
let createdDir: string | null = null;
@ -125,11 +159,13 @@ export const backportImpl = async (robot: Application,
if (purpose === BackportPurpose.Check) {
const checkRun = await getCheckRun();
if (checkRun) {
await context.github.checks.update(context.repo({
check_run_id: checkRun.id,
name: checkRun.name,
status: 'in_progress' as 'in_progress',
}));
await context.github.checks.update(
context.repo({
check_run_id: checkRun.id,
name: checkRun.name,
status: 'in_progress' as 'in_progress',
}),
);
}
}
@ -148,38 +184,63 @@ export const backportImpl = async (robot: Application,
const targetRepoRemote = `https://x-access-token:${repoAccessToken}@github.com/${slug}.git`;
await setupRemotes({
dir,
remotes: [{
name: 'target_repo',
value: targetRepoRemote,
}],
remotes: [
{
name: 'target_repo',
value: targetRepoRemote,
},
],
});
// Get list of commits.
log('backportImpl', LogLevel.INFO, `Getting rev list from: ${pr.base.sha}..${pr.head.sha}`);
const commits = (await context.github.pulls.listCommits(context.repo({
pull_number: pr.number,
}))).data.map((commit: PullsListCommitsResponseItem) => commit.sha!);
log(
'backportImpl',
LogLevel.INFO,
`Getting rev list from: ${pr.base.sha}..${pr.head.sha}`,
);
const commits = (
await context.github.pulls.listCommits(
context.repo({
pull_number: pr.number,
}),
)
).data.map((commit: PullsListCommitsResponseItem) => commit.sha!);
// No commits == WTF
if (commits.length === 0) {
log('backportImpl', LogLevel.INFO, 'Found no commits to backport - aborting backport process');
log(
'backportImpl',
LogLevel.INFO,
'Found no commits to backport - aborting backport process',
);
return;
}
// Over 240 commits is probably the limit from GitHub so let's not bother.
if (commits.length >= 240) {
log('backportImpl', LogLevel.ERROR, `Too many commits (${commits.length})...backport will not be performed.`);
await context.github.issues.createComment(context.repo({
issue_number: pr.number,
body: 'This PR has exceeded the automatic backport commit limit \
log(
'backportImpl',
LogLevel.ERROR,
`Too many commits (${commits.length})...backport will not be performed.`,
);
await context.github.issues.createComment(
context.repo({
issue_number: pr.number,
body:
'This PR has exceeded the automatic backport commit limit \
and must be performed manually.',
}));
}),
);
return;
}
log('backportImpl', LogLevel.INFO, `Found ${commits.length} commits to backport - requesting details now.`);
const patches: string[] = (new Array(commits.length)).fill('');
log(
'backportImpl',
LogLevel.INFO,
`Found ${commits.length} commits to backport - requesting details now.`,
);
const patches: string[] = new Array(commits.length).fill('');
const q = makeQueue({
concurrency: 5,
});
@ -195,20 +256,29 @@ export const backportImpl = async (robot: Application,
},
});
patches[i] = await patchBody.text();
log('backportImpl', LogLevel.INFO, `Got patch (${i + 1}/${commits.length})`);
log(
'backportImpl',
LogLevel.INFO,
`Got patch (${i + 1}/${commits.length})`,
);
});
}
await new Promise(r => q.start(r));
await new Promise((r) => q.start(r));
log('backportImpl', LogLevel.INFO, 'Got all commit info');
// Create temporary branch name.
const sanitizedTitle = pr.title
.replace(/\*/g, 'x').toLowerCase()
.replace(/\*/g, 'x')
.toLowerCase()
.replace(/[^a-z0-9_]+/g, '-');
const tempBranch = `trop/${targetBranch}-bp-${sanitizedTitle}-${Date.now()}`;
log('backportImpl', LogLevel.INFO, `Checking out target: "target_repo/${targetBranch}" to temp: "${tempBranch}"`);
log(
'backportImpl',
LogLevel.INFO,
`Checking out target: "target_repo/${targetBranch}" to temp: "${tempBranch}"`,
);
log('backportImpl', LogLevel.INFO, 'Will start backporting now');
await backportCommitsToBranch({
@ -221,33 +291,43 @@ export const backportImpl = async (robot: Application,
shouldPush: purpose === BackportPurpose.ExecuteBackport,
});
log('backportImpl', LogLevel.INFO, 'Cherry pick success - pushed up to target_repo');
log(
'backportImpl',
LogLevel.INFO,
'Cherry pick success - pushed up to target_repo',
);
if (purpose === BackportPurpose.ExecuteBackport) {
log('backportImpl', LogLevel.INFO, 'Creating Pull Request');
const { data: newPr } = (await context.github.pulls.create(context.repo({
head: `${tempBranch}`,
base: targetBranch,
title: pr.title,
body: createBackportComment(pr),
maintainer_can_modify: false,
})));
const { data: newPr } = await context.github.pulls.create(
context.repo({
head: `${tempBranch}`,
base: targetBranch,
title: pr.title,
body: createBackportComment(pr),
maintainer_can_modify: false,
}),
);
// If user has sufficient permissions (i.e has write access)
// request their review on the automatically backported pull request
if (await checkUserHasWriteAccess(context, pr.user.login)) {
await context.github.pulls.createReviewRequest(context.repo({
pull_number: newPr.number,
reviewers: [pr.user.login],
}));
await context.github.pulls.createReviewRequest(
context.repo({
pull_number: newPr.number,
reviewers: [pr.user.login],
}),
);
}
log('backportImpl', LogLevel.INFO, 'Adding breadcrumb comment');
await context.github.issues.createComment(context.repo({
issue_number: pr.number,
body: `I have automatically backported this PR to "${targetBranch}", \
await context.github.issues.createComment(
context.repo({
issue_number: pr.number,
body: `I have automatically backported this PR to "${targetBranch}", \
please check out #${newPr.number}`,
}));
}),
);
if (labelToRemove) {
await labelUtils.removeLabel(context, pr.number, labelToRemove);
@ -257,7 +337,10 @@ export const backportImpl = async (robot: Application,
await labelUtils.addLabel(context, pr.number, [labelToAdd]);
}
await labelUtils.addLabel(context, newPr.number!, ['backport', `${targetBranch}`]);
await labelUtils.addLabel(context, newPr.number!, [
'backport',
`${targetBranch}`,
]);
log('backportImpl', LogLevel.INFO, 'Backport process complete');
}
@ -265,16 +348,18 @@ export const backportImpl = async (robot: Application,
if (purpose === BackportPurpose.Check) {
const checkRun = await getCheckRun();
if (checkRun) {
context.github.checks.update(context.repo({
check_run_id: checkRun.id,
name: checkRun.name,
conclusion: 'success' as 'success',
completed_at: (new Date()).toISOString(),
output: {
title: 'Clean Backport',
summary: `This PR was checked and can be backported to "${targetBranch}" cleanly.`,
},
}));
context.github.checks.update(
context.repo({
check_run_id: checkRun.id,
name: checkRun.name,
conclusion: 'success' as 'success',
completed_at: new Date().toISOString(),
output: {
title: 'Clean Backport',
summary: `This PR was checked and can be backported to "${targetBranch}" cleanly.`,
},
}),
);
}
}
@ -293,17 +378,27 @@ export const backportImpl = async (robot: Application,
for (const file of diff) {
if (file.binary) continue;
for (const hunk of (file.hunks || [])) {
const startOffset = hunk.lines.findIndex((line: string) => line.includes('<<<<<<<'));
const endOffset = hunk.lines.findIndex((line: string) => line.includes('=======')) - 2;
const finalOffset = hunk.lines.findIndex((line: string) => line.includes('>>>>>>>'));
for (const hunk of file.hunks || []) {
const startOffset = hunk.lines.findIndex((line: string) =>
line.includes('<<<<<<<'),
);
const endOffset =
hunk.lines.findIndex((line: string) => line.includes('=======')) -
2;
const finalOffset = hunk.lines.findIndex((line: string) =>
line.includes('>>>>>>>'),
);
annotations.push({
path: file.filePath,
start_line: hunk.theirStartLine + Math.max(0, startOffset),
end_line: hunk.theirStartLine + Math.max(0, endOffset),
annotation_level: 'failure',
message: 'Patch Conflict',
raw_details: hunk.lines.filter((_: any, i: number) => i >= startOffset && i <= finalOffset).join('\n'),
raw_details: hunk.lines
.filter(
(_: any, i: number) => i >= startOffset && i <= finalOffset,
)
.join('\n'),
});
}
}
@ -313,11 +408,13 @@ export const backportImpl = async (robot: Application,
const pr = context.payload.pull_request;
if (purpose === BackportPurpose.ExecuteBackport) {
await context.github.issues.createComment(context.repo({
issue_number: pr.number,
body: `I was unable to backport this PR to "${targetBranch}" cleanly;
await context.github.issues.createComment(
context.repo({
issue_number: pr.number,
body: `I was unable to backport this PR to "${targetBranch}" cleanly;
you will need to perform this backport manually.`,
}) as any);
}) as any,
);
const labelToRemove = PRStatus.TARGET + targetBranch;
await labelUtils.removeLabel(context, pr.number, labelToRemove);
@ -334,11 +431,13 @@ export const backportImpl = async (robot: Application,
check_run_id: checkRun.id,
name: checkRun.name,
conclusion: 'neutral' as 'neutral',
completed_at: (new Date()).toISOString(),
completed_at: new Date().toISOString(),
output: {
title: 'Backport Failed',
summary: `This PR was checked and could not be automatically backported to "${targetBranch}" cleanly`,
text: diff ? `Failed Diff:\n\n${mdSep}diff\n${rawDiff}\n${mdSep}` : undefined,
text: diff
? `Failed Diff:\n\n${mdSep}diff\n${rawDiff}\n${mdSep}`
: undefined,
annotations: annotations ? annotations : undefined,
},
});

Просмотреть файл

@ -11,32 +11,49 @@ import { LogLevel } from '../enums';
* @param {Context} context - the context of the event that was triggered
* @returns {Promise<string[]>} - an array of currently supported branches in x-y-z format
*/
export async function getSupportedBranches(context: Context): Promise<string[]> {
log('getSupportedBranches', LogLevel.INFO, 'Fetching supported branches for this repository');
export async function getSupportedBranches(
context: Context,
): Promise<string[]> {
log(
'getSupportedBranches',
LogLevel.INFO,
'Fetching supported branches for this repository',
);
const SUPPORTED_BRANCH_ENV_PATTERN = getEnvVar('SUPPORTED_BRANCH_PATTERN', '^(\d)+-(?:(?:[0-9]+-x$)|(?:x+-y$))$');
const SUPPORTED_BRANCH_ENV_PATTERN = getEnvVar(
'SUPPORTED_BRANCH_PATTERN',
'^(d)+-(?:(?:[0-9]+-x$)|(?:x+-y$))$',
);
const SUPPORTED_BRANCH_PATTERN = new RegExp(SUPPORTED_BRANCH_ENV_PATTERN);
const { data: branches } = await context.github.repos.listBranches(context.repo({
protected: true,
}));
const { data: branches } = await context.github.repos.listBranches(
context.repo({
protected: true,
}),
);
const releaseBranches = branches.filter(branch => branch.name.match(SUPPORTED_BRANCH_PATTERN));
const releaseBranches = branches.filter((branch) =>
branch.name.match(SUPPORTED_BRANCH_PATTERN),
);
const filtered: Record<string, string> = {};
releaseBranches.sort((a, b) => {
const aParts = a.name.split('-');
const bParts = b.name.split('-');
for (let i = 0; i < aParts.length; i += 1) {
if (aParts[i] === bParts[i]) continue;
return parseInt(aParts[i], 10) - parseInt(bParts[i], 10);
}
return 0;
}).forEach((branch) => {
return (filtered[branch.name.split('-')[0]] = branch.name);
});
releaseBranches
.sort((a, b) => {
const aParts = a.name.split('-');
const bParts = b.name.split('-');
for (let i = 0; i < aParts.length; i += 1) {
if (aParts[i] === bParts[i]) continue;
return parseInt(aParts[i], 10) - parseInt(bParts[i], 10);
}
return 0;
})
.forEach((branch) => {
return (filtered[branch.name.split('-')[0]] = branch.name);
});
const values = Object.values(filtered);
return values.sort((a, b) => parseInt(a, 10) - parseInt(b, 10)).slice(-NUM_SUPPORTED_VERSIONS);
return values
.sort((a, b) => parseInt(a, 10) - parseInt(b, 10))
.slice(-NUM_SUPPORTED_VERSIONS);
}
/**

Просмотреть файл

@ -6,20 +6,23 @@ export async function updateBackportValidityCheck(
context: Context,
checkRun: ChecksListForRefResponseCheckRunsItem,
statusItems: {
conclusion: CheckRunStatus,
title: string
summary: string,
conclusion: CheckRunStatus;
title: string;
summary: string;
},
) {
await context.github.checks.update(context.repo({
check_run_id: checkRun.id,
name: checkRun.name,
conclusion: statusItems.conclusion as CheckRunStatus,
completed_at: (new Date()).toISOString(),
details_url: 'https://github.com/electron/trop/blob/master/docs/manual-backports.md',
output: {
title: statusItems.title,
summary: statusItems.summary,
},
}));
await context.github.checks.update(
context.repo({
check_run_id: checkRun.id,
name: checkRun.name,
conclusion: statusItems.conclusion as CheckRunStatus,
completed_at: new Date().toISOString(),
details_url:
'https://github.com/electron/trop/blob/master/docs/manual-backports.md',
output: {
title: statusItems.title,
summary: statusItems.summary,
},
}),
);
}

Просмотреть файл

@ -14,7 +14,11 @@ export function getEnvVar(envVar: string, defaultValue?: string): string {
const value = process.env[envVar] || defaultValue;
if (!value && value !== '') {
log('getEnvVar', LogLevel.ERROR, `Missing environment variable '${envVar}'`);
log(
'getEnvVar',
LogLevel.ERROR,
`Missing environment variable '${envVar}'`,
);
throw new Error(`Missing environment variable '${envVar}'`);
}
return value;

Просмотреть файл

@ -3,39 +3,68 @@ import { PullsGetResponseLabelsItem } from '@octokit/rest';
import { log } from './log-util';
import { LogLevel } from '../enums';
export const addLabel = async (context: Context, prNumber: number, labelsToAdd: string[]) => {
export const addLabel = async (
context: Context,
prNumber: number,
labelsToAdd: string[],
) => {
log('addLabel', LogLevel.INFO, `Adding ${labelsToAdd} to PR #${prNumber}`);
return context.github.issues.addLabels(context.repo({
issue_number: prNumber,
labels: labelsToAdd,
}));
return context.github.issues.addLabels(
context.repo({
issue_number: prNumber,
labels: labelsToAdd,
}),
);
};
export const removeLabel = async (context: Context, prNumber: number, labelToRemove: string) => {
log('removeLabel', LogLevel.INFO, `Removing ${labelToRemove} from PR #${prNumber}`);
export const removeLabel = async (
context: Context,
prNumber: number,
labelToRemove: string,
) => {
log(
'removeLabel',
LogLevel.INFO,
`Removing ${labelToRemove} from PR #${prNumber}`,
);
// If the issue does not have the label, don't try remove it
if (!await labelExistsOnPR(context, prNumber, labelToRemove)) return;
if (!(await labelExistsOnPR(context, prNumber, labelToRemove))) return;
return context.github.issues.removeLabel(context.repo({
issue_number: prNumber,
name: labelToRemove,
}));
return context.github.issues.removeLabel(
context.repo({
issue_number: prNumber,
name: labelToRemove,
}),
);
};
export const labelToTargetBranch = (label: PullsGetResponseLabelsItem, prefix: string) => {
export const labelToTargetBranch = (
label: PullsGetResponseLabelsItem,
prefix: string,
) => {
return label.name.replace(prefix, '');
};
export const labelExistsOnPR = async (context: Context, prNumber: number, labelName: string) => {
log('labelExistsOnPR', LogLevel.INFO, `Checking if ${labelName} exists on #${prNumber}`);
export const labelExistsOnPR = async (
context: Context,
prNumber: number,
labelName: string,
) => {
log(
'labelExistsOnPR',
LogLevel.INFO,
`Checking if ${labelName} exists on #${prNumber}`,
);
const labels = await context.github.issues.listLabelsOnIssue(context.repo({
issue_number: prNumber,
per_page: 100,
page: 1,
}));
const labels = await context.github.issues.listLabelsOnIssue(
context.repo({
issue_number: prNumber,
per_page: 100,
page: 1,
}),
);
return labels.data.some(label => label.name === labelName);
return labels.data.some((label) => label.name === labelName);
};

Просмотреть файл

@ -7,7 +7,11 @@ import { LogLevel } from '../enums';
* @param {LogLevel }logLevel - the severity level of the log
* @param {any[]} message - the message to write to console
*/
export const log = (functionName: string, logLevel: LogLevel, ...message: any[]) => {
export const log = (
functionName: string,
logLevel: LogLevel,
...message: any[]
) => {
const output = `${functionName}: ${message}`;
if (logLevel === LogLevel.INFO) {

Просмотреть файл

@ -9,7 +9,10 @@ import { LogLevel } from '../enums';
* @param {Context} context - the context of the event that was triggered
* @returns {Promise<string>} - a string representing a GitHub App installation token
*/
export const getRepoToken = async (robot: Application, context: Context): Promise<string> => {
export const getRepoToken = async (
robot: Application,
context: Context,
): Promise<string> => {
log('getRepoToken', LogLevel.INFO, 'Creating GitHub App token');
const hub = await robot.auth();

Просмотреть файл

@ -1,8 +0,0 @@
{
"extends": "tslint-config-airbnb",
"rules": {
"import-name": false,
"max-line-length": [true, 140],
"prefer-array-literal": false
}
}