Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: Dependabot auto-merge

# Merges a dependabot PR once CI has actually passed on it.
#
# NOT GitHub's own auto-merge: that needs `allow_auto_merge` on the repo and,
# to be worth anything, required status checks on `main`. This repo has neither
# (`allow_auto_merge: false`, `main` unprotected), and with no required checks
# GitHub's auto-merge merges *immediately* rather than waiting for CI — worse
# than merging by hand. Triggering on `workflow_run` instead means "CI finished
# and it was green" is the entry condition, by construction.
#
# Two updates are deliberately left for a person:
#
# escapepod-signal its PyPI twin `escapepod` must move in the same commit
# (see the escapepod section of CLAUDE.md, and #193). Merging
# the crate alone puts main in exactly the skew that
# escapepod-sync.yml then has to repair.
# major bumps a major is an API break by declaration; CI passing means
# the suite still runs, not that the semantics held.

on:
workflow_run:
workflows: ["CI"]
types: [completed]

permissions:
contents: write
pull-requests: write

concurrency:
group: dependabot-auto-merge-${{ github.event.workflow_run.head_sha }}
cancel-in-progress: false

jobs:
merge:
name: Merge if green and in policy
# `conclusion` gates on CI having passed; `actor` is a cheap pre-filter, and
# the authoritative author check is against the PR itself below.
if: >
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.actor.login == 'dependabot[bot]'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Resolve the pull request for this run
id: pr
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail
# workflow_run.pull_requests is empty for forks, so resolve by SHA.
number=$(gh api "repos/${REPO}/commits/${HEAD_SHA}/pulls" \
--jq '[.[] | select(.state == "open")] | first | .number // empty')
if [ -z "$number" ]; then
echo "No open PR for ${HEAD_SHA}; nothing to merge."
echo "found=false" >> "$GITHUB_OUTPUT"
exit 0
fi
author=$(gh pr view "$number" --repo "$REPO" --json author -q .author.login)
if [ "$author" != "app/dependabot" ]; then
echo "PR #${number} is authored by ${author}, not dependabot."
echo "found=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "number=${number}" >> "$GITHUB_OUTPUT"
echo "found=true" >> "$GITHUB_OUTPUT"

- name: Check the update against policy
id: policy
if: steps.pr.outputs.found == 'true'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
NUMBER: ${{ steps.pr.outputs.number }}
run: |
set -euo pipefail
title=$(gh pr view "$NUMBER" --repo "$REPO" --json title -q .title)
echo "title: ${title}"

# "bump <name> from <a> to <b>" — the shape dependabot always uses.
name=$(sed -n 's/.*bump \([^ ]*\) from .* to .*/\1/p' <<<"$title")
from=$(sed -n 's/.*bump [^ ]* from \([^ ]*\) to .*/\1/p' <<<"$title")
to=$(sed -n 's/.*bump [^ ]* from [^ ]* to \([^ ]*\).*/\1/p' <<<"$title")
echo "name=${name} from=${from} to=${to}"

if [ -z "$name" ] || [ -z "$from" ] || [ -z "$to" ]; then
echo "::notice::Could not parse the update from the title; leaving it for a person."
echo "merge=false" >> "$GITHUB_OUTPUT"; exit 0
fi

if [ "$name" = "escapepod-signal" ] || [ "$name" = "escapepod" ]; then
echo "::notice::${name} moves together with its twin; leaving it for a person."
echo "merge=false" >> "$GITHUB_OUTPUT"; exit 0
fi

# Compare leading numeric components; tags carry a leading "v".
major_from=$(sed 's/^v//' <<<"$from" | cut -d. -f1)
major_to=$(sed 's/^v//' <<<"$to" | cut -d. -f1)
if [ "$major_from" != "$major_to" ]; then
echo "::notice::major bump ${from} -> ${to}; leaving it for a person."
echo "merge=false" >> "$GITHUB_OUTPUT"; exit 0
fi

echo "merge=true" >> "$GITHUB_OUTPUT"

- name: Squash merge
if: steps.pr.outputs.found == 'true' && steps.policy.outputs.merge == 'true'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
NUMBER: ${{ steps.pr.outputs.number }}
run: gh pr merge "$NUMBER" --repo "$REPO" --squash --delete-branch
13 changes: 7 additions & 6 deletions .github/workflows/escapepod-sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,13 @@ jobs:
if: steps.check.outputs.skewed == 'true' && steps.check.outputs.actionable == 'true'
env:
TARGET: ${{ steps.check.outputs.target }}
# The guard on the hand-written entry. NOT `uv lock --check`, which also
# demands the lock match the running uv's serialization conventions --
# this lock does not (it predates the emscripten markers), so that gate
# fails on an untouched checkout and would block every proposal.
# `--frozen` resolves the lock as written and fails if the entry is
# incoherent, which is the risk of editing it by hand.
# The guard on the hand-written entry. Deliberately not `uv lock
# --check`: that also demands the lock match the *running* uv's
# serialization conventions, so it fails on an untouched checkout
# whenever the runner's uv is newer than the one that wrote the lock
# (this lock has been in that state, and would have blocked every
# proposal). `--frozen` resolves the lock as written and fails only if
# the entry is incoherent, which is the actual risk of editing by hand.
run: |
set -euo pipefail
uv export --frozen --no-emit-project --extra rust > /tmp/requirements.txt
Expand Down