Skip to content
Closed
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
26 changes: 26 additions & 0 deletions .github/workflows/renovate-automerge.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Renovate Auto-merge

on:
workflow_run:
workflows:
- Consumer Validation
- Dependency Review
- PAT ban — no new unapproved secrets
- actionlint
- Unit Tests
types: [completed]

permissions:
contents: write
pull-requests: write

jobs:
automerge:
if: github.event.workflow_run.conclusion == 'success'
uses: ./.github/workflows/reusable-renovate-automerge.yml
with:
head_sha: ${{ github.event.workflow_run.head_sha }}
base_branch: main
secrets:
app_id: ${{ secrets.MERGERAPTOR_APP_ID }}
private_key: ${{ secrets.MERGERAPTOR_PRIVATE_KEY }}
125 changes: 81 additions & 44 deletions .github/workflows/reusable-renovate-automerge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
# uses: projectbluefin/actions/.github/workflows/reusable-renovate-automerge.yml@v1
# with:
# head_sha: ${{ github.event.workflow_run.head_sha }}
# # Optional — REQUIRED when base_branch uses a merge queue: merge-queue
# # groups created by github-actions[bot] never dispatch required checks
# # Optional — only needed when the base branch review-bypass rules
# # exclude github-actions[bot] and require the MergeRaptor app identity.
# # REQUIRED when base_branch uses a merge queue: merge-queue groups
# # created by github-actions[bot] never dispatch required checks
# # (GITHUB_TOKEN events do not trigger workflows), so the queue entry
# # wedges at AWAITING_CHECKS until it times out. Pass GitHub App
# # credentials so the merge is performed by the app instead:
Expand All @@ -47,24 +49,22 @@ on:
default: "testing"
required: false
secrets:
token:
description: >
Optional GitHub token with merge permissions. Use when the base
branch has review-bypass rules that exclude github-actions[bot]
(e.g. a mergeraptor app token). Falls back to github.token when
not provided.
required: false
app_id:
description: >
Optional GitHub App ID. When set together with private_key, a
short-lived app token is minted and used for the merge. Required
for merge-queue base branches: queue groups created by
github-actions[bot] never dispatch required checks. Takes
precedence over token.
Optional GitHub App ID used to mint the merge token for protected
branches that exclude github-actions[bot] from review bypass. Also
required for merge-queue base branches, where github-actions[bot]
queue groups never dispatch required checks.
required: false
private_key:
description: >
Private key for app_id.
Optional GitHub App private key used with app_id to mint the merge
token for protected branches.
required: false
token:
description: >
Optional GitHub token with merge permissions. Falls back to
github.token when no app token or explicit token is provided.
required: false

