Skip to content

Commit

Permalink
feat: use github automerge as experimental feature
Browse files Browse the repository at this point in the history
  • Loading branch information
christophehurpeau committed Dec 11, 2022
1 parent 668ed28 commit ac14990
Show file tree
Hide file tree
Showing 10 changed files with 449 additions and 60 deletions.
1 change: 1 addition & 0 deletions src/accountConfigs/christophehurpeau.ts
Expand Up @@ -11,6 +11,7 @@ const config: Config<'dev', never> = {
},
experimentalFeatures: {
lintPullRequestTitleWithConventionalCommit: true,
githubAutoMerge: true,
},
groups: {
dev: {
Expand Down
1 change: 1 addition & 0 deletions src/accountConfigs/types.ts
Expand Up @@ -69,6 +69,7 @@ export interface LabelsConfig<GroupNames extends string> {

interface ExperimentalFeatures {
lintPullRequestTitleWithConventionalCommit?: true;
githubAutoMerge?: true;
}

interface WarnOnForcePushAfterReviewStarted {
Expand Down
4 changes: 3 additions & 1 deletion src/events/pr-handlers/actions/autoMergeIfPossible.ts
Expand Up @@ -8,6 +8,7 @@ import type {
PullRequestData,
PullRequestFromRestEndpoint,
PullRequestLabels,
PullRequestWithDecentData,
} from '../utils/PullRequestData';
import type { ReviewflowPrContext } from '../utils/createPullRequestContext';
import { createMergeLockPrFromPr } from '../utils/mergeLock';
Expand All @@ -19,7 +20,7 @@ import hasLabelInPR from './utils/hasLabelInPR';
import { readPullRequestCommits } from './utils/readPullRequestCommits';

interface CreateCommitMessageOptions {
pullRequest: PullRequestFromRestEndpoint;
pullRequest: PullRequestWithDecentData;
parsedBody: ParsedBody;
options: Options;
}
Expand Down Expand Up @@ -127,6 +128,7 @@ export const autoMergeIfPossible = async <
if (!repo) return false;

if (repoContext.config.disableAutoMerge) return false;
if (repoContext.config.experimentalFeatures?.githubAutoMerge) return false;

const autoMergeLabel = repoContext.labels['merge/automerge'];

Expand Down
56 changes: 44 additions & 12 deletions src/events/pr-handlers/actions/commentBodyEdited.ts
Expand Up @@ -5,6 +5,10 @@ import type { PullRequestFromRestEndpoint } from '../utils/PullRequestData';
import type { ReviewflowPrContext } from '../utils/createPullRequestContext';
import { autoMergeIfPossible } from './autoMergeIfPossible';
import { editOpenedPR } from './editOpenedPR';
import {
disableGithubAutoMerge,
enableGithubAutoMerge,
} from './enableGithubAutoMerge';
import { updateBranch } from './updateBranch';
import { updatePrCommentBodyIfNeeded } from './updatePrCommentBody';
import { updateStatusCheckFromLabels } from './updateStatusCheckFromLabels';
Expand Down Expand Up @@ -75,20 +79,48 @@ export const commentBodyEdited = async <Name extends EventsWithRepository>(
shouldHaveLabel: options.autoMerge,
label: automergeLabel,
onAdd: async (prLabels) => {
await autoMergeIfPossible(
pullRequest,
context,
repoContext,
reviewflowPrContext,
prLabels,
);
if (
repoContext.settings.allowAutoMerge &&
repoContext.config.experimentalFeatures?.githubAutoMerge
) {
return (
(await enableGithubAutoMerge(
pullRequest,
context,
repoContext,
reviewflowPrContext,
)) !== null
);
} else {
await autoMergeIfPossible(
pullRequest,
context,
repoContext,
reviewflowPrContext,
prLabels,
);
return true;
}
},
onRemove: async () => {
await repoContext.removePrFromAutomergeQueue(
context,
pullRequest,
'label removed',
);
if (
repoContext.settings.allowAutoMerge &&
repoContext.config.experimentalFeatures?.githubAutoMerge
) {
return disableGithubAutoMerge(
pullRequest,
context,
repoContext,
reviewflowPrContext,
);
} else {
await repoContext.removePrFromAutomergeQueue(
context,
pullRequest,
'label removed',
);
return true;
}
},
},
]),
Expand Down
104 changes: 104 additions & 0 deletions src/events/pr-handlers/actions/enableGithubAutoMerge.ts
@@ -0,0 +1,104 @@
import type { EventsWithRepository, RepoContext } from 'context/repoContext';
import type { AutoMergeRequest } from '../../../utils/github/pullRequest/autoMerge';
import {
enableGithubAutoMergeMutation,
disableGithubAutoMergeMutation,
} from '../../../utils/github/pullRequest/autoMerge';
import type { ProbotEvent } from '../../probot-types';
import type { PullRequestWithDecentData } from '../utils/PullRequestData';
import type { ReviewflowPrContext } from '../utils/createPullRequestContext';
import { createCommitMessage } from './autoMergeIfPossible';
import { parseBody } from './utils/body/parseBody';

export const enableGithubAutoMerge = async <
EventName extends EventsWithRepository,
>(
pullRequest: PullRequestWithDecentData,
context: ProbotEvent<EventName>,
repoContext: RepoContext,
reviewflowPrContext: ReviewflowPrContext,
login?: string,
): Promise<AutoMergeRequest | null> => {
// TODO prevent merge when statuses are failing (see autoMergeIfPossible) => add in reviewflow status check

const parsedBody = parseBody(
reviewflowPrContext.commentBody,
repoContext.config.prDefaultOptions,
);
const options = parsedBody?.options || repoContext.config.prDefaultOptions;

const [commitHeadline, commitBody] = createCommitMessage({
pullRequest,
parsedBody,
options,
});

try {
/* Conditions:
Allow auto-merge enabled in settings.
The pull request base must have a branch protection rule with at least one requirement enabled.
The pull request must be in a state where requirements have not yet been satisfied. If the pull request can already be merged, attempting to enable auto-merge will fail.
*/
const response = await enableGithubAutoMergeMutation(context, {
pullRequestId: pullRequest.node_id,
mergeMethod: 'SQUASH',
commitHeadline,
commitBody,
});
return response.enablePullRequestAutoMerge.pullRequest.autoMergeRequest;
} catch (err) {
context.log.error(
'Could not enable automerge',
context.repo({
issue_number: pullRequest.number,
}),
err,
);
context.octokit.issues.createComment(
context.repo({
issue_number: pullRequest.number,
body: `${login ? `@${login} ` : ''}Could not enable automerge`,
}),
);
}
return null;
};

export const disableGithubAutoMerge = async <
EventName extends EventsWithRepository,
>(
pullRequest: PullRequestWithDecentData,
context: ProbotEvent<EventName>,
repoContext: RepoContext,
reviewflowPrContext: ReviewflowPrContext,
login?: string,
): Promise<boolean> => {
try {
/* Conditions:
Allow auto-merge enabled in settings.
The pull request base must have a branch protection rule with at least one requirement enabled.
The pull request must be in a state where requirements have not yet been satisfied. If the pull request can already be merged, attempting to enable auto-merge will fail.
*/
const response = await disableGithubAutoMergeMutation(context, {
pullRequestId: pullRequest.node_id,
});
return (
response.disablePullRequestAutoMerge.pullRequest.autoMergeRequest === null
);
} catch (err) {
context.log.error(
'Could not disable automerge',
context.repo({
issue_number: pullRequest.number,
}),
err,
);
context.octokit.issues.createComment(
context.repo({
issue_number: pullRequest.number,
body: `${login ? `@${login} ` : ''}Could not disable automerge`,
}),
);
return false;
}
};
22 changes: 19 additions & 3 deletions src/events/pr-handlers/actions/utils/syncLabel.ts
Expand Up @@ -5,7 +5,10 @@ import type { ProbotEvent } from 'events/probot-types';
import type { LabelResponse } from '../../../../context/initRepoLabels';
import hasLabelInPR from './hasLabelInPR';

type SyncLabelCallback = (prLabels: LabelResponse[]) => void | Promise<void>;
type SyncLabelCallback = (
prLabels: LabelResponse[],
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
) => void | undefined | boolean | Promise<void | undefined | boolean>;

interface SyncLabelOptions {
onRemove?: SyncLabelCallback;
Expand All @@ -27,13 +30,25 @@ export default async function syncLabel<
const response = await context.octokit.issues.removeLabel(
context.issue({ name: label.name }),
);
if (onRemove) await onRemove(response.data);
if (onRemove) {
if ((await onRemove(response.data)) === false) {
await context.octokit.issues.addLabels(
context.issue({ labels: [label.name] }),
);
}
}
}
if (shouldHaveLabel && !prHasLabel) {
const response = await context.octokit.issues.addLabels(
context.issue({ labels: [label.name] }),
);
if (onAdd) await onAdd(response.data);
if (onAdd) {
if ((await onAdd(response.data)) === false) {
await context.octokit.issues.removeLabel(
context.issue({ name: label.name }),
);
}
}
}
}

Expand Down Expand Up @@ -75,6 +90,7 @@ export async function syncLabels<EventName extends EmitterWebhookEventName>(
onRemove,
onAdd,
}) => {
if (!label) return;
if (prHasLabel && shouldHaveLabel === false) {
labelsToRemove.push(label);
if (onRemove) callbacks.push(onRemove);
Expand Down
78 changes: 78 additions & 0 deletions src/events/pr-handlers/autoMergeChanged.ts
@@ -0,0 +1,78 @@
import type { Probot } from 'probot';
import type { AppContext } from '../../context/AppContext';
import { checkIfIsThisBot } from '../../utils/github/isBotUser';
import { updatePrCommentBodyOptions } from './actions/updatePrCommentBody';
import { syncLabels } from './actions/utils/syncLabel';
import { createPullRequestHandler } from './utils/createPullRequestHandler';

export default function autoMergeChangedHandler(
app: Probot,
appContext: AppContext,
): void {
createPullRequestHandler(
app,
appContext,
'pull_request.auto_merge_enabled',
(payload, context, repoContext) => {
if (repoContext.shouldIgnore) return null;

if (checkIfIsThisBot(payload.sender)) {
// ignore from this bot
return null;
}

return payload.pull_request;
},
async (pullRequest, context, repoContext, reviewflowPrContext) => {
const autoMergeLabel = repoContext.labels['merge/automerge'];

await Promise.all([
reviewflowPrContext &&
(await updatePrCommentBodyOptions(
context,
repoContext,
reviewflowPrContext,
{
autoMerge: true,
},
)),
syncLabels(pullRequest, context, [
{
shouldHaveLabel: true,
label: autoMergeLabel,
},
]),
]);
},
);
createPullRequestHandler(
app,
appContext,
'pull_request.auto_merge_disabled',
(payload, context, repoContext) => {
if (repoContext.shouldIgnore) return null;
return payload.pull_request;
},
async (pullRequest, context, repoContext, reviewflowPrContext) => {
const autoMergeLabel = repoContext.labels['merge/automerge'];

await Promise.all([
reviewflowPrContext &&
(await updatePrCommentBodyOptions(
context,
repoContext,
reviewflowPrContext,
{
autoMerge: false,
},
)),
syncLabels(pullRequest, context, [
{
shouldHaveLabel: false,
label: autoMergeLabel,
},
]),
]);
},
);
}

0 comments on commit ac14990

Please sign in to comment.