Skip to content

Commit

Permalink
Merge pull request #1 from Bandwidth/DX-2950
Browse files Browse the repository at this point in the history
DX-2950
  • Loading branch information
ajrice6713 authored Dec 7, 2022
2 parents 18efab9 + 800b70c commit bd1942d
Show file tree
Hide file tree
Showing 7 changed files with 733 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Global rule:
* @Bandwidth/dx
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
54 changes: 53 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,53 @@
# automerge-docs-action
# automerge-pr-action

This action polls for required checks within a branch, waits for successful completion, and automatically merges a pull request.

It grabs required check names from your branch protection rules via query of GitHub's API, polls until they are complete, and merges if they are successful.

If there are no required checks - as long as there are no merge conflicts, the PR can be merged successfully.

### Inputs

| Name | Description | Required | Default |
|:-----------|:----------------------------------------------------------------------------------|:--------:|:-----------:|
| repoOwner | The owner of the repository with the PR you wish to automatically merge | false | `bandwidth` |
| repoName | The name of the repo with the PR in question | true | N/A |
| prNumber | The PR number to automatically merge | true | N/A |
| token | GH user token with permission to merge PRs and read branch and checks information | true | N/A |
| maxRetries | The amount of times to retry requests for checks status (string) | false | '10' |
| retryDelay | Amount of time (in seconds) to wait between retries (string) | false | '60' |

### Example Usage

```yml
jobs:
merge:
if: ${{ github.event.action == 'Merge' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Set PR number as env variable
run: |
echo "PR_NUMBER=$(hub pr list -h ${{ github.event.client_payload.branchName }} -f %I)" >> $GITHUB_ENV
- uses: bandwidth/[email protected]
with:
repoOwner: my-org-that-isnt-bandwidth
repoName: my-cool-repo
prNumber: ${{ env.PR_NUMBER }}
token: ${{ secrets.MY_BOT_GH_USER_TOKEN }}
maxRetries: '5'
retryDelay: '30'

- uses: actions/github-script@v6
if: failure()
with:
script: |
github.rest.issues.createComment({
issue_number: ${{ env.PR_NUMBER }},
owner: context.repo.owner,
repo: context.repo.repo,
body: 'Failed to auto-merge this PR. Check workflow logs for more information'
})
```
46 changes: 46 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Auto Merge
description: Auto Merges an open PR given a Branch Name
inputs:
repoOwner:
required: false
description: Repository Owner
default: bandwidth
repoName:
required: true
description: Repository Name
prNumber:
required: true
description: Pull Request Number
token:
required: true
description: Github Token
maxRetries:
required: false
description: Amount of times to retry polling for status checks
default: '10'
retryDelay:
required: false
description: Time to sleep in between retry attempts (in seconds)
default: '60'
runs:
using: composite
steps:
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 16

- name: Install and Run Script
run: |
cd ${{ github.action_path }}
npm install && npm run start
env:
REPO_OWNER: ${{ inputs.repoOwner }}
REPO_NAME: ${{ inputs.repoName }}
PR_NUMBER: ${{ inputs.prNumber }}
CHECK_NAMES: ${{ inputs.checkNames }}
TOKEN: ${{ inputs.token }}
MAX_RETRIES: ${{ inputs.maxRetries }}
RETRY_DELAY: ${{ inputs.retryDelay }}
shell: bash

176 changes: 176 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
const core = require("@actions/core");
const github = require("@actions/github");

const repoOwner = process.env.REPO_OWNER;
const repoName = process.env.REPO_NAME;
const prNumber = process.env.PR_NUMBER;
const token = process.env.TOKEN;
const maxRetries = Number(process.env.MAX_RETRIES);
const retryDelay = Number(process.env.RETRY_DELAY);

const octokit = github.getOctokit(token);