permissions:
Expand All @@ -75,57 +75,94 @@ jobs:
automerge:
name: Auto-merge Renovate PRs
runs-on: ubuntu-latest
env:
APP_ID: ${{ secrets.app_id }}
PRIVATE_KEY: ${{ secrets.private_key }}
steps:
- name: Mint app token
- name: Generate MergeRaptor token
if: ${{ env.APP_ID != '' && env.PRIVATE_KEY != '' }}
id: app-token
if: ${{ secrets.app_id != '' }}
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3
with:
app-id: ${{ secrets.app_id }}
private-key: ${{ secrets.private_key }}
app-id: ${{ env.APP_ID }}
private-key: ${{ env.PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write

- name: Find Renovate PR for this commit
- name: Find qualifying Renovate PR for this commit
id: find-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.token || github.token }}
HEAD_SHA: ${{ inputs.head_sha }}
BASE_BRANCH: ${{ inputs.base_branch }}
run: |
PR_NUMBER=$(gh pr list \
--repo "${{ github.repository }}" \
--base "$BASE_BRANCH" \
--state open \
--json number,headRefOid,author \
--jq ".[] | select(.headRefOid == \"$HEAD_SHA\") | select(.author.login == \"renovate[bot]\" or .author.login == \"app/mergeraptor\") | .number" \
| head -1)
PR_NUMBER=$(gh api graphql -f query="
query(\$owner: String!, \$repo: String!, \$base: String!) {
repository(owner: \$owner, name: \$repo) {
pullRequests(first: 100, states: OPEN, baseRefName: \$base) {
nodes {
number
headRefOid
author { login }
autoMergeRequest {
enabledAt
enabledBy { login }
}
}
}
}
}" \
-f owner="${GITHUB_REPOSITORY_OWNER}" \
-f repo="${GITHUB_REPOSITORY#*/}" \
-f base="$BASE_BRANCH" \
| jq -r --arg head "$HEAD_SHA" '.data.repository.pullRequests.nodes[]
| select(.headRefOid == $head)
| select(.author.login == "app/mergeraptor" or .author.login == "renovate[bot]")
| select(.autoMergeRequest != null)
| select(.autoMergeRequest.enabledBy != null)
| select(.autoMergeRequest.enabledBy.login == "app/mergeraptor" or .autoMergeRequest.enabledBy.login == "renovate[bot]")
| .number' | head -1)

if [ -z "$PR_NUMBER" ]; then
echo "No open Renovate/Mergeraptor PR found for SHA $HEAD_SHA on base $BASE_BRANCH — skipping"
echo "No eligible Renovate/Mergeraptor PR found for SHA $HEAD_SHA on base $BASE_BRANCH — skipping"
echo "pr_number=" >> "$GITHUB_OUTPUT"
else
echo "Found Renovate/Mergeraptor PR #$PR_NUMBER"
echo "Found eligible Renovate/Mergeraptor PR #$PR_NUMBER"
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
fi

- name: Merge PR
if: steps.find-pr.outputs.pr_number != ''
env:
GH_TOKEN: ${{ steps.app-token.outputs.token || secrets.token || github.token }}
PR_NUMBER: ${{ steps.find-pr.outputs.pr_number }}
run: |
# Direct squash-merge. --auto is intentionally absent:
# 1. without branch protection rules, --auto fails
# (enablePullRequestAutoMerge → "Protected branch rules not configured")
# 2. --auto uses GitHub's auto-merge queue which does NOT honour
# bypass_pull_request_allowances; only direct merges do.
# On a merge-queue branch this call enqueues instead of merging
# directly; the queue entry's actor is this step's token identity,
# and github-actions[bot] entries never dispatch required checks —
# pass app_id/private_key in that case.
# CI success is already guaranteed by the workflow_run trigger condition.
gh pr merge "${{ steps.find-pr.outputs.pr_number }}" \
--squash \
--repo "${{ github.repository }}" \
|| echo "::warning::PR merge skipped (already merged or conflicting)"
echo "✅ Merged PR #${{ steps.find-pr.outputs.pr_number }}"
set +e
CHECKS=$(gh pr checks "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json bucket,name)
CHECKS_STATUS=$?
set -e

if [ "$CHECKS_STATUS" -ne 0 ] && [ "$CHECKS_STATUS" -ne 1 ] && [ "$CHECKS_STATUS" -ne 8 ]; then
exit "$CHECKS_STATUS"
fi

if [ -z "$CHECKS" ] || ! jq -e 'type == "array"' >/dev/null 2>&1 <<<"$CHECKS"; then
echo "Failed to read PR check rollup for PR #$PR_NUMBER" >&2
exit 1
fi

if [ "$(jq 'length' <<<"$CHECKS")" -eq 0 ] ||
[ "$(jq '[.[] | select(.bucket != "pass")] | length' <<<"$CHECKS")" -ne 0 ]; then
echo "PR #$PR_NUMBER does not have a complete successful check rollup; skipping"
exit 0
fi

# Direct squash-merge is intentional: the MergeRaptor installation
# token can use the protected-branch review bypass, while queue/auto
# merge cannot rely on that app-only exception. On a merge-queue
# branch this call enqueues instead of merging directly; the queue
# entry's actor is this step's token identity, and github-actions[bot]
# entries never dispatch required checks — pass app_id/private_key in
# that case.
gh pr merge "$PR_NUMBER" --squash --repo "$GITHUB_REPOSITORY"
echo "Merged PR #$PR_NUMBER"
69 changes: 48 additions & 21 deletions docs/skills/factory-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ name: factory-operations
description: Production gate (2-human approval), promotion cadence and merge-queue contract, factory health monitor, and Renovate auto-merge.
metadata:
type: reference
context7-sources:
- /actions/create-github-app-token
- /websites/cli_github_manual
---

# Factory Operations Skill
Expand Down Expand Up @@ -178,34 +181,58 @@ issues are opened.

### What it does

Renovate runs as the MergeRaptors GitHub App and opens PRs to bump pinned action SHAs and digests. Qualifying PRs auto-merge when CI passes. If auto-merge is not enabled, an agent may merge a qualifying PR when it carries the `clanker-queue` label and all required checks pass.
Renovate runs as the MergeRaptor GitHub App and opens PRs to bump pinned action SHAs and digests. Qualifying PRs auto-merge when CI passes without human review. If auto-merge is not enabled, an agent may merge a qualifying PR when it carries the `clanker-queue` label and all required checks pass.

### Config
### Review-bypass procedure

`main` keeps required CODEOWNERS review. MergeRaptor is the only app allowed in
`required_pull_request_reviews.bypass_pull_request_allowances.apps`, and only
the CI-gated reusable workflow may mint a MergeRaptor installation token and
use it to squash-merge a Renovate-eligible PR.

The local `renovate-automerge.yml` caller is only a thin `workflow_run`
wrapper. It forwards the completed workflow SHA, `base_branch: main`, and the
MergeRaptor app credentials to the reusable workflow, which then:

1. Finds a Renovate/MergeRaptor PR for the completed SHA
2. Confirms the PR author and auto-merge enabler are Renovate/MergeRaptor
3. Requires a non-empty PR check rollup where every bucket is `pass`
4. Performs a direct squash merge with the app token

Two files co-exist:
- `.github/renovate.json5` - base org config (inherited from `projectbluefin/renovate-config`)
- `renovate.json` - repo-level overrides, including the `packageRules` automerge block

The effective automerge rule in `renovate.json`:

```json
{
"packageRules": [
{
"description": "Automerge chore dep updates (digest, pin, patch, minor) when CI passes",
"matchUpdateTypes": ["digest", "pin", "patch", "minor"],
"automerge": true,
"automergeType": "pr",
"automergeStrategy": "squash"
}
]
}
Check the live branch-protection state with:

```bash
gh api repos/projectbluefin/actions/branches/main/protection \
--jq '.required_pull_request_reviews.bypass_pull_request_allowances'
```

**What auto-merges:** SHA digest bumps, pin updates, patch and minor version bumps - when all CI checks pass. These are safe to auto-merge because they carry no behavior change. When handling the queue manually, the `clanker-queue` label authorizes an agent to merge only after confirming the PR is mergeable and every required check is green.
Expect exactly one bypass app allowance: MergeRaptor. No users or teams should
be present.

### Config

The repo-level Renovate config lives in `.github/renovate.json5`; there is no
root `renovate.json` in this repository. The checked-in config extends
`config:best-practices`, pins `baseBranchPatterns` to `main`, and automerges
pin/pinDigest updates plus GitHub Actions digest/pinDigest bumps.

**What auto-merges:** pin/pinDigest updates, plus GitHub Actions digest/pinDigest bumps, when all CI checks pass. These are safe to auto-merge because they carry no behavior change. When handling the queue manually, the `clanker-queue` label authorizes an agent to merge only after confirming the PR is mergeable and every required check is green.

**What never auto-merges:** Major version bumps and any PR that fails, has pending, or is missing required CI checks. A major bump may still be merged manually by an agent when it has `clanker-queue` and all required checks pass.

### Reusable auto-merge guardrails

The reusable Renovate auto-merge workflow must validate **who enabled auto-merge**, not just that
auto-merge is enabled. Query `pullRequest.autoMergeRequest.enabledBy` and require it to be
`app/mergeraptor` or `renovate[bot]` in addition to the PR author check. This prevents a human
from manually enabling auto-merge on a Renovate-authored major update and accidentally bypassing
the intended review requirement.

For final status checks, use `gh pr checks --json bucket,...` and merge **only** when the rollup is
non-empty and every `bucket` is `pass`. Treat `pending`, `fail`, `skipping`, and `cancel` as a
successful defer (`exit 0`) so the next `workflow_run` retry can re-evaluate, but still fail the
job on infrastructure/API errors that do not return a valid JSON check array.

**Consumer-validation exemption:** Renovate PRs (author login ending in `[bot]` or starting with `app/`) are automatically exempt from the consumer PR + CI run evidence requirement, even when they touch action files. See `docs/skills/consumer-validation.md`.

### Validation workflow
Expand Down
Loading
Loading