Skip to content

Commit

Permalink
Added core functionality to auto-merge PRs (#5)
Browse files Browse the repository at this point in the history
Created base of the bot, with the ability to comment on a PR and to
enable/disable auto-merge.

You can find a working example in paritytech-stg#1

This resolves #1 

The bot can enable auto-merge, disable it and show the available
commands.

If the user is not a member of the org or the author of the PR, the bot
will not work. This is to stop external parties of enabling/disabling
the bot.

This is a required step to solve polkadot-fellows/runtimes#41

---------

Co-authored-by: Przemek Rzad <[email protected]>
  • Loading branch information
Bullrich and rzadp authored Sep 27, 2023
1 parent f3d89fd commit 24f7c79
Show file tree
Hide file tree
Showing 12 changed files with 874 additions and 530 deletions.
17 changes: 17 additions & 0 deletions .github/workflows/auto-merge-bot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Auto Merge Bot

on:
# GitHub considers PRs as issues
issue_comment:
types: [created]

jobs:
set-auto-merge:
runs-on: ubuntu-latest
# Important! This forces the job to run only on Pull Requests
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/bot') }}
steps:
- name: Set auto merge
uses: paritytech/auto-merge-bot@main
with:
GITHUB_TOKEN: '${{ github.token }}'
61 changes: 55 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,58 @@
# Parity GitHub Action template
# Auto-Merge-Bot

Template used to generate GitHub Actions.
Bot which enables or disable [`auto-merge`](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/automatically-merging-a-pull-request) in a repository.

## To start
## Why?

- Remember to modify the `action.yml` file to have your required attributes and details.
- You can use [GitHub Action Brandings cheatsheet](https://github.com/haya14busa/github-action-brandings) to set the style of the action.
- Remember to modify the name in the `package.json`.
This action was developed to help external parties merge their own Pull Requests.

If an external party makes a PR, and it is approved, they still can not merge it. This bot gives them the ability to enable the `auto-merge` function so, once their PR gets approved, it is merged.

## Configuration
Be sure that **Allow auto-merge** is enabled in the repository options.

Create a file named `.github/workflows/auto-merge-bot.yml` and add the following:
```yaml
name: Auto Merge Bot

on:
# GitHub considers PRs as issues
issue_comment:
types: [created]

jobs:
set-auto-merge:
runs-on: ubuntu-latest
# Important! This forces the job to run only on comments on Pull Requests that starts with '/bot'
if: ${{ github.event.issue.pull_request && startsWith(github.event.comment.body, '/bot') }}
steps:
- name: Set auto merge
uses: paritytech/auto-merge-bot@main
with:
GITHUB_TOKEN: '${{ github.token }}'
MERGE_METHOD: "SQUASH"
```
#### Inputs
You can find all the inputs in [the action file](./action.yml), but let's walk through each one of them:
- `GITHUB_TOKEN`: Token to access to the repository.
- **required**
- This is provided by the repo, you can simply use `${{ github.token }}`.
- `MERGE_METHOD`: Type of merge to enable.
- **Optional**: Defaults to `SQUASH`.
- Available types are `MERGE`, `REBASE` and `SQUASH`.
- Make sure that the type of merge you selected is available in the repository merge options.

## Usage

To trigger the bot, you need to write a comment in a Pull Request where the action is installed. The available actions are:
- `/bot merge`: Enables auto-merge for Pull Request
- `/bot cancel`: Cancels auto-merge for Pull Request
- `/bot help`: Shows this menu

The bot can only be triggered by the author of the PR or by users who *publicly* belongs to the organization of the repository.

By publicly, I refer to the members of an organization which can be seen by external parties. If you are not sure if you are part of an organization, simply open https://github.com/orgs/**your_organization**/people in a private window. If you don’t see your name there, you are not a public member.

Find related docs here: [ Publicizing or hiding organization membership](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-your-membership-in-organizations/publicizing-or-hiding-organization-membership).
14 changes: 9 additions & 5 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
name: "Example Action"
description: "This values need to be changed"
name: "Auto Merge Bot"
description: "Bot which enables or disable auto-merge"
author: Bullrich
branding:
icon: copy
color: yellow
icon: git-merge
color: red
inputs:
GITHUB_TOKEN:
required: true
description: The token to access the repo
description: The token to access the repo information
MERGE_METHOD:
required: false
description: The merge method to use. Must be one of MERGE, SQUASH or REBASE.
default: SQUASH
outputs:
repo:
description: 'The name of the repo in owner/repo pattern'
Expand Down
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "parity-action-template",
"name": "auto-merge-bot",
"version": "0.0.1",
"description": "GitHub action template for Parity",
"description": "Bot which enables or disable auto-merge",
"main": "src/index.ts",
"scripts": {
"start": "node dist",
Expand All @@ -12,17 +12,19 @@
},
"repository": {
"type": "git",
"url": "git+https://github.com/Bullrich/parity-action-template.git"
"url": "git+https://github.com/paritytech/auto-merge-bot.git"
},
"author": "Javier Bullrich <[email protected]>",
"license": "MIT",
"bugs": {
"url": "https://github.com/Bullrich/parity-action-template/issues"
"url": "https://github.com/paritytech/auto-merge-bot/issues"
},
"homepage": "https://github.com/Bullrich/parity-action-template#readme",
"homepage": "https://github.com/paritytech/auto-merge-bot#readme",
"dependencies": {
"@actions/core": "^1.10.1",
"@actions/github": "^5.1.1",
"@octokit/graphql": "^7.0.2",
"@octokit/graphql-schema": "^14.32.1",
"@octokit/webhooks-types": "^7.3.1"
},
"devDependencies": {
Expand Down
107 changes: 107 additions & 0 deletions src/bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { Issue, IssueComment } from "@octokit/webhooks-types";

import { CommentsApi } from "./github/comments";
import { Merger } from "./github/merger";
import { ActionLogger } from "./github/types";

const BOT_COMMAND = "/bot";

type Command = "merge" | "cancel" | "help";

const botCommands = `
**Available commands**
- \`/bot merge\`: Enables auto-merge for Pull Request
- \`/bot cancel\`: Cancels auto-merge for Pull Request
- \`/bot help\`: Shows this menu
For more information see the [documentation](https://github.com/paritytech/auto-merge-bot)
`;

export class Bot {
constructor(
private readonly comment: IssueComment,
private readonly pr: Issue,
private readonly logger: ActionLogger,
private readonly commentsApi: CommentsApi,
) {}

/** Verifies if the author is the author of the PR or a member of the org */
async canTriggerBot(): Promise<boolean> {
this.logger.debug("Evaluating if user can trigger the bot");
const author = this.pr.user.id;
if (this.comment.user.id === author) {
this.logger.debug("Author of comment is also author of PR");
return true;
}
this.logger.debug("Author of comment is not the author of the PR");

return await this.commentsApi.userBelongsToOrg(this.comment.user.login);
}

async run(merger: Merger): Promise<void> {
this.logger.info("Running action on comment: " + this.comment.html_url);
if (!this.comment.body.startsWith(BOT_COMMAND)) {
this.logger.info(
`Ignoring comment ${this.comment.html_url} as it does not start with '${BOT_COMMAND}'`,
);
return;
}

if (this.pr.state === "closed") {
this.logger.info("Ignoring PR as it is closed");
return;
}

if (!(await this.canTriggerBot())) {
const { login } = this.comment.user;
const org = this.commentsApi.pullData.owner;
this.logger.warn(
"User is not allowed to trigger the bot. " +
`He is not the author of the PR and does not *publicly* belong to the org: https://github.com/orgs/${org}/people`,
);
await this.commentsApi.reactToComment(this.comment.id, "-1");
await this.commentsApi.comment(
"## Auto-Merge-Bot\n" +
`User @${login} is not the author of the PR and does not [*publicly* belong to the org \`${org}\`](https://github.com/orgs/${org}/people).\n\n` +
"Only author or *public* org members can trigger the bot.",
);
return;
}
this.logger.debug("User can trigger bot");

const [_, command] = this.comment.body.split(" ");
try {
switch (command as Command) {
case "merge":
await this.commentsApi.reactToComment(this.comment.id, "+1");
await merger.enableAutoMerge();
await this.commentsApi.comment(
"Enabled `auto-merge` in Pull Request",
);
break;
case "cancel":
await this.commentsApi.reactToComment(this.comment.id, "+1");
await merger.disableAutoMerge();
await this.commentsApi.comment(
"Disabled `auto-merge` in Pull Request",
);
break;
case "help":
await this.commentsApi.comment("## Auto-Merge-Bot\n" + botCommands);
break;
default: {
await this.commentsApi.reactToComment(this.comment.id, "confused");
await this.commentsApi.comment(
"## Auto-Merge-Bot\n" +
`Command \`${command}\` not recognized.\n\n` +
botCommands,
);
}
}
} catch (e) {
this.logger.error(e as Error);
throw e;
}
}
}
47 changes: 47 additions & 0 deletions src/github/comments.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ActionLogger, GitHubClient } from "./types";

/** API class that uses the default token to access the data from the pull request and the repository */
export class CommentsApi {
constructor(
private readonly api: GitHubClient,
private readonly logger: ActionLogger,
public readonly pullData: { repo: string; owner: string; number: number },
) {}

async comment(message: string): Promise<void> {
await this.api.rest.issues.createComment({
...this.pullData,
body: message,
issue_number: this.pullData.number,
});
}

async reactToComment(
commentId: number,
reaction: "+1" | "-1" | "confused",
): Promise<void> {
await this.api.rest.reactions.createForIssueComment({
...this.pullData,
comment_id: commentId,
content: reaction,
});
}

async userBelongsToOrg(username: string): Promise<boolean> {
const org = this.pullData.owner;
this.logger.debug(
`Checking if user ${username} belongs to ${org} as a public user.`,
);
// If the user does not belong to the org, this will throw an http error
try {
const { status } = await this.api.rest.orgs.checkPublicMembershipForUser({
org,
username,
});
return status === 204;
} catch (error) {
this.logger.warn(error as Error);
return false;
}
}
}
50 changes: 50 additions & 0 deletions src/github/merger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { graphql } from "@octokit/graphql";
import { PullRequestMergeMethod } from "@octokit/graphql-schema";

import { ActionLogger } from "./types";

// https://docs.github.com/en/graphql/reference/mutations#enablepullrequestautomerge
export const ENABLE_AUTO_MERGE = `
mutation($prId: ID!, $mergeMethod: PullRequestMergeMethod!) {
enablePullRequestAutoMerge(input: {pullRequestId: $prId, mergeMethod: $mergeMethod}) {
clientMutationId
}
}`;

// https://docs.github.com/en/graphql/reference/mutations#disablepullrequestautomerge
export const DISABLE_AUTO_MERGE = `
mutation($prId: ID!) {
disablePullRequestAutoMerge(input: {pullRequestId: $prId}) {
clientMutationId
}
}`;

export type MergeMethod = "SQUASH" | "MERGE" | "REBASE";

export class Merger {
constructor(
private readonly nodeId: string,
private readonly gql: typeof graphql,
private readonly logger: ActionLogger,
private readonly mergeMethod: PullRequestMergeMethod,
) {}

async enableAutoMerge(): Promise<void> {
await this.gql<{
enablePullRequestAutoMerge: { clientMutationId: unknown };
}>(ENABLE_AUTO_MERGE, {
prId: this.nodeId,
mergeMethod: this.mergeMethod,
});
this.logger.info("Succesfully enabled auto-merge");
}

async disableAutoMerge(): Promise<void> {
await this.gql<{
disablePullRequestAutoMerge: { clientMutationId: unknown };
}>(DISABLE_AUTO_MERGE, {
prId: this.nodeId,
});
this.logger.info("Succesfully disabled auto-merge");
}
}
15 changes: 0 additions & 15 deletions src/github/pullRequest.ts

This file was deleted.

Loading

0 comments on commit 24f7c79

Please sign in to comment.