Skip to content

Update reviewdog/action-actionlint action to v1.73.2 #429

Update reviewdog/action-actionlint action to v1.73.2

Update reviewdog/action-actionlint action to v1.73.2 #429

Workflow file for this run

name: Auto-merge dependency PRs
on:
pull_request:
workflow_call:
inputs:
safe-updates-only:
description: >-
When true, only auto-merge safe updates (semver minor/patch for
Dependabot, or Docker non-major bumps). Majors are approved but
left for a human to merge. Renovate is always trusted — its own
config (renovate.json) decides what it opens.
type: boolean
default: false
merge-strategy:
description: >-
Override the merge strategy. One of "squash", "merge", "rebase",
or "" (default) to auto-detect from repo settings. Use this when
the repo has `required_linear_history` or other constraints the
auto-detector can't see.
type: string
default: ""
checks-timeout-minutes:
description: >-
How long to wait for the other checks when the repository has
auto-merge DISABLED and this workflow therefore has to gate the
merge itself (see the note on the merge step). Ignored on repos
with auto-merge enabled, where GitHub does the waiting. Capped by
the job's own 30-minute timeout, so values above 25 have no
effect. Timing out is not a failure: the PR stays approved and
open for a human.
type: number
default: 15
# CALLER REQUIREMENTS
# ===================
# When calling this reusable workflow, the caller's job-level
# `permissions:` block MUST grant at least this full set, otherwise
# the run fails with `startup_failure` before any job executes
# (GitHub rejects the workflow at startup when the reusable declares
# a permission the caller did not grant). NOTE: setting ANY scope in a
# `permissions:` block forces every UNLISTED scope to `none`, and a job
# that omits `permissions:` inherits the repository default
# (default_workflow_permissions) — so a repo hardened to `read` will
# startup-fail here unless these scopes are granted explicitly.
#
# jobs:
# auto-merge:
# uses: netresearch/.github/.github/workflows/auto-merge-deps.yml@main
# permissions:
# contents: write
# pull-requests: write
permissions: {}
jobs:
auto-merge:
name: Auto-merge dependency PRs
runs-on: ubuntu-latest
# Hard ceiling for the check-gating path below. GitHub expressions
# have no arithmetic, so this cannot be derived from the input —
# it is a fixed bound and `checks-timeout-minutes` above 25 has no
# effect. The auto-merge path finishes in seconds either way.
timeout-minutes: 30
# The label check sits here as well as on the merge step, so a labelled
# update is not approved either — an approved-but-unmerged pull request
# looks cleared when nobody has looked at it. See the note on that step.
#
# Both label names are honoured: the preset renames deps-major to
# deps-no-automerge, and whichever of the two changes lands first, the
# other name is still in circulation on pull requests Renovate has not
# rewritten yet. Accepting both means the order of the two merges cannot
# open a window in which labelled updates merge unseen. The old name can
# be dropped once no open pull request carries it.
if: >-
(github.event.pull_request.user.login == 'dependabot[bot]' ||
github.event.pull_request.user.login == 'renovate[bot]') &&
!contains(github.event.pull_request.labels.*.name, 'deps-no-automerge') &&
!contains(github.event.pull_request.labels.*.name, 'deps-major')
permissions:
contents: write
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1
with:
egress-policy: audit
- name: Approve PR
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr review --approve "$PR_URL"
- name: Dependabot metadata
id: metadata
if: inputs.safe-updates-only && github.event.pull_request.user.login == 'dependabot[bot]'
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
# The `deps-no-automerge` label comes from netresearch/renovate-config
# and marks an update Renovate deliberately did NOT mark for automerge:
# a major, or a replacement that swaps the dependency for a differently
# named successor. Without it this workflow merged those too - the
# Dependabot metadata action produces no update-type for Renovate, so
# the condition below waved every Renovate pull request through on the
# reasoning that "Renovate's own config decides what it opens". Opening
# is not the same as wanting it merged, and the result was that this
# workflow overrode the org policy it was supposed to follow.
#
# Checked on the job as well as the step, so the label also stops the
# approval - approving one and then not merging it would leave a pull
# request that looks cleared when nobody has looked at it.
- name: Enable auto-merge
if: >-
!contains(github.event.pull_request.labels.*.name, 'deps-no-automerge') &&
!contains(github.event.pull_request.labels.*.name, 'deps-major') &&
(!inputs.safe-updates-only ||
github.event.pull_request.user.login == 'renovate[bot]' ||
steps.metadata.outputs.update-type == 'version-update:semver-minor' ||
steps.metadata.outputs.update-type == 'version-update:semver-patch' ||
(steps.metadata.outputs.package-ecosystem == 'docker' &&
steps.metadata.outputs.update-type != 'version-update:semver-major'))
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
BASE_REF: ${{ github.event.pull_request.base.ref }}
STRATEGY_OVERRIDE: ${{ inputs.merge-strategy }}
CHECKS_TIMEOUT_MINUTES: ${{ inputs.checks-timeout-minutes || 15 }}
run: |
set -euo pipefail
REPO_JSON=$(gh api "repos/$REPO")
if [[ -n "$STRATEGY_OVERRIDE" ]]; then
STRATEGY="--$STRATEGY_OVERRIDE"
else
STRATEGY=$(printf '%s' "$REPO_JSON" | jq -r '
if .allow_squash_merge then "--squash"
elif .allow_merge_commit then "--merge"
elif .allow_rebase_merge then "--rebase"
else "--squash" end')
fi
echo "Using merge strategy: $STRATEGY"
ALLOW_AUTO_MERGE=$(printf '%s' "$REPO_JSON" | jq -r '.allow_auto_merge // false')
# `--auto` is only a GATE where GitHub actually has something to hold
# the merge on. Native auto-merge waits for REQUIRED status checks and
# required reviews - not for "whatever is currently running". On a
# branch with none required it does not wait at all: the PR either
# merges the moment it is mergeable, before CI reports, or the command
# errors out because the PR is already in a clean state.
#
# Branching on `allow_auto_merge` alone therefore silently drops the
# CI gate on every repo that permits auto-merge without requiring any
# check - which, measured across this org on 2026-07-23, is nearly all
# of them (netresearch/.github included; only timetracker carried a
# required-status-checks rule). So establish that a real gate exists
# before trusting GitHub to enforce one.
NATIVE_GATE=false
if [ "$ALLOW_AUTO_MERGE" = "true" ]; then
# Rulesets are readable with ordinary read access.
if RULES=$(gh api "repos/$REPO/rules/branches/$BASE_REF" 2>/dev/null); then
if printf '%s' "$RULES" | jq -e 'any(.[]?; .type == "required_status_checks")' >/dev/null 2>&1; then
NATIVE_GATE=true
fi
fi
# Classic branch protection needs admin rights, so this call may
# 403. That is fine: an unreadable protection config counts as
# "cannot prove a gate exists" and falls through to gating here -
# slower, never unsafe.
if [ "$NATIVE_GATE" != "true" ] && PROT=$(gh api "repos/$REPO/branches/$BASE_REF/protection" 2>/dev/null); then
REQ_COUNT=$(printf '%s' "$PROT" | jq '((.required_status_checks.contexts // []) + (.required_status_checks.checks // [])) | length' 2>/dev/null || echo 0)
if [ "${REQ_COUNT:-0}" -gt 0 ]; then
NATIVE_GATE=true
fi
fi
fi
if [ "$NATIVE_GATE" = "true" ]; then
echo "Base branch '$BASE_REF' has required status checks - letting GitHub hold the merge."
# Re-arm rather than re-request. An auto-merge request already
# attached to this PR is NOT refreshed by asking again: `gh pr
# merge --auto` succeeds, prints nothing unusual, and leaves the
# OLD request in place. That matters because the old request was
# evaluated against the branch rules as they were when it was
# created — GitHub does not re-evaluate it when those rules change.
# A request created before a rule change can therefore sit on the
# PR forever, never completing.
#
# Worse, while it sits there it silently swallows every later
# attempt to merge or enqueue the PR by hand: the command reports
# success and nothing happens. Four dependency PRs across this org
# were stuck that way on 2026-08-08, the oldest since 2026-07-30,
# and the state is invisible unless you query autoMergeRequest
# directly.
#
# Dropping the request first makes each run of this workflow
# create a fresh one under the rules in force right now.
if gh pr view "$PR_URL" --json autoMergeRequest \
--jq '.autoMergeRequest != null' 2>/dev/null | grep -q true; then
echo "::notice::Clearing a pre-existing auto-merge request before re-arming."
gh pr merge --disable-auto "$PR_URL" || true
fi
# `enablePullRequestAutoMerge` is refused outright when the PR
# touches .github/workflows and the token has no `workflows`
# scope: "refusing to allow a GitHub App to create or update
# workflow ... without `workflows` permission". Renovate bumping
# a GitHub Action hits this every time, and the scope cannot
# simply be added here - the caller has to grant it too, so a
# new permission in this file startup-fails every caller that
# has not been updated yet (see CALLER REQUIREMENTS above).
#
# Falling through to the gating path below is not a workaround
# but the correct handling: it waits for the same checks itself
# and then merges over REST, which is not subject to that
# restriction. Measured 2026-08-09 in netresearch/git-workflow-
# skill, where PR #153 changed a workflow file and merged over
# exactly this path with these permissions.
if gh pr merge --auto "$STRATEGY" "$PR_URL" 2>"$RUNNER_TEMP/automerge.err"; then
exit 0
fi
sed 's/^/ /' "$RUNNER_TEMP/automerge.err" >&2 || true
echo "::notice::Native auto-merge was refused - gating on the checks here instead."
fi
# Reached three ways: auto-merge is disabled, it is enabled but
# nothing is required on the base branch, or GitHub refused the
# native request above. In all three nothing outside this job
# keeps an unverified dependency bump from landing, so gate here.
#
# This is why repos in that state used to keep a hand-rolled copy of
# this workflow. They no longer have to.
if [ "$NATIVE_GATE" != "true" ]; then
echo "No required status checks on '$BASE_REF' (allow_auto_merge=$ALLOW_AUTO_MERGE) - gating on the checks here."
fi
DEADLINE=$(( $(date +%s) + ${CHECKS_TIMEOUT_MINUTES:-15} * 60 ))
while :; do
if [ "$(date +%s)" -ge "$DEADLINE" ]; then
echo "::notice::Checks did not all pass within ${CHECKS_TIMEOUT_MINUTES} minutes — approved, not merged."
exit 0
fi
# This job's own check must be excluded, or it waits on itself
# forever. Match by SUFFIX, not equality: when this reusable is
# called as a job, the check surfaces as "<caller-job> / Auto-merge
# dependency PRs" (the caller's job id/name is prefixed), so an
# exact `!= "Auto-merge dependency PRs"` never matches and the loop
# counts its own IN_PROGRESS check as pending — waiting on itself
# until the timeout, and orphaning every bot PR in a repo without
# required checks. `endswith` matches whether the caller prefixes it
# or not (this reusable owns the job name that forms the suffix).
#
# A FAILED API call must never be read as "no checks": falling back
# to an empty array here would make the two counters below zero and
# the loop would conclude everything passed and merge an unverified
# PR on a transient error. Retry until the deadline instead.
if ! CHECKS=$(gh pr checks "$PR_URL" --json name,state \
--jq '[.[] | select((.name | endswith("Auto-merge dependency PRs")) | not)]' 2>/dev/null); then
echo "Could not read the check list — retrying."
sleep 15
continue
fi
FAILED=$(printf '%s' "$CHECKS" | jq '[.[] | select(.state == "FAILURE" or .state == "ERROR" or .state == "TIMED_OUT" or .state == "CANCELLED")] | length')
PENDING=$(printf '%s' "$CHECKS" | jq '[.[] | select(.state == "PENDING" or .state == "QUEUED" or .state == "IN_PROGRESS" or .state == "WAITING")] | length')
if [ "$FAILED" != "0" ]; then
echo "::notice::${FAILED} check(s) failed — approved, not merged."
exit 0
fi
if [ "$PENDING" = "0" ]; then
echo "All checks passed."
break
fi
echo "Waiting for ${PENDING} check(s)..."
sleep 15
done
# A refused merge leaves the PR approved and open, which is the
# same outcome as the two notices above - so report it the same
# way rather than failing the job. A failed job turns this into a
# red check on the PR, and a red check is exactly what holds the
# PR at UNSTABLE and keeps every other route to merging it shut.
# Failing here would therefore make the PR harder to land by hand
# than if this workflow had never run.
if ! gh pr merge "$STRATEGY" "$PR_URL" 2>"$RUNNER_TEMP/merge.err"; then
sed 's/^/ /' "$RUNNER_TEMP/merge.err" >&2 || true
echo "::notice::Merge was refused — approved, not merged."
fi