From e8e310b2888202b5aacdace2bebc98ad6d86647a Mon Sep 17 00:00:00 2001 From: Tom Martensen Date: Fri, 10 Apr 2026 14:23:21 +0200 Subject: [PATCH 01/13] Enhance auto-merge configurability --- automerge/README.md | 16 ++-- automerge/action.yml | 101 +++++++----------------- automerge/automerge.sh | 171 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 78 deletions(-) create mode 100755 automerge/automerge.sh diff --git a/automerge/README.md b/automerge/README.md index db8b333..60c4327 100644 --- a/automerge/README.md +++ b/automerge/README.md @@ -1,23 +1,27 @@ # Auto-merge Action -This composite action enables auto-merge for eligible pull requests based on specified labels. +This composite action enables auto-merge for eligible pull requests based on specified labels. Defaults to approving Dependabot PRs with green CI against all branches. ## Features -- Finds PRs with specified label (default: `auto-merge`) +- Finds PRs with specified label (default: `auto-merge`) for the allowed base branches - Verifies PRs are in mergeable state (non-draft) -- Checks that all status checks have passed +- Checks that required status checks have passed - Enables auto-merge with squash strategy -- Auto-approves Dependabot PRs +- Auto-approves PRs for allowed author ## Inputs | Input | Description | Required | Default | |-------|-------------|----------|---------| +| `allowed-authors` | Authors to filter PRs for auto-merge (regex)| No | `app/dependabot` | +| `allowed-base-branches` | Allowed base branches for auto-merge (regex) | No | `.*` | +| `dry-run` | Whether to dry-run the auto-merge | No | `false` | | `github-token` | GitHub token with permissions to merge PRs and approve reviews (`contents: write` and `pull-requests: write` permissions) | Yes | - | +| `labels` | Labels to filter PRs for auto-merge (comma-separated `and` logic) | No | `auto-merge` | +| `limit` | Maximum number of PRs to process per run | No | `50` | | `repository` | Repository in owner/repo format | No | `${{ github.repository }}` | -| `label` | Label to filter PRs for auto-merge | No | `auto-merge` | -| `limit` | Maximum number of PRs to process | No | `50` | +| `required-checks` | Required checks to pass for auto-merge (regex) | No | `.*` | ## Usage diff --git a/automerge/action.yml b/automerge/action.yml index d106e18..118070c 100644 --- a/automerge/action.yml +++ b/automerge/action.yml @@ -2,13 +2,21 @@ name: Auto-merge PRs description: Enable auto-merge for eligible PRs with specified labels inputs: + allowed-author: + description: 'Author to filter PRs for auto-merge' + required: false + default: 'app/dependabot' + allowed-base-branches: + description: 'Allowed base branches for auto-merge' + required: false + default: '.*' + dry-run: + description: 'Whether to dry-run the auto-merge' + required: false + default: 'false' 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' required: false @@ -17,6 +25,14 @@ inputs: description: 'Maximum number of PRs to process per run.' required: false default: '50' + repository: + description: 'Repository in owner/repo format' + required: false + default: ${{ github.repository }} + required-checks: + description: 'Required checks to pass for auto-merge' + 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.label }}" \ + "${{ inputs.allowed-author }}" \ + "${{ inputs.required-checks }}" \ + "${{ inputs.allowed-base-branches }}" diff --git a/automerge/automerge.sh b/automerge/automerge.sh new file mode 100755 index 0000000..e83f150 --- /dev/null +++ b/automerge/automerge.sh @@ -0,0 +1,171 @@ +#!/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 +# + +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 + + 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') + + gh_log notice "PR #${PR_NUMBER} - author='${AUTHOR}', base branch='${BASE_BRANCH}'" + if [[ ! "${BASE_BRANCH}" =~ ^(${ALLOWED_BASE_BRANCHES})$ ]]; then + gh_log warning "PR #${PR_NUMBER} skipped - base branch '${BASE_BRANCH}' not allowed" + continue + fi + + STATUS="$(get_combined_success_status "${PR_NUMBER}")" + + # Only proceed if the required checks have passed + if [[ "${STATUS}" == "true" ]]; then + gh_log notice "✓ PR #${PR_NUMBER} - all required checks passed or skipped" + else + gh_log warning "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 + gh_log notice "✓ PR #${PR_NUMBER} - would have enabled auto-merge [DRY RUN]" + else + gh pr merge --repo "${REPOSITORY}" \ + --auto --squash "${PR_NUMBER}" + gh_log notice "✓ PR #${PR_NUMBER} - auto-merge enabled" + fi + + # Approve only PRs by allowed authors + if [[ "${AUTHOR}" =~ ^(${ALLOWED_AUTHORS})$ ]]; then + if [[ "${DRY_RUN}" == "true" ]]; then + gh_log notice "✓ PR #${PR_NUMBER} - would have approved [DRY RUN]" + else + gh pr review --repo "${REPOSITORY}" \ + --approve "${PR_NUMBER}" + gh_log notice "✓ PR #${PR_NUMBER} - approved" + fi + else + gh_log warning "x PR #${PR_NUMBER} not approved - author '${AUTHOR}' not in allowed authors" + fi + done +} + +function get_combined_success_status() { + PAGE_SIZE=100 + CURSOR="" + NODES_JSON='[]' + + 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 + } + } + } + } + } + } + } + } + } + } + ' + + 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 + echo "::error::pagination indicated hasNextPage but endCursor is empty" >&2 + exit 1 + fi + done + + echo "$NODES_JSON" | jq ' + map(select( + .name != null + and (.name | test("'"${REQUIRED_CHECKS}"'")) + )) + | { + ALL_SUCCESS: (length > 0 and all(.conclusion == "SUCCESS" or .conclusion == "SKIPPED")) + } | .ALL_SUCCESS + ' +} + +main "$@" From 8036b7da6a040b0978e3f7d714fe57acdab768ef Mon Sep 17 00:00:00 2001 From: Tom Martensen Date: Fri, 10 Apr 2026 14:27:41 +0200 Subject: [PATCH 02/13] ignore shellcheck warning for GH API GraphQL query --- automerge/automerge.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/automerge/automerge.sh b/automerge/automerge.sh index e83f150..ce7b63f 100755 --- a/automerge/automerge.sh +++ b/automerge/automerge.sh @@ -98,6 +98,7 @@ function get_combined_success_status() { CURSOR="" NODES_JSON='[]' + # shellcheck disable=SC2016 QUERY=' query($owner: String!, $repo: String!, $number: Int!, $first: Int!, $after: String) { repository(owner: $owner, name: $repo) { From db189d3c1ce0590d29317885be9ecfc34e6e5010 Mon Sep 17 00:00:00 2001 From: Tom Martensen Date: Fri, 10 Apr 2026 14:32:20 +0200 Subject: [PATCH 03/13] test dry-run input --- automerge/automerge.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/automerge/automerge.sh b/automerge/automerge.sh index ce7b63f..0878e56 100755 --- a/automerge/automerge.sh +++ b/automerge/automerge.sh @@ -29,6 +29,8 @@ function main() { 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}" + exit 1 + # Extract repo owner and name IFS='/' read -r OWNER REPO <<< "${REPOSITORY}" From 92ef0f27b1eb089de04e3501c404e3ad879467e7 Mon Sep 17 00:00:00 2001 From: Tom Martensen Date: Fri, 10 Apr 2026 14:37:09 +0200 Subject: [PATCH 04/13] fix path to common.sh --- automerge/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automerge/action.yml b/automerge/action.yml index 118070c..db6d094 100644 --- a/automerge/action.yml +++ b/automerge/action.yml @@ -45,7 +45,7 @@ runs: run: | set -euo pipefail - "${GITHUB_ACTION_PATH}/../../common/common.sh" \ + "${GITHUB_ACTION_PATH}/../common/common.sh" \ "${GITHUB_ACTION_PATH}/automerge.sh" \ "${{ inputs.repository }}" \ "${{ inputs.limit }}" \ From d19bfcac8d5bb9501ae507c277bea00fd294dc76 Mon Sep 17 00:00:00 2001 From: Tom Martensen Date: Fri, 10 Apr 2026 14:41:37 +0200 Subject: [PATCH 05/13] less verbose output --- automerge/automerge.sh | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/automerge/automerge.sh b/automerge/automerge.sh index 0878e56..7a64811 100755 --- a/automerge/automerge.sh +++ b/automerge/automerge.sh @@ -1,7 +1,4 @@ #!/bin/bash - -# - # # Enables auto-merge for eligible PRs with specified labels. # PRs can be filtered by labels, base branches, and allowed author. @@ -55,9 +52,9 @@ function main() { AUTHOR=$(echo "$PR_JSON" | jq -r '.author') BASE_BRANCH=$(echo "$PR_JSON" | jq -r '.baseRefName') - gh_log notice "PR #${PR_NUMBER} - author='${AUTHOR}', base branch='${BASE_BRANCH}'" + gh_summary "PR #${PR_NUMBER} - author='${AUTHOR}', base branch='${BASE_BRANCH}'" if [[ ! "${BASE_BRANCH}" =~ ^(${ALLOWED_BASE_BRANCHES})$ ]]; then - gh_log warning "PR #${PR_NUMBER} skipped - base branch '${BASE_BRANCH}' not allowed" + gh_summary "PR #${PR_NUMBER} skipped - base branch '${BASE_BRANCH}' not allowed" continue fi @@ -65,32 +62,32 @@ function main() { # Only proceed if the required checks have passed if [[ "${STATUS}" == "true" ]]; then - gh_log notice "✓ PR #${PR_NUMBER} - all required checks passed or skipped" + gh_summary "✓ PR #${PR_NUMBER} - all required checks passed or skipped" else - gh_log warning "x PR #${PR_NUMBER} skipped - not all required checks passed or skipped" + gh_summary "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 - gh_log notice "✓ PR #${PR_NUMBER} - would have enabled auto-merge [DRY RUN]" + gh_summary "✓ PR #${PR_NUMBER} - would have enabled auto-merge [DRY RUN]" else gh pr merge --repo "${REPOSITORY}" \ --auto --squash "${PR_NUMBER}" - gh_log notice "✓ PR #${PR_NUMBER} - auto-merge enabled" + gh_summary "✓ PR #${PR_NUMBER} - auto-merge enabled" fi # Approve only PRs by allowed authors if [[ "${AUTHOR}" =~ ^(${ALLOWED_AUTHORS})$ ]]; then if [[ "${DRY_RUN}" == "true" ]]; then - gh_log notice "✓ PR #${PR_NUMBER} - would have approved [DRY RUN]" + gh_summary "✓ PR #${PR_NUMBER} - would have approved [DRY RUN]" else gh pr review --repo "${REPOSITORY}" \ --approve "${PR_NUMBER}" - gh_log notice "✓ PR #${PR_NUMBER} - approved" + gh_summary "✓ PR #${PR_NUMBER} - approved" fi else - gh_log warning "x PR #${PR_NUMBER} not approved - author '${AUTHOR}' not in allowed authors" + gh_summary "x PR #${PR_NUMBER} not approved - author '${AUTHOR}' not in allowed authors" fi done } From 3b1b6811e91add7107204163d35fb5e513b58877 Mon Sep 17 00:00:00 2001 From: Tom Martensen Date: Fri, 10 Apr 2026 14:53:39 +0200 Subject: [PATCH 06/13] undo dry-run guard, fix input parameters --- automerge/action.yml | 16 ++++++++-------- automerge/automerge.sh | 2 -- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/automerge/action.yml b/automerge/action.yml index db6d094..9026485 100644 --- a/automerge/action.yml +++ b/automerge/action.yml @@ -2,12 +2,12 @@ name: Auto-merge PRs description: Enable auto-merge for eligible PRs with specified labels inputs: - allowed-author: - description: 'Author to filter PRs for auto-merge' + allowed-authors: + description: 'Authors to filter PRs for auto-merge (regex)' required: false default: 'app/dependabot' allowed-base-branches: - description: 'Allowed base branches for auto-merge' + description: 'Allowed base branches for auto-merge (regex)' required: false default: '.*' dry-run: @@ -17,8 +17,8 @@ inputs: github-token: description: 'GitHub token with permissions to merge PRs and approve reviews' required: true - label: - description: 'Label to filter PRs for auto-merge' + labels: + description: 'Labels to filter PRs for auto-merge (comma-separated `and` logic)' required: false default: 'auto-merge' limit: @@ -30,7 +30,7 @@ inputs: required: false default: ${{ github.repository }} required-checks: - description: 'Required checks to pass for auto-merge' + description: 'Required checks to pass for auto-merge (regex)' required: false default: '.*' @@ -49,7 +49,7 @@ runs: "${GITHUB_ACTION_PATH}/automerge.sh" \ "${{ inputs.repository }}" \ "${{ inputs.limit }}" \ - "${{ inputs.label }}" \ - "${{ inputs.allowed-author }}" \ + "${{ inputs.labels }}" \ + "${{ inputs.allowed-authors }}" \ "${{ inputs.required-checks }}" \ "${{ inputs.allowed-base-branches }}" diff --git a/automerge/automerge.sh b/automerge/automerge.sh index 7a64811..92313b6 100755 --- a/automerge/automerge.sh +++ b/automerge/automerge.sh @@ -26,8 +26,6 @@ function main() { 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}" - exit 1 - # Extract repo owner and name IFS='/' read -r OWNER REPO <<< "${REPOSITORY}" From 63ad0edf3e13a56d7d89145439c91f4532d3199a Mon Sep 17 00:00:00 2001 From: Tom Martensen Date: Fri, 10 Apr 2026 15:00:28 +0200 Subject: [PATCH 07/13] fix: use gh_log debug instead of gh_summary --- automerge/automerge.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/automerge/automerge.sh b/automerge/automerge.sh index 92313b6..b800978 100755 --- a/automerge/automerge.sh +++ b/automerge/automerge.sh @@ -50,9 +50,9 @@ function main() { AUTHOR=$(echo "$PR_JSON" | jq -r '.author') BASE_BRANCH=$(echo "$PR_JSON" | jq -r '.baseRefName') - gh_summary "PR #${PR_NUMBER} - author='${AUTHOR}', base branch='${BASE_BRANCH}'" + gh_log debug "PR #${PR_NUMBER} - author='${AUTHOR}', base branch='${BASE_BRANCH}'" if [[ ! "${BASE_BRANCH}" =~ ^(${ALLOWED_BASE_BRANCHES})$ ]]; then - gh_summary "PR #${PR_NUMBER} skipped - base branch '${BASE_BRANCH}' not allowed" + gh_log debug "PR #${PR_NUMBER} skipped - base branch '${BASE_BRANCH}' not allowed" continue fi @@ -60,32 +60,32 @@ function main() { # Only proceed if the required checks have passed if [[ "${STATUS}" == "true" ]]; then - gh_summary "✓ PR #${PR_NUMBER} - all required checks passed or skipped" + gh_log debug "✓ PR #${PR_NUMBER} - all required checks passed or skipped" else - gh_summary "x PR #${PR_NUMBER} skipped - not all required checks passed or skipped" + gh_log 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 - gh_summary "✓ PR #${PR_NUMBER} - would have enabled auto-merge [DRY RUN]" + gh_log debug "✓ PR #${PR_NUMBER} - would have enabled auto-merge [DRY RUN]" else gh pr merge --repo "${REPOSITORY}" \ --auto --squash "${PR_NUMBER}" - gh_summary "✓ PR #${PR_NUMBER} - auto-merge enabled" + gh_log debug "✓ PR #${PR_NUMBER} - auto-merge enabled" fi # Approve only PRs by allowed authors if [[ "${AUTHOR}" =~ ^(${ALLOWED_AUTHORS})$ ]]; then if [[ "${DRY_RUN}" == "true" ]]; then - gh_summary "✓ PR #${PR_NUMBER} - would have approved [DRY RUN]" + gh_log debug "✓ PR #${PR_NUMBER} - would have approved [DRY RUN]" else gh pr review --repo "${REPOSITORY}" \ --approve "${PR_NUMBER}" - gh_summary "✓ PR #${PR_NUMBER} - approved" + gh_log debug "✓ PR #${PR_NUMBER} - approved" fi else - gh_summary "x PR #${PR_NUMBER} not approved - author '${AUTHOR}' not in allowed authors" + gh_log debug "x PR #${PR_NUMBER} not approved - author '${AUTHOR}' not in allowed authors" fi done } From 9c7098462e315c0d2b31b0ff1b9eb5c6b9ebada2 Mon Sep 17 00:00:00 2001 From: Tom Martensen Date: Fri, 10 Apr 2026 15:02:56 +0200 Subject: [PATCH 08/13] improvement to output --- automerge/automerge.sh | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/automerge/automerge.sh b/automerge/automerge.sh index b800978..52a83db 100755 --- a/automerge/automerge.sh +++ b/automerge/automerge.sh @@ -50,9 +50,9 @@ function main() { AUTHOR=$(echo "$PR_JSON" | jq -r '.author') BASE_BRANCH=$(echo "$PR_JSON" | jq -r '.baseRefName') - gh_log debug "PR #${PR_NUMBER} - author='${AUTHOR}', base branch='${BASE_BRANCH}'" + echo "[DEBUG] PR #${PR_NUMBER} - author='${AUTHOR}', base branch='${BASE_BRANCH}'" if [[ ! "${BASE_BRANCH}" =~ ^(${ALLOWED_BASE_BRANCHES})$ ]]; then - gh_log debug "PR #${PR_NUMBER} skipped - base branch '${BASE_BRANCH}' not allowed" + echo "[DEBUG] PR #${PR_NUMBER} skipped - base branch '${BASE_BRANCH}' not allowed" continue fi @@ -60,32 +60,32 @@ function main() { # Only proceed if the required checks have passed if [[ "${STATUS}" == "true" ]]; then - gh_log debug "✓ PR #${PR_NUMBER} - all required checks passed or skipped" + echo "[DEBUG] ✓ PR #${PR_NUMBER} - all required checks passed or skipped" else - gh_log debug "x PR #${PR_NUMBER} skipped - not all required checks passed or skipped" + 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 - gh_log debug "✓ PR #${PR_NUMBER} - would have enabled auto-merge [DRY RUN]" + echo "[DEBUG] ✓ PR #${PR_NUMBER} - would have enabled auto-merge [DRY RUN]" else gh pr merge --repo "${REPOSITORY}" \ --auto --squash "${PR_NUMBER}" - gh_log debug "✓ PR #${PR_NUMBER} - auto-merge enabled" + echo "[DEBUG] ✓ PR #${PR_NUMBER} - auto-merge enabled" fi # Approve only PRs by allowed authors if [[ "${AUTHOR}" =~ ^(${ALLOWED_AUTHORS})$ ]]; then if [[ "${DRY_RUN}" == "true" ]]; then - gh_log debug "✓ PR #${PR_NUMBER} - would have approved [DRY RUN]" + echo "[DEBUG] ✓ PR #${PR_NUMBER} - would have approved [DRY RUN]" else gh pr review --repo "${REPOSITORY}" \ --approve "${PR_NUMBER}" - gh_log debug "✓ PR #${PR_NUMBER} - approved" + echo "[DEBUG] ✓ PR #${PR_NUMBER} - approved" fi else - gh_log debug "x PR #${PR_NUMBER} not approved - author '${AUTHOR}' not in allowed authors" + echo "[DEBUG] x PR #${PR_NUMBER} not approved - author '${AUTHOR}' not in allowed authors" fi done } From 59de5bbc992b2c9e576a929fad272340af330762 Mon Sep 17 00:00:00 2001 From: Tom Martensen Date: Fri, 10 Apr 2026 15:19:26 +0200 Subject: [PATCH 09/13] self-review --- automerge/automerge.sh | 46 +++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/automerge/automerge.sh b/automerge/automerge.sh index 52a83db..41ffda7 100755 --- a/automerge/automerge.sh +++ b/automerge/automerge.sh @@ -127,32 +127,32 @@ function get_combined_success_status() { ' 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 + 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[@]}") + 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') + 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 - echo "::error::pagination indicated hasNextPage but endCursor is empty" >&2 - exit 1 - fi + 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 ' From c3f2894067eb18ff43d2fe587f7841273335b194 Mon Sep 17 00:00:00 2001 From: Tom Martensen Date: Mon, 13 Apr 2026 16:47:13 +0200 Subject: [PATCH 10/13] address review comments; add StatusChecks for testing --- automerge/README.md | 4 ++-- automerge/action.yml | 2 +- automerge/automerge.sh | 32 +++++++++++++++++++++----------- 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/automerge/README.md b/automerge/README.md index 60c4327..f86053e 100644 --- a/automerge/README.md +++ b/automerge/README.md @@ -14,14 +14,14 @@ This composite action enables auto-merge for eligible pull requests based on spe | Input | Description | Required | Default | |-------|-------------|----------|---------| -| `allowed-authors` | Authors to filter PRs for auto-merge (regex)| No | `app/dependabot` | +| `allowed-authors` | Authors to filter PRs for auto-merge (comma-separated list)| No | `app/dependabot` | | `allowed-base-branches` | Allowed base branches for auto-merge (regex) | No | `.*` | | `dry-run` | Whether to dry-run the auto-merge | No | `false` | | `github-token` | GitHub token with permissions to merge PRs and approve reviews (`contents: write` and `pull-requests: write` permissions) | Yes | - | | `labels` | Labels to filter PRs for auto-merge (comma-separated `and` logic) | No | `auto-merge` | | `limit` | Maximum number of PRs to process per run | No | `50` | | `repository` | Repository in owner/repo format | No | `${{ github.repository }}` | -| `required-checks` | Required checks to pass for auto-merge (regex) | No | `.*` | +| `required-checks` | Required checks to succeed for auto-merge (regex) | No | `.*` | ## Usage diff --git a/automerge/action.yml b/automerge/action.yml index 9026485..2aca43a 100644 --- a/automerge/action.yml +++ b/automerge/action.yml @@ -3,7 +3,7 @@ description: Enable auto-merge for eligible PRs with specified labels inputs: allowed-authors: - description: 'Authors to filter PRs for auto-merge (regex)' + description: 'Authors to filter PRs for auto-merge (comma-separated)' required: false default: 'app/dependabot' allowed-base-branches: diff --git a/automerge/automerge.sh b/automerge/automerge.sh index 41ffda7..2a83acf 100755 --- a/automerge/automerge.sh +++ b/automerge/automerge.sh @@ -21,9 +21,9 @@ function main() { check_not_empty \ DRY_RUN GH_TOKEN \ - REPOSITORY LIMIT LABELS ALLOWED_AUTHORS REQUIRED_CHECKS + REPOSITORY LIMIT LABELS ALLOWED_AUTHORS REQUIRED_CHECKS ALLOWED_BASE_BRANCHES - 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 "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 @@ -76,7 +76,8 @@ function main() { fi # Approve only PRs by allowed authors - if [[ "${AUTHOR}" =~ ^(${ALLOWED_AUTHORS})$ ]]; then + IFS=',' read -r -a ALLOWED_AUTHORS_ARRAY <<< "${ALLOWED_AUTHORS}" + if [[ " ${ALLOWED_AUTHORS_ARRAY[*]} " =~ " ${AUTHOR} " ]]; then if [[ "${DRY_RUN}" == "true" ]]; then echo "[DEBUG] ✓ PR #${PR_NUMBER} - would have approved [DRY RUN]" else @@ -90,6 +91,8 @@ function main() { 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="" @@ -115,6 +118,10 @@ function get_combined_success_status() { status conclusion } + ... on StatusContext { + context + state + } } } } @@ -155,14 +162,17 @@ function get_combined_success_status() { fi done - echo "$NODES_JSON" | jq ' - map(select( - .name != null - and (.name | test("'"${REQUIRED_CHECKS}"'")) - )) - | { - ALL_SUCCESS: (length > 0 and all(.conclusion == "SUCCESS" or .conclusion == "SKIPPED")) - } | .ALL_SUCCESS + 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") ' } From e320206f1ef1c05e235546ecaa7a9ebcb4d09dfa Mon Sep 17 00:00:00 2001 From: Tom Martensen Date: Mon, 13 Apr 2026 16:55:49 +0200 Subject: [PATCH 11/13] fix shellcheck --- automerge/automerge.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automerge/automerge.sh b/automerge/automerge.sh index 2a83acf..d15ca17 100755 --- a/automerge/automerge.sh +++ b/automerge/automerge.sh @@ -77,7 +77,7 @@ function main() { # Approve only PRs by allowed authors IFS=',' read -r -a ALLOWED_AUTHORS_ARRAY <<< "${ALLOWED_AUTHORS}" - if [[ " ${ALLOWED_AUTHORS_ARRAY[*]} " =~ " ${AUTHOR} " ]]; then + if [[ " ${ALLOWED_AUTHORS_ARRAY[*]} " == *"${AUTHOR}"* ]]; then if [[ "${DRY_RUN}" == "true" ]]; then echo "[DEBUG] ✓ PR #${PR_NUMBER} - would have approved [DRY RUN]" else From 430f1c26986f0de042a501e771efdef5047df700 Mon Sep 17 00:00:00 2001 From: Tom Martensen Date: Tue, 14 Apr 2026 13:33:02 +0200 Subject: [PATCH 12/13] simple code review suggestions --- automerge/action.yml | 16 ++++++++-------- automerge/automerge.sh | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/automerge/action.yml b/automerge/action.yml index 2aca43a..008e9c3 100644 --- a/automerge/action.yml +++ b/automerge/action.yml @@ -3,22 +3,22 @@ description: Enable auto-merge for eligible PRs with specified labels inputs: allowed-authors: - description: 'Authors to filter PRs for auto-merge (comma-separated)' + description: 'Authors (comma-separated) to filter PRs for auto-merge. Empty list will fail validation.' required: false default: 'app/dependabot' allowed-base-branches: - description: 'Allowed base branches for auto-merge (regex)' + description: 'Allowed base branches for auto-merge (regex).' required: false default: '.*' dry-run: - description: 'Whether to dry-run the auto-merge' + description: 'Whether to dry-run the auto-merge.' required: false - default: 'false' + default: 'true' github-token: - description: 'GitHub token with permissions to merge PRs and approve reviews' + description: 'GitHub token with permissions to merge PRs and approve reviews.' required: true labels: - description: 'Labels to filter PRs for auto-merge (comma-separated `and` logic)' + description: '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. Empty list will fail validation.' required: false default: 'auto-merge' limit: @@ -26,11 +26,11 @@ inputs: required: false default: '50' repository: - description: 'Repository in owner/repo format' + description: 'Repository in owner/repo format.' required: false default: ${{ github.repository }} required-checks: - description: 'Required checks to pass for auto-merge (regex)' + description: 'Required checks to pass for auto-merge (regex).' required: false default: '.*' diff --git a/automerge/automerge.sh b/automerge/automerge.sh index d15ca17..e3e8799 100755 --- a/automerge/automerge.sh +++ b/automerge/automerge.sh @@ -6,7 +6,7 @@ # # Local run: # -# test/local-env.sh automerge/automerge.sh +# test/local-env.sh automerge/automerge.sh # set -euo pipefail @@ -51,7 +51,7 @@ function main() { 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 + if [[ ! "${BASE_BRANCH}" =~ ${ALLOWED_BASE_BRANCHES} ]]; then echo "[DEBUG] PR #${PR_NUMBER} skipped - base branch '${BASE_BRANCH}' not allowed" continue fi @@ -71,7 +71,7 @@ function main() { echo "[DEBUG] ✓ PR #${PR_NUMBER} - would have enabled auto-merge [DRY RUN]" else gh pr merge --repo "${REPOSITORY}" \ - --auto --squash "${PR_NUMBER}" + --auto --squash "${PR_NUMBER}" echo "[DEBUG] ✓ PR #${PR_NUMBER} - auto-merge enabled" fi From 831e3d0b1dcf147b94f16a3a092eec3798b77feb Mon Sep 17 00:00:00 2001 From: Tom Martensen Date: Tue, 14 Apr 2026 13:41:30 +0200 Subject: [PATCH 13/13] use simplification for allowed_authors --- automerge/automerge.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/automerge/automerge.sh b/automerge/automerge.sh index e3e8799..57e0b1f 100755 --- a/automerge/automerge.sh +++ b/automerge/automerge.sh @@ -76,8 +76,7 @@ function main() { fi # Approve only PRs by allowed authors - IFS=',' read -r -a ALLOWED_AUTHORS_ARRAY <<< "${ALLOWED_AUTHORS}" - if [[ " ${ALLOWED_AUTHORS_ARRAY[*]} " == *"${AUTHOR}"* ]]; then + if [[ ",${ALLOWED_AUTHORS}," == *",${AUTHOR},"* ]]; then if [[ "${DRY_RUN}" == "true" ]]; then echo "[DEBUG] ✓ PR #${PR_NUMBER} - would have approved [DRY RUN]" else