Repository navigation
fix: unblock Dependabot and pre-commit automation #62
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
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
3427094
chore: migrate ship skill to global ship-it
gamaware dab7115
ci: replace approval-based auto-merge with scheduled admin merge
gamaware 974d145
fix: send both squash keys and surface PATCH failures in settings sync
gamaware 60105a1
fix: close superseded drift issues reliably
gamaware 1368a20
fix: skip PR on merge-state read failure and fail drift cleanup loudly
gamaware cb10c7c
ci: run bot merge job in a dedicated environment
gamaware c2b8fdb
fix: harden auto-merge check parsing and candidate filter
gamaware c84a94c
fix: surface branch protection PUT failures and skip Free-plan repos
gamaware 84811fd
ci: align auto-merge workflow with shared template
gamaware 3bb9581
fix: harden auto-merge head SHA guard and error reporting
gamaware b2843c6
fix: allowlist merge states and merge one PR per run
gamaware 990b820
fix: fail closed on branch-protection read errors and drift-issue cle…
gamaware b8badd3
ci: scope automation job secrets to an environment
gamaware 994e97d
fix: keep sync log lines out of the drift report
gamaware c5b7300
fix: require a passing check and honour change requests
gamaware be46d1a
fix: match HTTP status codes and log security PATCH errors in setting…
gamaware File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| # ----------------------------------------------------------------- | ||
| # Auto-merge bot PRs | ||
| # | ||
| # Squash-merges Dependabot PRs and the weekly pre-commit hook update | ||
| # PRs once every check on the PR is green and none is pending. | ||
| # | ||
| # Why admin bypass: the pre-commit PRs are authored with the repo | ||
| # owner's PAT, and GitHub refuses self-approval, so a review-based | ||
| # auto-merge can never satisfy the CODEOWNERS rule. Branch protection | ||
| # is not enforced on admins, so the owner's PAT merges directly. | ||
| # | ||
| # Safety: never touches drafts, fork PRs, PRs from other authors, | ||
| # PRs with conflicts, PRs with no registered checks, or PRs with any | ||
| # failing or pending check. A PR that is behind main is updated and | ||
| # picked up on the next run. | ||
| # | ||
| # Requires: PRE_COMMIT_PAT secret (fine-grained PAT of the repo owner | ||
| # with contents, pull requests and workflows write access). | ||
| # ----------------------------------------------------------------- | ||
| name: Auto-merge bot PRs | ||
| run-name: "Auto-merge bot PRs (${{ github.event_name }})" | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: "17 * * * *" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: {} | ||
|
|
||
| concurrency: | ||
| group: auto-merge-bot-prs | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| merge: | ||
| name: Merge green bot PRs | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 15 | ||
| # zizmor's secrets-outside-env audit requires secrets to be read inside a | ||
| # job environment. "automation" carries no protection rules and GitHub | ||
| # creates it on first use. | ||
| environment: automation | ||
| steps: | ||
| - name: Merge eligible PRs | ||
| env: | ||
| GH_TOKEN: ${{ secrets.PRE_COMMIT_PAT }} | ||
| REPO: ${{ github.repository }} | ||
| OWNER: ${{ github.repository_owner }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| if [ -z "$GH_TOKEN" ]; then | ||
| echo "::error::PRE_COMMIT_PAT secret is not set on this repository" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Candidate PRs: Dependabot, or the weekly pre-commit update branch | ||
| # authored by the repo owner (the PAT owner). Fork PRs are excluded | ||
| # because anyone can name a fork branch to match the prefix. | ||
| # Capture the list first so a gh failure aborts the run instead of | ||
| # being mistaken for "no open PRs". | ||
| pr_list=$( | ||
| gh pr list --repo "$REPO" --state open --limit 100 \ | ||
| --json number,author,headRefName,baseRefName,headRefOid,isDraft,isCrossRepository,headRepositoryOwner \ | ||
| --jq '.[] | ||
| | select(.isDraft | not) | ||
| | select(.isCrossRepository | not) | ||
| | select(.headRepositoryOwner.login == env.OWNER) | ||
| | select(.baseRefName == "main") | ||
| | select( | ||
| ((.author.login | sub("^app/"; "")) == "dependabot") | ||
|
gamaware marked this conversation as resolved.
Resolved
|
||
| or ( | ||
| (.headRefName | startswith("chore/update-pre-commit-hooks")) | ||
| and (.author.login == env.OWNER) | ||
| ) | ||
| ) | ||
| | "\(.number) \(.headRefOid)"' | ||
| ) | ||
| mapfile -t candidates <<< "$pr_list" | ||
| if [ "${#candidates[@]}" -eq 1 ] && [ -z "${candidates[0]}" ]; then | ||
| candidates=() | ||
| fi | ||
|
|
||
| if [ "${#candidates[@]}" -eq 0 ]; then | ||
| echo "No eligible bot PRs open." | ||
| exit 0 | ||
| fi | ||
|
|
||
| merged=0 | ||
| skipped=0 | ||
|
|
||
| for entry in "${candidates[@]}"; do | ||
| number="${entry%% *}" | ||
| head_sha="${entry##* }" | ||
| if [[ ! "$head_sha" =~ ^[0-9a-f]{40}$ ]]; then | ||
| echo "::error::PR #$number: missing head SHA in '$entry', skipping" | ||
| skipped=$((skipped + 1)) | ||
| continue | ||
| fi | ||
|
|
||
| if ! state_line=$(gh pr view "$number" --repo "$REPO" \ | ||
| --json mergeStateStatus,reviewDecision \ | ||
| --jq '"\(.mergeStateStatus) \(.reviewDecision)"'); then | ||
| echo "PR #$number: could not read merge state, skipping" | ||
| skipped=$((skipped + 1)) | ||
| continue | ||
| fi | ||
| state="${state_line%% *}" | ||
| review="${state_line##* }" | ||
|
|
||
| # Admin bypass must never override a reviewer who asked for changes. | ||
| if [ "$review" = "CHANGES_REQUESTED" ]; then | ||
| echo "PR #$number: changes requested by a reviewer, skipping" | ||
| skipped=$((skipped + 1)) | ||
| continue | ||
| fi | ||
|
|
||
| case "$state" in | ||
| DIRTY) | ||
| echo "PR #$number: merge conflicts, skipping" | ||
| skipped=$((skipped + 1)) | ||
| continue | ||
| ;; | ||
| BEHIND) | ||
| echo "PR #$number: behind base, requesting branch update" | ||
| gh pr update-branch "$number" --repo "$REPO" \ | ||
| || echo "::warning::PR #$number: branch update failed, will retry next run" | ||
| skipped=$((skipped + 1)) | ||
| continue | ||
| ;; | ||
| CLEAN|BLOCKED|UNSTABLE|HAS_HOOKS) | ||
| ;; | ||
| *) | ||
| # UNKNOWN (mergeability still being computed) or anything new. | ||
| echo "PR #$number: merge state $state, skipping" | ||
| skipped=$((skipped + 1)) | ||
| continue | ||
| ;; | ||
| esac | ||
|
|
||
| # gh pr checks exits non-zero on API errors or when no checks exist | ||
| # (with --json it exits 0 for failing or pending checks). Keep the | ||
| # output, drop the exit code, and treat empty output as "no checks" | ||
| # so the guard below skips. | ||
| checks_err="${RUNNER_TEMP:-/tmp}/pr-checks-$number.err" | ||
| summary=$(gh pr checks "$number" --repo "$REPO" --json bucket \ | ||
| --jq '[ | ||
| length, | ||
| ([.[] | select(.bucket == "fail" or .bucket == "cancel")] | length), | ||
| ([.[] | select(.bucket == "pending")] | length), | ||
| ([.[] | select(.bucket == "pass")] | length) | ||
| ] | @tsv' 2>"$checks_err") || true | ||
| if [ -z "$summary" ]; then | ||
| # Distinguish a real API/auth failure from "no checks reported". | ||
| if [ -s "$checks_err" ] && ! grep -qi "no checks" "$checks_err"; then | ||
| echo "::warning::PR #$number: gh pr checks failed: $(head -n 1 "$checks_err")" | ||
| fi | ||
| summary=$'0\t0\t0\t0' | ||
| fi | ||
| IFS=$'\t' read -r total fail pending pass <<< "$summary" | ||
|
|
||
| if [ "$fail" -gt 0 ]; then | ||
| echo "PR #$number: $fail failing check(s), skipping" | ||
| skipped=$((skipped + 1)) | ||
| continue | ||
| fi | ||
| if [ "$pending" -gt 0 ]; then | ||
| echo "PR #$number: $pending pending check(s), skipping" | ||
| skipped=$((skipped + 1)) | ||
| continue | ||
| fi | ||
| # Never merge on an empty check list: a brand-new PR may not have | ||
| # registered its checks yet, and a held fork run has none at all. | ||
| if [ "$total" -eq 0 ]; then | ||
| echo "PR #$number: no checks registered, skipping" | ||
| skipped=$((skipped + 1)) | ||
| continue | ||
| fi | ||
| # Skipped checks are not failures, but they are not evidence either: | ||
| # require at least one check that actually ran and passed. | ||
| if [ "$pass" -eq 0 ]; then | ||
| echo "PR #$number: $total check(s) registered but none passed, skipping" | ||
| skipped=$((skipped + 1)) | ||
| continue | ||
| fi | ||
|
|
||
| echo "PR #$number: $pass of $total check(s) passed, none failing or pending, merging" | ||
| # Bind the merge to the head captured before the checks were | ||
| # inspected; if the head moved since then, the merge is refused. | ||
| if gh pr merge "$number" --repo "$REPO" --squash --admin --delete-branch \ | ||
| --match-head-commit "$head_sha"; then | ||
|
coderabbitai[bot] marked this conversation as resolved.
Resolved
coderabbitai[bot] marked this conversation as resolved.
Resolved
|
||
| merged=$((merged + 1)) | ||
| # main has moved: stop here so every remaining candidate is | ||
| # re-evaluated against the new base on the next run. | ||
| echo "Merged one PR; remaining candidates are re-evaluated next run." | ||
| break | ||
| else | ||
| echo "::warning::PR #$number: merge failed" | ||
| skipped=$((skipped + 1)) | ||
| fi | ||
| done | ||
|
|
||
| echo "Merged: $merged | Skipped: $skipped" | ||
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.