Retarget and Merge Dependabot PRs #91
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
| name: Retarget and Merge Dependabot PRs | |
| on: | |
| schedule: | |
| - cron: "0 6 * * *" | |
| workflow_dispatch: | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| retarget-and-merge: | |
| runs-on: blacksmith-2vcpu-ubuntu-2404 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 1 | |
| - name: Resolve newest dev branch | |
| id: dev | |
| env: | |
| GH_TOKEN: ${{ secrets.GHCR_TOKEN }} | |
| run: | | |
| REFS=$(gh api "repos/${{ github.repository }}/branches" --paginate -q '.[].name') | |
| # The helper exits non-zero when no dev-X.Y.Z branch exists; treat that | |
| # as "nothing to do" rather than a workflow failure. | |
| if DEV_BRANCH=$(printf '%s\n' "$REFS" | node scripts/latest-dev-branch.cjs 2>/dev/null); then | |
| echo "Newest dev branch: $DEV_BRANCH" | |
| echo "branch=$DEV_BRANCH" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "No dev-X.Y.Z branch open; nothing to retarget." | |
| echo "branch=" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Retarget and merge Dependabot PRs | |
| if: ${{ steps.dev.outputs.branch != '' }} | |
| env: | |
| GH_TOKEN: ${{ secrets.GHCR_TOKEN }} | |
| DEV_BRANCH: ${{ steps.dev.outputs.branch }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -uo pipefail | |
| CONFLICT_LABEL="dependabot-rebase-requested" | |
| # Ensure the bookkeeping label exists (no-op if it already does). | |
| gh label create "$CONFLICT_LABEL" --repo "$REPO" \ | |
| --color "D93F0B" --description "Retarget workflow asked Dependabot to rebase a conflicting PR" \ | |
| 2>/dev/null || true | |
| # True if the PR already carries the conflict label. | |
| has_conflict_label() { | |
| gh pr view "$1" --repo "$REPO" --json labels \ | |
| -q '.labels[].name' | grep -qx "$CONFLICT_LABEL" | |
| } | |
| # Wait until GitHub has a definite mergeable verdict for a PR (it | |
| # returns UNKNOWN while recomputing after a base change or a push). | |
| # Echoes "<mergeable> <mergeStateStatus>". | |
| wait_for_verdict() { | |
| local pr="$1" mergeable state | |
| for _ in $(seq 1 30); do | |
| read -r mergeable state < <(gh pr view "$pr" --repo "$REPO" \ | |
| --json mergeable,mergeStateStatus \ | |
| -q '.mergeable + " " + .mergeStateStatus') | |
| if [ "$mergeable" != "UNKNOWN" ] && [ "$state" != "UNKNOWN" ]; then | |
| echo "$mergeable $state" | |
| return 0 | |
| fi | |
| sleep 20 | |
| done | |
| echo "$mergeable $state" | |
| } | |
| # Phase 1: retarget every open Dependabot PR from main onto the dev | |
| # branch. This kicks off a Dependabot rebase for each. | |
| PR_NUMBERS=$(gh pr list --repo "$REPO" \ | |
| --author "app/dependabot" \ | |
| --base main \ | |
| --state open \ | |
| --json number -q '.[].number') | |
| # Pick up PRs already sitting on the dev branch from a previous run too. | |
| PR_NUMBERS="$PR_NUMBERS $(gh pr list --repo "$REPO" \ | |
| --author "app/dependabot" \ | |
| --base "$DEV_BRANCH" \ | |
| --state open \ | |
| --json number -q '.[].number')" | |
| PR_NUMBERS=$(printf '%s\n' $PR_NUMBERS | sort -un) | |
| if [ -z "$PR_NUMBERS" ]; then | |
| echo "No open Dependabot PRs to process." | |
| exit 0 | |
| fi | |
| for PR in $PR_NUMBERS; do | |
| BASE=$(gh pr view "$PR" --repo "$REPO" --json baseRefName -q .baseRefName) | |
| if [ "$BASE" != "$DEV_BRANCH" ]; then | |
| echo "Retargeting PR #$PR ($BASE -> $DEV_BRANCH)" | |
| gh pr edit "$PR" --repo "$REPO" --base "$DEV_BRANCH" | |
| fi | |
| done | |
| # Phase 2: merge one at a time. Each merge can make the remaining npm | |
| # PRs stale, so re-check immediately before merging and rebase stragglers. | |
| for PR in $PR_NUMBERS; do | |
| echo "::group::PR #$PR" | |
| read -r MERGEABLE STATE < <(wait_for_verdict "$PR") | |
| echo " mergeable=$MERGEABLE mergeStateStatus=$STATE" | |
| # BEHIND = clean but needs the latest base; ask Dependabot to rebase | |
| # and skip for now (next run merges it once it is up to date). | |
| if [ "$STATE" = "BEHIND" ]; then | |
| echo " PR #$PR is behind $DEV_BRANCH; asking Dependabot to rebase." | |
| gh pr comment "$PR" --repo "$REPO" --body "@dependabot rebase" | |
| echo "::endgroup::" | |
| continue | |
| fi | |
| # DIRTY / CONFLICTING = a real conflict. Try a rebase once (label it so | |
| # we can tell next time); if it is STILL conflicting on a later run | |
| # despite already being labelled, the rebase failed for good - close it | |
| # so Dependabot reopens a fresh PR against the current dev branch. | |
| if [ "$MERGEABLE" = "CONFLICTING" ] || [ "$STATE" = "DIRTY" ]; then | |
| if has_conflict_label "$PR"; then | |
| echo " PR #$PR still conflicts after a prior rebase request; closing so Dependabot reopens it fresh." | |
| gh pr close "$PR" --repo "$REPO" --delete-branch \ | |
| --comment "Closing: this PR still conflicts with $DEV_BRANCH after a rebase attempt (its changes are likely already merged). Dependabot will reopen a fresh PR computed against the current $DEV_BRANCH." | |
| else | |
| echo " PR #$PR conflicts with $DEV_BRANCH; requesting a rebase and labelling it." | |
| gh pr edit "$PR" --repo "$REPO" --add-label "$CONFLICT_LABEL" | |
| gh pr comment "$PR" --repo "$REPO" --body "@dependabot rebase" | |
| fi | |
| echo "::endgroup::" | |
| continue | |
| fi | |
| # A clean PR that was previously flagged has recovered - drop the label. | |
| if has_conflict_label "$PR"; then | |
| gh pr edit "$PR" --repo "$REPO" --remove-label "$CONFLICT_LABEL" || true | |
| fi | |
| echo " Squash-merging PR #$PR" | |
| if gh pr merge "$PR" --repo "$REPO" --squash --admin; then | |
| echo " Merged PR #$PR" | |
| # Give GitHub a moment to mark the now-stale siblings BEHIND. | |
| sleep 15 | |
| else | |
| echo " Could not merge PR #$PR now; it will be retried next run." | |
| fi | |
| echo "::endgroup::" | |
| done |