-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from Bandwidth/DX-2950
DX-2950
- Loading branch information
Showing
7 changed files
with
733 additions
and
1 deletion.
There are no files selected for viewing
Validating CODEOWNERS rules …
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Global rule: | ||
* @Bandwidth/dx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
}) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); |
Oops, something went wrong.