-
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.
First commit of automerge public action
- Loading branch information
0 parents
commit 651e3f7
Showing
5 changed files
with
283 additions
and
0 deletions.
There are no files selected for viewing
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,18 @@ | ||
name: Basic validation | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
paths-ignore: | ||
- '**.md' | ||
pull_request: | ||
paths-ignore: | ||
- '**.md' | ||
|
||
jobs: | ||
call-basic-validation: | ||
name: Basic validation | ||
uses: actions/reusable-workflows/.github/workflows/basic-validation.yml@main | ||
with: | ||
node-version: '20' |
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,55 @@ | ||
Example workflow using Automerge across an Github Organization | ||
============================================================== | ||
This example workflow will run every 20 minutes and will automerge any PRs that are ready to be merged. It will also cancel any previous runs of the workflow if they are still running. | ||
|
||
From the repository the workflow is running from it will require a JSON file called `automerge.json` that contains a list of repositories to run the workflow on. The JSON file should be in the following format: | ||
|
||
```json | ||
[ | ||
"repo1", | ||
"repo2", | ||
"repo3" | ||
] | ||
``` | ||
In .github/workflows/automerge.yml | ||
```yaml | ||
name: 'Automerger' | ||
on: | ||
workflow_dispatch: | ||
schedule: | ||
- cron: '*/20 * * * *' | ||
concurrency: | ||
group: ${{ github.workflow }}-${{ github.ref }} | ||
cancel-in-progress: true | ||
|
||
jobs: | ||
|
||
list: | ||
name: 'List Repos' | ||
runs-on: [linux] | ||
outputs: | ||
matrix: ${{ steps.list.outputs.value }} | ||
steps: | ||
- name: 'Check out devops repo' | ||
uses: actions/checkout@v3 | ||
- id: list | ||
name: 'List automerge repos' | ||
run: echo "value=$(cat automerge.json | tr -d '\n')" >> $GITHUB_OUTPUT | ||
|
||
automerge: | ||
name: 'Automerge' | ||
runs-on: [self-hosted, linux, flyweight] | ||
needs: list | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
value: ${{fromJson(needs.list.outputs.matrix)}} | ||
steps: | ||
- name: 'Check out devops repo' | ||
uses: actions/checkout@v3 | ||
- name: 'Automerge runtimeverification/${{ matrix.value }}' | ||
uses: ./.github/actions/automerge | ||
with: | ||
repo: ${{ matrix.value }} | ||
token: ${{ secrets.JENKINS_GITHUB_PAT }} | ||
``` |
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,55 @@ | ||
name: "Automerge" | ||
author: "Runtime Verification Inc" | ||
description: "Automatically merge pull requests meeting the requiresments of All Checks Pass, Review Approved, and Up-to-date. This action is a substituion for Github merge queues. Merge queues fails to adhere to passing checks. Automerge helps support a linear well tested PR before it is merged." | ||
inputs: | ||
list-of-repos: | ||
description: 'List of repos to merge' | ||
required: true | ||
org: | ||
description: 'Organization name under which to run on' | ||
required: true | ||
repo: | ||
description: 'Repository name under Organization' | ||
required: true | ||
token: | ||
description: 'Access token to be able to write to the repository' | ||
required: true | ||
outputs: | ||
merged: | ||
value: ${{ steps.automerge.outputs.merged }} | ||
description: 'Whether or not the PR was merged' | ||
error: | ||
value: ${{ steps.automerge.outputs.error }} | ||
description: 'Error message if the PR was not merged' | ||
success: | ||
value: ${{ steps.automerge.outputs.success }} | ||
description: 'Success message if the PR was merged' | ||
runs: | ||
using: 'composite' | ||
steps: | ||
- name: Setup Python | ||
uses: actions/[email protected] | ||
with: | ||
python-version: '3.x' | ||
|
||
- name: Install dependencies | ||
shell: bash {0} | ||
run: | | ||
pip install logging os subprocess sys github typing | ||
- name: 'Check out repo: ${{ inputs.org }}/${{ inputs.repo }}' | ||
uses: actions/checkout@v3 | ||
with: | ||
token: ${{ inputs.token }} | ||
repository: ${{ inputs.org }}/${{ inputs.repo }} | ||
path: tmp-${{ inputs.repo }} | ||
fetch-depth: 0 | ||
|
||
- name: 'Run automerger: ${{ inputs.org }}/${{ inputs.repo }}' | ||
shell: bash {0} | ||
env: | ||
GITHUB_TOKEN: ${{ inputs.token }} | ||
# TODO: for some reason, even though the script fails, the workflow doesn't | ||
working-directory: tmp-${{ inputs.repo }} | ||
run: python3 ../src/automerge.py --org ${{ inputs.org }} --repo ${{ inputs.repo }} | ||
|
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,137 @@ | ||
#!/usr/bin/env python3 | ||
|
||
import logging | ||
import os | ||
import subprocess | ||
import sys | ||
import argparse | ||
|
||
from github import Github | ||
from typing import Final | ||
|
||
# Collect command arguments from argparse | ||
parser = argparse.ArgumentParser(description='Automerge approved PRs.') | ||
parser.add_argument('--repo', type=str, help='The repository to check.') | ||
parser.add_argument('--org', type=str, help='The GitHub organization to check.') | ||
args = parser.parse_args() | ||
|
||
_LOGGER: Final = logging.getLogger(__name__) | ||
_LOG_FORMAT: Final = '%(levelname)s %(asctime)s %(name)s - %(message)s' | ||
logging.basicConfig(level=logging.INFO) | ||
|
||
def pr_to_display_string(pr): | ||
return f'- {pr.number}: {pr.title}' | ||
|
||
def run_git_command(args: str) -> subprocess.CompletedProcess: | ||
command = ['git'] + args.split(' ') | ||
_LOGGER.debug(f'Running: {" ".join(command)}') | ||
try: | ||
res = subprocess.run(command, stdout=None, stderr=None, check=False, text=True) | ||
except subprocess.CalledProcessError as err: | ||
_LOGGER.error(f'Completed with status {err.returncode}: {" ".join(command)}') | ||
raise | ||
return res | ||
|
||
github = Github(login_or_token=os.environ['GITHUB_TOKEN']) | ||
repo = args.org/args.repo | ||
|
||
# 1. Get PRs that are: | ||
# - Open. | ||
open_prs = [] | ||
for pr in github.get_repo(repo).get_pulls(): | ||
if pr.state == 'open': | ||
open_prs.append(pr) | ||
pr_string = '\n'.join(map(pr_to_display_string, open_prs)) | ||
_LOGGER.info(f' PRs:\n{pr_string}\n') | ||
if not open_prs: | ||
_LOGGER.info(f' Quitting.') | ||
|
||
# 2. Get PRs that are: | ||
# - Open, | ||
# - Labeled as `automerge`, and | ||
# - Approved. | ||
automerge_prs = [] | ||
for pr in open_prs: | ||
labels = [l.name for l in pr.get_labels()] | ||
reviews = sorted([(r.state, r.submitted_at) for r in pr.get_reviews()], key=lambda x: x[1], reverse=True) | ||
reviews = [state for state, _ in reviews] | ||
if 'automerge' in labels: | ||
approved = False | ||
for i in reviews: | ||
if i == 'APPROVED': | ||
approved = True | ||
break | ||
if i != 'COMMENTED': | ||
break | ||
if approved: | ||
automerge_prs.append(pr) | ||
pr_string = '\n'.join(map(pr_to_display_string, automerge_prs)) | ||
_LOGGER.info(f' Automerge approved PRs:\n{pr_string}\n') | ||
if not automerge_prs: | ||
_LOGGER.info(' Quitting.') | ||
sys.exit(0) | ||
|
||
# 3. Get PRs that are: | ||
# - Open, | ||
# - Labelled as `automerge`, | ||
# - Approved, | ||
# - Up-to-date, and | ||
# - Passing tests. | ||
automerge_up_to_date_prs = [] | ||
for pr in automerge_prs: | ||
is_up_to_date = run_git_command(f'merge-base --is-ancestor {pr.base.sha} {pr.head.sha}').returncode == 0 | ||
if pr.mergeable_state == 'clean' and is_up_to_date: | ||
automerge_up_to_date_prs.append(pr) | ||
pr_string = '\n'.join(map(pr_to_display_string, automerge_up_to_date_prs)) | ||
_LOGGER.info(f' Automerge approved up-to-date PRs:\n{pr_string}\n') | ||
|
||
# 4. Get PRs that are: | ||
# - Open, | ||
# - Labelled as `automerge`, | ||
# - Approved, and | ||
# - Up-to-date. | ||
# If so, merge one. | ||
if automerge_up_to_date_prs: | ||
pr = automerge_up_to_date_prs[0] | ||
_LOGGER.info(f' Merging PR:\n{pr_to_display_string(pr)}\n') | ||
pr.merge(merge_method='squash') | ||
automerge_up_to_date_prs.pop(0) | ||
|
||
# 5. Get PRs that are: | ||
# - Open, | ||
# - Labelled as `automerge`, | ||
# - Approved, | ||
# - Up-to-date, and | ||
# - Pending tests. | ||
# If so, quit (reduce build pressure). | ||
automerge_up_to_date_pending_prs = [] | ||
for pr in automerge_prs: | ||
is_up_to_date = run_git_command(f'merge-base --is-ancestor {pr.base.sha} {pr.head.sha}').returncode == 0 | ||
commit = [c for c in pr.get_commits() if c.sha == pr.head.sha][0] | ||
is_failing = commit.get_combined_status().state == 'failure' | ||
if pr.mergeable_state == 'blocked' and is_up_to_date and not is_failing: | ||
print(commit.get_combined_status()) | ||
automerge_up_to_date_pending_prs.append(pr) | ||
pr_string = '\n'.join(map(pr_to_display_string, automerge_up_to_date_pending_prs)) | ||
_LOGGER.info(f' Automerge approved up-to-date pending PRs:\n{pr_string}\n') | ||
if automerge_up_to_date_pending_prs: | ||
_LOGGER.info(' Quitting.') | ||
sys.exit(0) | ||
|
||
# 6. Get PRs that are: | ||
# - Open, | ||
# - Labelled as `automerge`, | ||
# - Approved, | ||
# - Out-of-date, and | ||
# - Passing tests. | ||
# If so, update the branch. | ||
automerge_out_of_date_passing_prs = [] | ||
for pr in automerge_prs: | ||
if pr.mergeable_state == 'behind': | ||
automerge_out_of_date_passing_prs.append(pr) | ||
pr_string = '\n'.join(map(pr_to_display_string, automerge_out_of_date_passing_prs)) | ||
_LOGGER.info(f' Automerge approved out-of-date passing PRs:\n{pr_string}\n') | ||
if automerge_out_of_date_passing_prs: | ||
pr = automerge_out_of_date_passing_prs[0] | ||
_LOGGER.info(f' Updating PR:\n{pr_to_display_string(pr)}\n') | ||
pr.update_branch() |
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,18 @@ | ||
[ | ||
"algorand-sc-semantics" | ||
, "amp" | ||
, "audit-kontrol-template" | ||
, "avm-semantics" | ||
, "blockchain-k-plugin" | ||
, "blockswap-server" | ||
, "casper-cbc-proofs" | ||
, "c-semantics" | ||
, "erc20-verification" | ||
, "ercx" | ||
, "evm-semantics" | ||
, "firefly" | ||
, "firefly-web" | ||
, "foundry-tests" | ||
, "haskell-backend" | ||
, "homebrew-k" | ||
] |