From 1180b751d32d142dc1ea26cf64b0d7a4fdda9b47 Mon Sep 17 00:00:00 2001 From: Lucas Ramos <3140800+lucas-d-ramos@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:53:56 +0200 Subject: [PATCH] dependabot-auto-merge: retry arming, fall back to a direct merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wemove.eu, youmove, wemove-charity.eu and pubstatic never adopted the reusable. They each carry an inline copy that does something the reusable did not, and it is not incidental — it handles two real failure modes: 1. `gh pr merge --auto` intermittently returns a transient GraphQL error, stranding the PR until a human notices. 2. Auto-merge cannot be enabled on a PR whose required checks have ALREADY passed. With fast CI the checks often win the race, and the PR then sits green and unmerged indefinitely. Their comment records both. Porting the retry and the direct-merge fallback here means those four repos can drop ~20 lines each and converge on the shared workflow instead of diverging further. The fallback is not a bypass: branch protection still refuses a direct merge while required checks are pending or failing. On a repo with no required checks it merges immediately — which is why a ruleset is part of the entry bar for using this workflow at all. merge-method now reaches the script through env rather than `${{ }}` interpolation, so a caller cannot inject shell through it. Co-Authored-By: Claude Opus 5 --- .github/workflows/dependabot-auto-merge.yml | 28 ++++++++++++++++++++- README.md | 8 ++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 1a789af..3c7c9a5 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -121,8 +121,34 @@ jobs: steps.meta.outputs.update-type != '' && contains(format(',{0},', inputs.allowed-update-types), format(',{0},', steps.meta.outputs.update-type)) - run: gh pr merge --auto --${{ inputs.merge-method }} "$PR_URL" + # Two failure modes this handles, both seen in production on the + # wemove.eu / youmove / wemove-charity.eu inline copies this replaces: + # + # 1. `gh pr merge --auto` intermittently returns a transient GraphQL + # error, which strands the PR until a human notices. + # 2. Auto-merge cannot be enabled on a PR whose required checks have + # ALREADY passed. With fast CI the checks often win the race, and + # the PR then sits green and unmerged forever. + # + # So: retry, and fall back to a direct merge. The direct merge is not a + # bypass — branch protection still refuses it while required checks are + # pending or failing. On a repo with NO required checks it would merge + # immediately, which is exactly why a ruleset is part of the entry bar + # for using this workflow at all (see the note at the top). + # + # merge-method goes through env, not `${{ }}` interpolation into the + # script body, so a caller cannot inject shell through it. + run: | + for attempt in 1 2 3 4 5; do + if gh pr merge --auto --"$MERGE_METHOD" "$PR_URL"; then exit 0; fi + if gh pr merge --"$MERGE_METHOD" "$PR_URL"; then exit 0; fi + echo "attempt $attempt: could not enable auto-merge yet; retrying in 15s..." + sleep 15 + done + echo "::error::failed to enable auto-merge after 5 attempts" + exit 1 env: PR_URL: ${{ github.event.pull_request.html_url }} + MERGE_METHOD: ${{ inputs.merge-method }} # Falls back to GITHUB_TOKEN when the App token step was skipped. GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 5637d3d..d23a4bf 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,13 @@ depends on a branch ruleset (require a PR + required checks) on the consuming re Without that ruleset `gh pr merge --auto` has nothing to wait for and the PR merges immediately. +Arming is retried five times, and falls back to a direct merge. That covers two real +failure modes: `gh pr merge --auto` intermittently returns a transient GraphQL error, and +auto-merge cannot be enabled at all on a PR whose required checks have *already* gone +green — which fast CI makes common, and which otherwise leaves the PR sitting green and +unmerged. The direct merge is not a bypass: branch protection still refuses it while +required checks are pending or failing. + ### Inputs - **merge-method**: `merge` | `squash` | `rebase`. Default `squash`. @@ -261,6 +268,7 @@ jobs: Version tags `v1`–`v13` predate the current scheme and are frozen point releases. The floating-major convention (see [`RELEASING.md`](RELEASING.md)) starts at **`v14`**. +- **v14.2** — `dependabot-auto-merge.yml`: arming is now retried (5 attempts) with a direct-merge fallback, so a transient GraphQL error no longer strands a PR and an already-green PR still merges. Consolidates logic that existed only in the inline copies in wemove.eu, youmove, wemove-charity.eu and pubstatic, which this lets them drop. - **v14.1** — `dependabot-auto-merge.yml`: accepts optional `app-id` / `app-private-key` secrets so the merge is armed with a GitHub App token and fires a real push event (a `GITHUB_TOKEN`-armed merge does not, leaving push-triggered deploys silently unrun). Falls back to `GITHUB_TOKEN` when the secrets are omitted, and warns. The update-type gate is now a whole-token, fail-closed match — previously an empty `update-type` from `fetch-metadata` satisfied `contains()` and could auto-merge a major. - **v14** — First release under the semver + floating-major scheme. Ships the `docker-smoke` composite action (build + run + production-Host-header probe, with a `build-only` mode). - **v13** — Adds a reusable Dependabot auto-merge workflow and an actionlint CI gate.