-
Notifications
You must be signed in to change notification settings - Fork 0
ROX-34007: Enhance auto-merge configurability #91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e8e310b
8036b7d
db189d3
92ef0f2
d19bfca
3b1b681
63ad0ed
9c70984
59de5bb
c3f2894
e320206
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,21 +2,37 @@ name: Auto-merge PRs | |||||
| description: Enable auto-merge for eligible PRs with specified labels | ||||||
|
|
||||||
| inputs: | ||||||
| allowed-authors: | ||||||
| description: 'Authors to filter PRs for auto-merge (comma-separated)' | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: "Authors to filter PRs for auto-merge (comma-separated)" is confusing. How about "Authors (comma-separated) whose PRs will be auto-merged". Also, please mention what happens when its value is empty. See related #91 (comment) |
||||||
| required: false | ||||||
| default: 'app/dependabot' | ||||||
| allowed-base-branches: | ||||||
| description: 'Allowed base branches for auto-merge (regex)' | ||||||
| required: false | ||||||
| default: '.*' | ||||||
| dry-run: | ||||||
| description: 'Whether to dry-run the auto-merge' | ||||||
| required: false | ||||||
| default: 'false' | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the default for dry-run parameter be
Suggested change
|
||||||
| github-token: | ||||||
| description: 'GitHub token with permissions to merge PRs and approve reviews' | ||||||
| required: true | ||||||
| repository: | ||||||
| description: 'Repository in owner/repo format' | ||||||
| required: false | ||||||
| default: ${{ github.repository }} | ||||||
| label: | ||||||
| description: 'Label to filter PRs for auto-merge' | ||||||
| labels: | ||||||
| description: 'Labels to filter PRs for auto-merge (comma-separated `and` logic)' | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd suggest something like "Only PRs having these labels will be merged. Multiple labels can be specified as comma-separated, each of them must be present on the PR." Also, please mention what happens when an empty value is specified. See related #91 (comment) |
||||||
| required: false | ||||||
| default: 'auto-merge' | ||||||
| limit: | ||||||
| description: 'Maximum number of PRs to process per run.' | ||||||
| required: false | ||||||
| default: '50' | ||||||
| repository: | ||||||
kurlov marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| description: 'Repository in owner/repo format' | ||||||
| required: false | ||||||
| default: ${{ github.repository }} | ||||||
| required-checks: | ||||||
| description: 'Required checks to pass for auto-merge (regex)' | ||||||
| required: false | ||||||
| default: '.*' | ||||||
|
|
||||||
| runs: | ||||||
| using: composite | ||||||
|
|
@@ -25,74 +41,15 @@ runs: | |||||
| shell: bash | ||||||
| env: | ||||||
| GH_TOKEN: ${{ inputs.github-token }} | ||||||
| DRY_RUN: ${{ inputs.dry-run }} | ||||||
| run: | | ||||||
| set -euo pipefail | ||||||
|
|
||||||
| # Extract repo owner and name | ||||||
| IFS='/' read -r OWNER REPO <<< "${{ inputs.repository }}" | ||||||
|
|
||||||
| echo "::notice::Querying PRs with '${{ inputs.label }}' label in ${{ inputs.repository }}" | ||||||
|
|
||||||
| # Get all PRs with auto-merge labels (non-draft, mergeable only) | ||||||
| PR_DATA=$(gh pr list \ | ||||||
| --repo "${{ inputs.repository }}" \ | ||||||
| --label "${{ inputs.label }}" \ | ||||||
| --draft=false \ | ||||||
| --state open \ | ||||||
| --limit "${{ inputs.limit }}" \ | ||||||
| --json number,mergeable,author \ | ||||||
| --jq ".[] | select(.mergeable == \"MERGEABLE\") | {number, author: .author.login}") | ||||||
|
|
||||||
| if [[ -z "$PR_DATA" ]]; then | ||||||
| echo "::notice::No eligible PRs found with '${{ inputs.label }}' label" | ||||||
| exit 0 | ||||||
| fi | ||||||
|
|
||||||
| # Process each PR | ||||||
| echo "$PR_DATA" | jq -c '.' | while read -r PR_JSON; do | ||||||
| PR_NUMBER=$(echo "$PR_JSON" | jq -r '.number') | ||||||
| AUTHOR=$(echo "$PR_JSON" | jq -r '.author') | ||||||
|
|
||||||
| echo "::notice::Processing PR #$PR_NUMBER (author=$AUTHOR)" | ||||||
|
|
||||||
| # Check if all checks have passed using GraphQL statusCheckRollup | ||||||
| STATUS=$(gh api graphql -F owner="$OWNER" -F repo="$REPO" -F number="$PR_NUMBER" -f query=" | ||||||
| query(\$owner: String!, \$repo: String!, \$number: Int!) { | ||||||
| repository(owner: \$owner, name: \$repo) { | ||||||
| pullRequest(number: \$number) { | ||||||
| commits(last: 1) { | ||||||
| nodes { | ||||||
| commit { | ||||||
| statusCheckRollup { | ||||||
| state | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| " --jq ".data.repository.pullRequest.commits.nodes[0].commit.statusCheckRollup.state" || echo "null") | ||||||
|
|
||||||
| echo "::notice::PR #$PR_NUMBER status check rollup: $STATUS" | ||||||
|
|
||||||
| # Only proceed if all checks passed | ||||||
| if [[ "$STATUS" != "SUCCESS" ]]; then | ||||||
| echo "::warning::Skipping PR #$PR_NUMBER - checks not passed (status: $STATUS)" | ||||||
| continue | ||||||
| fi | ||||||
|
|
||||||
| # Enable auto-merge for all PRs with the label | ||||||
| echo "::notice::Enabling auto-merge for PR #$PR_NUMBER" | ||||||
| gh pr merge --repo "${{ inputs.repository }}" \ | ||||||
| --auto --squash "$PR_NUMBER" | ||||||
|
|
||||||
| # Auto-approve only Dependabot PRs | ||||||
| if [[ "$AUTHOR" == "app/dependabot" ]]; then | ||||||
| echo "::notice::Approving Dependabot PR #$PR_NUMBER" | ||||||
| gh pr review --repo "${{ inputs.repository }}" \ | ||||||
| --approve "$PR_NUMBER" || true | ||||||
| fi | ||||||
|
|
||||||
| echo "::notice::✓ Auto-merge enabled for PR #$PR_NUMBER" | ||||||
| done | ||||||
| "${GITHUB_ACTION_PATH}/../common/common.sh" \ | ||||||
| "${GITHUB_ACTION_PATH}/automerge.sh" \ | ||||||
| "${{ inputs.repository }}" \ | ||||||
| "${{ inputs.limit }}" \ | ||||||
| "${{ inputs.labels }}" \ | ||||||
| "${{ inputs.allowed-authors }}" \ | ||||||
| "${{ inputs.required-checks }}" \ | ||||||
| "${{ inputs.allowed-base-branches }}" | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,179 @@ | ||||||||||
| #!/bin/bash | ||||||||||
| # | ||||||||||
| # Enables auto-merge for eligible PRs with specified labels. | ||||||||||
| # PRs can be filtered by labels, base branches, and allowed author. | ||||||||||
| # Required status checks must pass for auto-merge to be enabled. | ||||||||||
| # | ||||||||||
| # Local run: | ||||||||||
| # | ||||||||||
| # test/local-env.sh automerge/automerge.sh <repository> <limit> <label1,label2,...> <allowed-author> <required-checks> <allowed-base-branches> | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| # | ||||||||||
|
|
||||||||||
| set -euo pipefail | ||||||||||
|
|
||||||||||
| function main() { | ||||||||||
| REPOSITORY="${1:-}" | ||||||||||
| LIMIT="${2:-}" | ||||||||||
| LABELS="${3:-}" | ||||||||||
| ALLOWED_AUTHORS="${4:-}" | ||||||||||
| REQUIRED_CHECKS="${5:-}" | ||||||||||
| ALLOWED_BASE_BRANCHES="${6:-}" | ||||||||||
|
|
||||||||||
| check_not_empty \ | ||||||||||
| DRY_RUN GH_TOKEN \ | ||||||||||
| REPOSITORY LIMIT LABELS ALLOWED_AUTHORS REQUIRED_CHECKS ALLOWED_BASE_BRANCHES | ||||||||||
|
Comment on lines
+22
to
+24
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm. Since you have |
||||||||||
|
|
||||||||||
| gh_log notice "Querying PRs with '${LABELS}' label(s) in '${REPOSITORY}', allowed authors: '${ALLOWED_AUTHORS}', required checks: '${REQUIRED_CHECKS}', allowed base branches: '${ALLOWED_BASE_BRANCHES}'" | ||||||||||
| gh_log notice "DRY_RUN: ${DRY_RUN}" | ||||||||||
|
|
||||||||||
| # Extract repo owner and name | ||||||||||
| IFS='/' read -r OWNER REPO <<< "${REPOSITORY}" | ||||||||||
|
|
||||||||||
| # Get all PRs with auto-merge labels (non-draft, mergeable only) | ||||||||||
| PR_DATA=$(gh pr list \ | ||||||||||
| --repo "${REPOSITORY}" \ | ||||||||||
| --label "${LABELS}" \ | ||||||||||
| --draft=false \ | ||||||||||
| --state open \ | ||||||||||
| --limit "${LIMIT}" \ | ||||||||||
| --json number,mergeable,author,baseRefName \ | ||||||||||
| --jq ".[] | select(.mergeable == \"MERGEABLE\") | {number, author: .author.login, baseRefName: .baseRefName}") | ||||||||||
|
|
||||||||||
| if [[ -z "${PR_DATA}" ]]; then | ||||||||||
| gh_log notice "No eligible PRs found with '${LABELS}' labels" | ||||||||||
| exit 0 | ||||||||||
| fi | ||||||||||
|
|
||||||||||
| # Process each PR | ||||||||||
| echo "${PR_DATA}" | jq -c '.' | while read -r PR_JSON; do | ||||||||||
| PR_NUMBER=$(echo "$PR_JSON" | jq -r '.number') | ||||||||||
| AUTHOR=$(echo "$PR_JSON" | jq -r '.author') | ||||||||||
| BASE_BRANCH=$(echo "$PR_JSON" | jq -r '.baseRefName') | ||||||||||
|
|
||||||||||
| echo "[DEBUG] PR #${PR_NUMBER} - author='${AUTHOR}', base branch='${BASE_BRANCH}'" | ||||||||||
| if [[ ! "${BASE_BRANCH}" =~ ^(${ALLOWED_BASE_BRANCHES})$ ]]; then | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd vote for
Suggested change
This would make things less surprising should you try using |
||||||||||
| echo "[DEBUG] PR #${PR_NUMBER} skipped - base branch '${BASE_BRANCH}' not allowed" | ||||||||||
| continue | ||||||||||
|
Comment on lines
+55
to
+56
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The default Is there any way to randomize the results of Alternatively, |
||||||||||
| fi | ||||||||||
|
|
||||||||||
| STATUS="$(get_combined_success_status "${PR_NUMBER}")" | ||||||||||
|
|
||||||||||
| # Only proceed if the required checks have passed | ||||||||||
| if [[ "${STATUS}" == "true" ]]; then | ||||||||||
| echo "[DEBUG] ✓ PR #${PR_NUMBER} - all required checks passed or skipped" | ||||||||||
| else | ||||||||||
| echo "[DEBUG] x PR #${PR_NUMBER} skipped - not all required checks passed or skipped" | ||||||||||
| continue | ||||||||||
| fi | ||||||||||
|
|
||||||||||
| # Enable auto-merge for all PRs with the label(s) | ||||||||||
| if [[ "${DRY_RUN}" == "true" ]]; then | ||||||||||
| echo "[DEBUG] ✓ PR #${PR_NUMBER} - would have enabled auto-merge [DRY RUN]" | ||||||||||
| else | ||||||||||
| gh pr merge --repo "${REPOSITORY}" \ | ||||||||||
| --auto --squash "${PR_NUMBER}" | ||||||||||
|
Comment on lines
+73
to
+74
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd suggest indenting here.
Suggested change
|
||||||||||
| echo "[DEBUG] ✓ PR #${PR_NUMBER} - auto-merge enabled" | ||||||||||
| fi | ||||||||||
|
|
||||||||||
| # Approve only PRs by allowed authors | ||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know you did not change that logic as compared to the original implementation, but I wonder what is the scenario when we want to enable auto-merge but don't want to approve? |
||||||||||
| IFS=',' read -r -a ALLOWED_AUTHORS_ARRAY <<< "${ALLOWED_AUTHORS}" | ||||||||||
| if [[ " ${ALLOWED_AUTHORS_ARRAY[*]} " == *"${AUTHOR}"* ]]; then | ||||||||||
|
Comment on lines
+79
to
+80
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this work?
Suggested change
|
||||||||||
| if [[ "${DRY_RUN}" == "true" ]]; then | ||||||||||
| echo "[DEBUG] ✓ PR #${PR_NUMBER} - would have approved [DRY RUN]" | ||||||||||
| else | ||||||||||
| gh pr review --repo "${REPOSITORY}" \ | ||||||||||
| --approve "${PR_NUMBER}" | ||||||||||
|
Comment on lines
+84
to
+85
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I spotted the older command had |
||||||||||
| echo "[DEBUG] ✓ PR #${PR_NUMBER} - approved" | ||||||||||
| fi | ||||||||||
| else | ||||||||||
| echo "[DEBUG] x PR #${PR_NUMBER} not approved - author '${AUTHOR}' not in allowed authors" | ||||||||||
| fi | ||||||||||
| done | ||||||||||
| } | ||||||||||
|
|
||||||||||
| # Collects all status checks and checkruns for the PR and returns a boolean indicating if all required checks have passed or been skipped. | ||||||||||
| # The REQUIRED_CHECKS regex parameter must return at least one check. | ||||||||||
| function get_combined_success_status() { | ||||||||||
| PAGE_SIZE=100 | ||||||||||
| CURSOR="" | ||||||||||
| NODES_JSON='[]' | ||||||||||
|
|
||||||||||
| # shellcheck disable=SC2016 | ||||||||||
| QUERY=' | ||||||||||
| query($owner: String!, $repo: String!, $number: Int!, $first: Int!, $after: String) { | ||||||||||
| repository(owner: $owner, name: $repo) { | ||||||||||
| pullRequest(number: $number) { | ||||||||||
| commits(last: 1) { | ||||||||||
| nodes { | ||||||||||
| commit { | ||||||||||
| statusCheckRollup { | ||||||||||
| contexts(first: $first, after: $after) { | ||||||||||
| pageInfo { | ||||||||||
| hasNextPage | ||||||||||
| endCursor | ||||||||||
| } | ||||||||||
| nodes { | ||||||||||
| ... on CheckRun { | ||||||||||
| name | ||||||||||
| status | ||||||||||
| conclusion | ||||||||||
| } | ||||||||||
| ... on StatusContext { | ||||||||||
| context | ||||||||||
| state | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| } | ||||||||||
| ' | ||||||||||
|
|
||||||||||
| while true; do | ||||||||||
| ARGS=( | ||||||||||
| graphql | ||||||||||
| -F owner="$OWNER" | ||||||||||
| -F repo="$REPO" | ||||||||||
| -F number="$PR_NUMBER" | ||||||||||
| -F first="$PAGE_SIZE" | ||||||||||
| -f query="$QUERY" | ||||||||||
| ) | ||||||||||
| if [[ -n "${CURSOR}" ]]; then | ||||||||||
| ARGS+=(-F after="${CURSOR}") | ||||||||||
| fi | ||||||||||
|
|
||||||||||
| RESP=$(gh api "${ARGS[@]}") | ||||||||||
|
|
||||||||||
| PAGE_NODES=$(echo "${RESP}" | jq '.data.repository.pullRequest.commits.nodes[0].commit.statusCheckRollup.contexts.nodes // []') | ||||||||||
| NODES_JSON=$(jq -n --argjson acc "${NODES_JSON}" --argjson page "${PAGE_NODES}" '$acc + $page') | ||||||||||
|
|
||||||||||
| HAS_NEXT=$(echo "${RESP}" | jq -r '.data.repository.pullRequest.commits.nodes[0].commit.statusCheckRollup.contexts.pageInfo.hasNextPage // false') | ||||||||||
| if [[ "${HAS_NEXT}" != "true" ]]; then | ||||||||||
| break | ||||||||||
| fi | ||||||||||
| CURSOR=$(echo "${RESP}" | jq -r '.data.repository.pullRequest.commits.nodes[0].commit.statusCheckRollup.contexts.pageInfo.endCursor // empty') | ||||||||||
| if [[ -z "${CURSOR}" ]]; then | ||||||||||
| gh_log error "Pagination indicated hasNextPage but endCursor is empty" | ||||||||||
| exit 1 | ||||||||||
| fi | ||||||||||
| done | ||||||||||
|
|
||||||||||
| echo "$NODES_JSON" | jq -r --arg pattern "${REQUIRED_CHECKS}" ' | ||||||||||
| [.[] | ||||||||||
| | select( | ||||||||||
| (.name != null and (.name | test($pattern))) | ||||||||||
| or (.context != null and (.context | test($pattern))) | ||||||||||
| ) | ||||||||||
| | if .name != null then {conclusion: .conclusion} | ||||||||||
| else {conclusion: .state} | ||||||||||
| end | ||||||||||
| ] | ||||||||||
| | length > 0 and all(.conclusion == "SUCCESS" or .conclusion == "SKIPPED") | ||||||||||
| ' | ||||||||||
| } | ||||||||||
|
|
||||||||||
| main "$@" | ||||||||||
Uh oh!
There was an error while loading. Please reload this page.