From 915a1bb78195a7050aeb83c64cd58d173277f025 Mon Sep 17 00:00:00 2001 From: Freeman <105403280+F-WRunTime@users.noreply.github.com> Date: Fri, 15 Mar 2024 22:14:47 -0600 Subject: [PATCH] Fix commit message options -- Rework Update/Merge Logic (#4) * Update workflow to use debug mode by choice, not required. Update action to include use of dry-run. Change test Yaml to point to special repository for testing * Test workflow now with dry-run as an option from the workflow action * pass flag --dry-run instead of bool * Updating instructions. Fixing usage of Github function merge() * Update README about test execution and purpose. Try and set output from action to enable validation of test results when action completes with a results output. Update automerge to cleanly handle buckets of PRs to pass/updated, pass/outofdate, and failed/pending scenarios --------- Co-authored-by: devops --- .github/workflows/test.yaml | 4 +- README.md | 22 ++++++- action.yaml | 9 +-- src/automerge.py | 125 ++++++++++++++++++------------------ 4 files changed, 92 insertions(+), 68 deletions(-) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 663a620..7f449e4 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -29,12 +29,14 @@ jobs: value: ${{fromJson(needs.list.outputs.matrix)}} steps: - name: 'Check Automerge Repo to Test' - uses: actions/checkout@v4.0.0 + uses: actions/checkout@v4 - name: 'Automerge runtimeverification/${{ matrix.value }}' uses: ./ # This uses the action in the root directory + id: automerge with: org: 'runtimeverification' repo: ${{ matrix.value }} token: ${{ secrets.JENKINS_GITHUB_PAT }} debug: --dry-run + \ No newline at end of file diff --git a/README.md b/README.md index ed12999..51508a7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Automerge PR Action +# About Automerge Action This action is intended to be used in tandem with a CI workflow. This workflow requires a github token with read/write access to all the repositories it will be tracking @@ -120,4 +120,24 @@ $(pwd)/../src/automerge.py --org runtimeverification --repo automerger-test --dr Recommended to first review the actions before running without. Then remove the `--dry-run` flag to run the action. +# Testing +## [test.yaml](.github/workflows/test.yaml) + + ### Purpose: + - The purpose of the test is to import automerger action. + - Evaluate the test Scenarios of a Live Test Setup and Report back the values + + ### Usage: + - The test.yaml file is used by the automerger to determine which pull requests to merge and under what conditions. + - It specifies the target repository, the specific states of the pull requests to test against, and the actions to perform. + + + ### Note: + - Results MUST BE MANUALLY VERIFIED BEFORE MERGE + - The test.yaml file should be updated whenever there are changes to the test scenarios or configurations. + - It is important to ensure that the test.yaml file accurately reflects the desired behavior of the automerger. + + For more information, please refer to the following resources: + - [Link to the repository rutimeverification/automerger-test](https://github.com/runtimeverification/automerger-test) + - [Link to the live pull requests in the repository](https://github.com/runtimeverification/automerger-test/pulls) diff --git a/action.yaml b/action.yaml index 432b99e..77d92b9 100644 --- a/action.yaml +++ b/action.yaml @@ -34,14 +34,14 @@ runs: - name: Setup Python uses: actions/setup-python@v5.0.0 with: - python-version: '3.10' + python-version: '3.10.12' - name: Install dependencies shell: bash {0} run: pip install logging PyGithub argparse - name: 'Check out repo: ${{ inputs.org }}/${{ inputs.repo }}' - uses: actions/checkout@v4.0.0 + uses: actions/checkout@v4 with: token: ${{ inputs.token }} repository: ${{ inputs.org }}/${{ inputs.repo }} @@ -53,5 +53,6 @@ runs: env: GITHUB_TOKEN: ${{ inputs.token }} working-directory: tmp-${{ inputs.repo }} - run: python3 ${{ github.action_path }}/src/automerge.py --org ${{ inputs.org }} --repo ${{ inputs.repo }} ${{ inputs.debug }} - + run: | + python3 ${{ github.action_path }}/src/automerge.py --org ${{ inputs.org }} --repo ${{ inputs.repo }} ${{ inputs.debug }} + diff --git a/src/automerge.py b/src/automerge.py index 91d2c2a..0d9e245 100755 --- a/src/automerge.py +++ b/src/automerge.py @@ -14,6 +14,7 @@ parser.add_argument('--repo', type=str, help='The repository to check.') parser.add_argument('--org', type=str, help='The GitHub organization to check.') parser.add_argument('--dry-run', action='store_true', default=False, help='Enable Debug/Dry-Run mode.') +parser.add_argument('--comment', action='store_true', default=False, help='Set Commit Message to Title and URL. Default to using existing PR Title / Body') args = parser.parse_args() _LOGGER: Final = logging.getLogger(__name__) @@ -41,11 +42,12 @@ def run_git_command(command_args: str) -> subprocess.CompletedProcess: github = Github(login_or_token=os.environ['GITHUB_TOKEN']) repo = args.org + "/" + args.repo +repo = github.get_repo(repo) # 1. Get PRs that are: # - Open. open_prs = [] -for pr in github.get_repo(repo).get_pulls(): +for pr in repo.get_pulls(): if pr.state == 'open': open_prs.append(pr) pr_string = '\n'.join(map(pr_to_display_string, open_prs)) @@ -73,72 +75,71 @@ def run_git_command(command_args: str) -> subprocess.CompletedProcess: 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') +_LOGGER.info(f'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 = [] + +# 3. Sort PRs into 3 categories +# - Up-to-date and passing +# - Up-to-date and behind +# - Pending/Blocked PRs +up_to_date_passing_prs = [] +do_nothing_pending_prs = [] +out_of_date_passing_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 -while automerge_up_to_date_prs: - pr = automerge_up_to_date_prs[0] - _LOGGER.info(f' Merging PR:\n{pr_to_display_string(pr)}\n') + base_branch = repo.get_branch(pr.base.ref) + if base_branch.protected: + required_status_checks = base_branch.get_required_status_checks() + latest_commit = pr.get_commits().reversed[0] + latest_commit_checks = {check_run.name: check_run for check_run in latest_commit.get_check_runs()} + all_checks_passed = True + for required_check in required_status_checks.contexts: + if required_check not in latest_commit_checks: + print(f"Required check {required_check} is missing in the latest commit.") + all_checks_passed = False + else: + check_run = latest_commit_checks[required_check] + if check_run.conclusion == 'success': + print(f"Required check {required_check} passed on PR#{pr.number}") + else: + print(f"Required check {required_check} failed or is pending on PR#{pr.number}") + all_checks_passed = False + commit = [c for c in pr.get_commits() if c.sha == pr.head.sha][0] + combined_status = commit.get_combined_status().state + if pr.mergeable_state == 'clean' and all_checks_passed: + up_to_date_passing_prs.append(pr) + elif pr.mergeable_state == 'behind' or pr.mergeable_state == 'blocked': + if all_checks_passed: + out_of_date_passing_prs.append(pr) + else: + do_nothing_pending_prs.append(pr) +pr_string = '\n'.join(map(pr_to_display_string, up_to_date_passing_prs)) +_LOGGER.info(f' Automerge Approved Up-to-Date PRs:\n{pr_string}\n') +pr_string = '\n'.join(map(pr_to_display_string, out_of_date_passing_prs)) +_LOGGER.info(f' Update Out-of-Date Passing PRs:\n{pr_string}\n') +pr_string = '\n'.join(map(pr_to_display_string, do_nothing_pending_prs)) +_LOGGER.info(f' Do Nothing Pending/Failing PRs:\n{pr_string}\n') + +while up_to_date_passing_prs: + pr = up_to_date_passing_prs[0] if args.dry_run: _LOGGER.info(f'Would have merged PR:\n{pr_to_display_string(pr)}\n') + _LOGGER.info(f'With Comment: {args.comment}') + _LOGGER.info(f"With Comment Message would be: \nAutomerged: [{pr.title}]({pr.html_url})") else: - pr.merge(merge_method='squash', commit_message=f'Automerge {pr.html_url}: {pr.title}') - automerge_up_to_date_prs.pop(0) - -# 5. Get PRs that are: - # - Open, - # - Labelled as `automerge`, - # - Approved, - # - Up-to-date, and - # - Pending tests. -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' Waiting on approved up-to-date pending/failing PRs:\n{pr_string}\n') - + if args.comment: + pr.merge(merge_method='squash', commit_message=f'Automerged {pr.html_url}: {pr.title}') + else: + pr.merge(merge_method='squash') + up_to_date_passing_prs.pop(0) + +while out_of_date_passing_prs: + pr = out_of_date_passing_prs[0] + if args.dry_run: + _LOGGER.info(f'Would have updated PR:\n{pr_to_display_string(pr)}\n') + else: + pr.update_branch() + out_of_date_passing_prs.pop(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' 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() +_LOGGER.info('Done.')