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
20 changes: 20 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
ignore:
- dependency-name: "*"
update-types:
- "version-update:semver-major"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
ignore:
- dependency-name: "*"
update-types:
- "version-update:semver-major"
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: CI

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install package
run: |
python -m pip install --upgrade pip
python -m pip install -e '.[test]'
- name: Run tests
run: python -m pytest -q
84 changes: 84 additions & 0 deletions .github/workflows/dependabot_auto_merge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Auto Merge Dependabot PR

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

jobs:
auto-merge:
if: github.event.workflow_run.conclusion == 'success' && startsWith(github.event.workflow_run.head_branch, 'dependabot/')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write

steps:
- name: Resolve Dependabot PR
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
BRANCH_NAME="${{ github.event.workflow_run.head_branch }}"
PR_NUMBER=$(gh pr list --repo "${GITHUB_REPOSITORY}" --state open --head "${BRANCH_NAME}" --json number --jq '.[0].number // empty')
if [ -z "${PR_NUMBER}" ]; then
echo "No open Dependabot PR found for ${BRANCH_NAME}." >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "pr_number=${PR_NUMBER}" >> "$GITHUB_OUTPUT"

- name: Evaluate merge eligibility
id: merge_guard
if: steps.pr.outputs.pr_number != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh pr view "${{ steps.pr.outputs.pr_number }}" --repo "${GITHUB_REPOSITORY}" --json number,isDraft,author,url,body,labels > pr.json
python3 - <<'PY'
import json
import os
from pathlib import Path

pr = json.loads(Path("pr.json").read_text(encoding="utf-8"))
author = (pr.get("author") or {}).get("login")
labels = {item.get("name", "") for item in pr.get("labels", [])}
body = pr.get("body") or ""
is_major = "update-type: version-update:semver-major" in body
dependabot_authors = {"dependabot[bot]", "app/dependabot"}
is_dependabot = author in dependabot_authors and "dependencies" in labels
should_merge = is_dependabot and not pr.get("isDraft") and not is_major

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Require CI success for the PR head being merged

When Dependabot pushes or rebases the same branch while an older CI run is still finishing, this workflow_run can be successful for the old commit, but the later merge step operates on the PR's current head. Since should_merge does not compare the successful run's SHA/check suite with the current PR head, a newer untested or failing commit can be merged unless branch protection independently blocks it. Gate this on the current PR head having the successful CI result before returning true.

Useful? React with 👍 / 👎.

if should_merge:
reason = "ready"
elif is_major:
reason = "major_update"
else:
reason = "not_eligible_dependabot_pr"

summary_lines = [
"## Auto-Merge Gate",
f"- PR: {pr['url']}",
f"- Author: `{author or '<unknown>'}`",
f"- Draft: `{'yes' if pr.get('isDraft') else 'no'}`",
f"- Dependabot label: `{'yes' if 'dependencies' in labels else 'no'}`",
f"- Major update: `{'yes' if is_major else 'no'}`",
f"- Final merge decision: `{'merge' if should_merge else 'skip'}`",
f"- Reason: `{reason}`",
]
Path("pr-summary.md").write_text("\n".join(summary_lines).strip() + "\n", encoding="utf-8")
with open(os.environ["GITHUB_OUTPUT"], "a", encoding="utf-8") as output:
print(f"should_merge={'true' if should_merge else 'false'}", file=output)
print(f"reason={reason}", file=output)
PY

- name: Append merge summary
if: steps.pr.outputs.pr_number != ''
run: cat pr-summary.md >> "$GITHUB_STEP_SUMMARY"

- name: Merge Dependabot PR
if: steps.merge_guard.outputs.should_merge == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr merge "${{ steps.pr.outputs.pr_number }}" --repo "${GITHUB_REPOSITORY}" --rebase --delete-branch