/**
* getCheckRunId Returns the GitHub Check ID given a check name
* @param {string} requiredCheckName
* @returns {int} Unique Check ID
*/
async function getCheckRunId(requiredCheckName) {
var checkId = null;

const { data: checks } = await octokit.request(
"GET /repos/{repoOwner}/{repoName}/commits/{commitId}/check-runs",
{
repoOwner: repoOwner,
repoName: repoName,
commitId: commitId,
}
);

for (run in checks.check_runs) {
if (requiredCheckName.includes(checks.check_runs[run].name)) {
checkId = checks.check_runs[run].id;
}
}

return checkId;
}

/**
* pollForChecks Polls the GitHub API For checks status
* @param {object} requiredChecks
* @returns {array} An array of status objects, with a checkId, name, and result property. `result` has status and conclusion properties, both strings.
*/
async function pollForChecks(requiredChecks) {
var checksStatus = [];
var currentTry = 0;

// Poll for checkIds. GitHub does not create an ID for checks until they have started
// Enqueued checks do not receive an ID, so unfortunately we must poll to get an ID for the
// required checks we previously grabbed from the branch protection rules
for (check in requiredChecks) {
while (currentTry <= maxRetries) {
checkId = await getCheckRunId(requiredChecks[check].context);
if (checkId == null) {
currentTry += 1;
await new Promise((resolve) =>
setTimeout(resolve, retryDelay * 1000) // Convert ms to seconds
);
} else {
currentTry = 0;
checksStatus.push({
checkId: checkId,
name: requiredChecks[check].context,
result: null,
});
break;
}
}
}

if (requiredChecks.length != checksStatus.length) {
console.log(requiredChecks.length)
console.log(checksStatus.length)
core.setFailed(
"Timed out waiting for required checks to complete. Cant Auto-Merge PR."
);
process.exit(1);
}

// Poll for completed status on all of the required checks
// Resets currentTry since we need to poll again for completion
currentTry = 0;
for (check in checksStatus) {
while (currentTry <= maxRetries) {
run = await octokit.rest.checks.get({
owner: repoOwner,
repo: repoName,
check_run_id: checksStatus[check].checkId,
});
if (run.data.status == "completed") {
currentTry = 0;
checksStatus[check].result = {
status: run.data.status,
conclusion: run.data.conclusion,
};
break;
}
currentTry += 1;
await new Promise((resolve) =>
setTimeout(resolve, retryDelay * 1000) // Convert ms to seconds
);
}
}

return checksStatus;
}

/**
* main contains all of the logic to gather required PR check information, poll for complete + successful status
* and then merge a PR contingent on required checks completing successfully.
*/
async function main() {
const { data: pullRequest } = await octokit.rest.pulls.get({
owner: repoOwner,
repo: repoName,
pull_number: prNumber,
});

if (pullRequest.mergeable != true) {
core.setFailed("Merge Conflicts Present. Cant Auto-Merge PR.");
process.exit(1);
}

const { data: branch } = await octokit.request(
"GET /repos/{owner}/{repo}/branches/{branch}",
{
owner: repoOwner,
repo: repoName,
branch: "main",
}
);

requiredChecks = branch.protection.required_status_checks.checks;

if (requiredChecks.length) {
global.commitId = pullRequest.head.sha;
const checksStatusList = await pollForChecks(requiredChecks);

// Check to ensure that each check completed with a successful result
for (check in checksStatusList) {
if (checksStatusList[check].result.conclusion != "success") {
core.setFailed(
checksStatusList[check].name +
"(ID: " +
checksStatusList[check].checkId +
")" +
" failed. Can't auto-merge PR"
);
process.exit(1);
}
}
}

// Attempt to merge the PR given all criteria was met
try {
await octokit.request(
"PUT /repos/{owner}/{repo}/pulls/{pull_number}/merge",
{
owner: repoOwner,
repo: repoName,
pull_number: prNumber,
commit_title: "Auto-merge PR based on merge event",
commit_message:
"Auto-merging PR based on merge event from upstream repository",
}
);
} catch (error) {
core.setFailed(
"Auto-merge criteria was met, but API call to merge PR failed:\n",
error
);
process.exit(1);
}
}

main();
Loading

0 comments on commit bd1942d

Please sign in to comment.