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
34 changes: 26 additions & 8 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# Claude Code — thin caller that delegates to the org-level reusable workflow.
# All logic and prompts are maintained centrally in claude-code-reusable.yml.
# Standard: https://github.com/petry-projects/.github/blob/main/standards/ci-standards.md#4-claude-code-claudeyml
name: Claude Code

on:
Expand All @@ -17,13 +14,34 @@ on:
permissions: {}

jobs:
claude-code:
uses: petry-projects/.github/.github/workflows/claude-code-reusable.yml@main
secrets: inherit
claude:
if: >-
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository) ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request &&
contains(github.event.comment.body, '@claude') &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) ||
(github.event_name == 'pull_request_review_comment' &&
contains(github.event.comment.body, '@claude') &&
contains(fromJson('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association)) ||
(github.event_name == 'issues' && github.event.action == 'labeled' &&
github.event.label.name == 'claude')
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
# write required for issue-triggered branch creation
contents: write
id-token: write
pull-requests: write
issues: write
actions: read
checks: read
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 1
- name: Run Claude Code
if: github.event_name != 'pull_request' || github.event.pull_request.user.login != 'dependabot[bot]'
uses: anthropics/claude-code-action@6e2bd52842c65e914eba5c8badd17560bd26b5de # v1.0.89
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
label_trigger: "claude"
85 changes: 23 additions & 62 deletions .github/workflows/dependabot-automerge.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
# Dependabot auto-merge workflow
# ─────────────────────────────────────────────────────────────────────────────
# SOURCE OF TRUTH: petry-projects/.github/standards/workflows/dependabot-automerge.yml
# Standard: petry-projects/.github/standards/dependabot-policy.md
# Reusable: petry-projects/.github/.github/workflows/dependabot-automerge-reusable.yml
#
# Requires repository secrets:
# AGENTS — READ BEFORE EDITING:
# • This file is a THIN CALLER STUB. All eligibility logic and the GitHub
# App token dance live in the reusable workflow above.
# • You MAY change: nothing in this file in normal use. Adopt verbatim.
# • You MUST NOT change: trigger event (must be `pull_request_target`),
# the `uses:` line, `secrets: inherit`, or the job-level `permissions:`
# block — reusable workflows can be granted no more permissions than the
# calling job has, so removing the stanza breaks the reusable's gh API
# calls.
# • If you need different behaviour, open a PR against the reusable in the
# central repo.
# ─────────────────────────────────────────────────────────────────────────────
#
# Dependabot auto-merge — thin caller for the org-level reusable.
# To adopt: copy this file to .github/workflows/dependabot-automerge.yml in your repo.
# Required org/repo secrets (inherited):
# APP_ID — GitHub App ID with contents:write and pull-requests:write
# APP_PRIVATE_KEY — GitHub App private key
#
# Auto-approves and enables auto-merge for Dependabot PRs that are:
# - GitHub Actions updates (patch or minor version bumps)
# - Security updates for any ecosystem (patch or minor)
# - Indirect (transitive) dependency updates
# Major version updates are always left for human review.
# Uses --auto so the merge waits for all required CI checks to pass.
#
# Safety model: application ecosystems use open-pull-requests-limit: 0 in
# dependabot.yml, so the only app-ecosystem PRs Dependabot can create are
# security updates. This workflow adds defense-in-depth by also checking
# the package ecosystem.
name: Dependabot auto-merge

on:
Expand All @@ -25,54 +31,9 @@ on:
permissions: {}

jobs:
dependabot:
runs-on: ubuntu-latest
dependabot-automerge:
permissions:
contents: read
pull-requests: read
if: github.event.pull_request.user.login == 'dependabot[bot]'
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@ffa630c65fa7e0ecfa0625b5ceda64399aea1b36 # v2
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
skip-commit-verification: true

- name: Determine if auto-merge eligible
id: eligible
run: |
UPDATE_TYPE="${{ steps.metadata.outputs.update-type }}"
DEP_TYPE="${{ steps.metadata.outputs.dependency-type }}"
ECOSYSTEM="${{ steps.metadata.outputs.package-ecosystem }}"

# Must be patch, minor, or indirect
if [[ "$UPDATE_TYPE" != "version-update:semver-patch" && \
"$UPDATE_TYPE" != "version-update:semver-minor" && \
"$DEP_TYPE" != "indirect" ]]; then
echo "eligible=false" >> "$GITHUB_OUTPUT"
echo "Skipping: major update requires human review"
exit 0
fi

# GitHub Actions version updates are always eligible
# App ecosystem PRs can only exist as security updates (limit: 0)
echo "eligible=true" >> "$GITHUB_OUTPUT"
echo "Auto-merge eligible: ecosystem=$ECOSYSTEM update=$UPDATE_TYPE"

- name: Generate app token
if: steps.eligible.outputs.eligible == 'true'
id: app-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- name: Approve and enable auto-merge
if: steps.eligible.outputs.eligible == 'true'
run: |
gh pr review --approve "$PR_URL"
gh pr merge --auto --squash "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ steps.app-token.outputs.token }}
uses: petry-projects/.github/.github/workflows/dependabot-automerge-reusable.yml@v1
secrets: inherit
Comment thread
don-petry marked this conversation as resolved.
137 changes: 23 additions & 114 deletions .github/workflows/dependabot-rebase.yml
Original file line number Diff line number Diff line change
@@ -1,35 +1,26 @@
# Dependabot update and merge workflow
# Copy to .github/workflows/dependabot-rebase.yml
# ─────────────────────────────────────────────────────────────────────────────
# SOURCE OF TRUTH: petry-projects/.github/standards/workflows/dependabot-rebase.yml
# Standard: petry-projects/.github/standards/dependabot-policy.md
# Reusable: petry-projects/.github/.github/workflows/dependabot-rebase-reusable.yml
#
# Requires repository secrets:
# AGENTS — READ BEFORE EDITING:
# • This file is a THIN CALLER STUB. All rebase/merge serialization logic
# lives in the reusable workflow above.
# • You MAY change: nothing in this file in normal use. Adopt verbatim.
# • You MUST NOT change: trigger event, the concurrency group name,
# the `uses:` line, `secrets: inherit`, or the job-level `permissions:`
# block — reusable workflows can be granted no more permissions than the
# calling job has, so removing the stanza breaks the reusable's gh API
# calls.
# • If you need different behaviour, open a PR against the reusable in the
# central repo.
# ─────────────────────────────────────────────────────────────────────────────
#
# Dependabot update-and-merge — thin caller for the org-level reusable.
# To adopt: copy this file to .github/workflows/dependabot-rebase.yml in your repo.
# Required org/repo secrets (inherited):
# APP_ID — GitHub App ID with contents:write and pull-requests:write
# APP_PRIVATE_KEY — GitHub App private key
#
# Problem: when branch protection requires branches to be up-to-date
# (strict status checks), merging one Dependabot PR makes the others fall
# behind. Dependabot does not auto-rebase PRs that are merely behind — it
# only rebases on its next scheduled run or when there are merge conflicts.
# Additionally, GitHub auto-merge (--auto) may not trigger when rulesets
# cause mergeable_state to report "blocked" even though all requirements
# are actually met.
#
# Solution: after every push to main (typically a merged PR), this workflow:
# 1. Updates behind Dependabot PRs using the merge method (not rebase)
# 2. Merges any Dependabot PR that is up-to-date, approved, and passing CI
#
# Using the app token for merges ensures the resulting push to main triggers
# this workflow again, creating a self-sustaining chain that serializes
# Dependabot PR merges one at a time.
#
# Important: never use the API update-branch endpoint with rebase method on
# Dependabot PRs — it replaces Dependabot's commit signature with GitHub's,
# which breaks dependabot/fetch-metadata verification and causes Dependabot
# to refuse future rebases on that PR. The merge method preserves the
# original commits.
#
# Note: the merge commit is authored by GitHub, not Dependabot, so the
# dependabot-automerge workflow must use skip-commit-verification: true
# in the dependabot/fetch-metadata step.
name: Dependabot update and merge

on:
Expand All @@ -44,91 +35,9 @@ concurrency:
permissions: {}

jobs:
update-and-merge:
runs-on: ubuntu-latest
dependabot-rebase:
permissions:
contents: read
pull-requests: read
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}

- name: Update and merge Dependabot PRs
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
REPO: ${{ github.repository }}
run: |
# Find open Dependabot PRs
PRS=$(gh pr list --repo "$REPO" --author "app/dependabot" \
--json number,headRefName \
--jq '.[] | "\(.number) \(.headRefName)"')

if [[ -z "$PRS" ]]; then
echo "No open Dependabot PRs"
exit 0
fi

MERGED=false

while IFS=' ' read -r PR_NUMBER HEAD_REF; do
BEHIND=$(gh api "repos/$REPO/compare/main...$HEAD_REF" \
--jq '.behind_by')

if [[ "$BEHIND" -gt 0 ]]; then
echo "PR #$PR_NUMBER ($HEAD_REF) is $BEHIND commit(s) behind — merging main into branch"
gh api "repos/$REPO/pulls/$PR_NUMBER/update-branch" \
-X PUT -f update_method=merge \
--silent || echo "Warning: failed to update PR #$PR_NUMBER"
continue
fi

echo "PR #$PR_NUMBER ($HEAD_REF) is up to date — checking if merge-ready"

# Skip if we already merged one (strict mode means others are now behind)
if [[ "$MERGED" == "true" ]]; then
echo " Skipping — already merged a PR this run"
continue
fi

# Check if auto-merge is enabled (set by the automerge workflow)
AUTO_MERGE=$(gh pr view "$PR_NUMBER" --repo "$REPO" \
--json autoMergeRequest --jq '.autoMergeRequest != null')

if [[ "$AUTO_MERGE" != "true" ]]; then
echo " Skipping — auto-merge not enabled"
continue
fi

# Check if all required checks pass (look at overall rollup)
CHECKS_PASS=$(gh pr view "$PR_NUMBER" --repo "$REPO" \
--json statusCheckRollup \
--jq '[.statusCheckRollup[]? | select(.name != null and .status == "COMPLETED") | .conclusion] | all(. == "SUCCESS" or . == "NEUTRAL" or . == "SKIPPED")')

CHECKS_PENDING=$(gh pr view "$PR_NUMBER" --repo "$REPO" \
--json statusCheckRollup \
--jq '[.statusCheckRollup[]? | select(.name != null and .status != "COMPLETED")] | length')

if [[ "$CHECKS_PENDING" -gt 0 ]]; then
echo " Skipping — $CHECKS_PENDING check(s) still pending"
continue
fi

if [[ "$CHECKS_PASS" != "true" ]]; then
echo " Skipping — some checks failed"
continue
fi

echo " All checks pass — merging PR #$PR_NUMBER"
if gh api "repos/$REPO/pulls/$PR_NUMBER/merge" \
-X PUT -f merge_method=squash \
--silent; then
echo " Merged PR #$PR_NUMBER"
MERGED=true
else
echo " Warning: failed to merge PR #$PR_NUMBER"
fi
done <<< "$PRS"
uses: petry-projects/.github/.github/workflows/dependabot-rebase-reusable.yml@v1
secrets: inherit
Loading
Loading