From 97b68b3a9cd748be5d3c7d0a081b7e6e51b399e6 Mon Sep 17 00:00:00 2001 From: Don Petry Date: Sun, 22 Mar 2026 22:34:00 -0500 Subject: [PATCH 1/5] fix(ci): use admin PAT for Dependabot auto-merge to bypass ruleset The GITHUB_TOKEN runs as github-actions[bot] (integration 15368) which is not in the repository ruleset bypass list. Auto-merge silently stalls because the merge actor cannot bypass the protect-branches ruleset. Switch to GH_ADMIN_PAT so merges execute as a repo admin with bypass permissions, and add a PR approval step. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/dependabot-automerge.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index 047758eb..f61b7064 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -20,16 +20,17 @@ jobs: with: github-token: '${{ secrets.GITHUB_TOKEN }}' - - name: Enable auto-merge for patch, minor, and indirect updates + - name: Approve and auto-merge patch, minor, and indirect updates if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.dependency-type == 'indirect' run: | - # Enable auto-merge to handle future check completions + # Approve the PR to satisfy any review requirements + gh pr review --approve "$PR_URL" + # Enable auto-merge — requires a token whose actor can bypass + # the repository ruleset. GITHUB_TOKEN (github-actions[bot], + # integration 15368) is NOT in the ruleset bypass list, so + # auto-merge silently stalls. GH_ADMIN_PAT from a repo admin + # merges as that user, who has RepositoryRole bypass. gh pr merge --auto --squash "$PR_URL" - # If all required checks are already passing, merge immediately since - # auto-merge only triggers on subsequent check completions, not existing ones - if [ "$(gh pr view "$PR_URL" --json mergeable --jq '.mergeable')" = "true" ]; then - gh pr merge --squash "$PR_URL" - fi env: PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GH_ADMIN_PAT }} From 9d25060c731fb26fb30128103ecf198fdd910909 Mon Sep 17 00:00:00 2001 From: Don Petry Date: Sun, 22 Mar 2026 22:49:22 -0500 Subject: [PATCH 2/5] fix(ci): use GitHub App for Dependabot auto-merge to bypass ruleset The GITHUB_TOKEN runs as github-actions[bot] (integration 15368) which is not in the repository ruleset bypass list. Auto-merge silently stalls because the merge actor cannot bypass the protect-branches ruleset. Create a dedicated GitHub App with contents:write and pull_requests:write permissions. The workflow generates short-lived tokens via actions/create-github-app-token, and the app is added to the ruleset bypass list so auto-merge can execute. Includes setup script: scripts/setup-dependabot-app.sh Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/dependabot-automerge.yml | 18 +- scripts/setup-dependabot-app.sh | 248 +++++++++++++++++++++ 2 files changed, 258 insertions(+), 8 deletions(-) create mode 100755 scripts/setup-dependabot-app.sh diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index f61b7064..a1b1536c 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -20,17 +20,19 @@ jobs: with: github-token: '${{ secrets.GITHUB_TOKEN }}' - - name: Approve and auto-merge patch, minor, and indirect updates + - name: Generate app token + if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.dependency-type == 'indirect' + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Approve and auto-merge if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.dependency-type == 'indirect' run: | - # Approve the PR to satisfy any review requirements gh pr review --approve "$PR_URL" - # Enable auto-merge — requires a token whose actor can bypass - # the repository ruleset. GITHUB_TOKEN (github-actions[bot], - # integration 15368) is NOT in the ruleset bypass list, so - # auto-merge silently stalls. GH_ADMIN_PAT from a repo admin - # merges as that user, who has RepositoryRole bypass. gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ secrets.GH_ADMIN_PAT }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/scripts/setup-dependabot-app.sh b/scripts/setup-dependabot-app.sh new file mode 100755 index 00000000..b3620e3b --- /dev/null +++ b/scripts/setup-dependabot-app.sh @@ -0,0 +1,248 @@ +#!/usr/bin/env bash +# setup-dependabot-app.sh +# +# Creates a GitHub App for Dependabot auto-merge, installs it on the repo, +# stores credentials as repo secrets, and adds it to the ruleset bypass list. +# +# Prerequisites: gh (authenticated), python3, openssl, xdg-open or open +# Usage: bash scripts/setup-dependabot-app.sh + +set -euo pipefail + +# ── Configuration ───────────────────────────────────────────────────────────── +REPO="${REPO:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" +OWNER="${REPO%%/*}" +REPO_NAME="${REPO##*/}" +APP_NAME="${APP_NAME:-dependabot-merger-${REPO_NAME}}" +CALLBACK_PORT="${CALLBACK_PORT:-8976}" +RULESET_NAME="protect-branches" + +echo "=== GitHub App Setup for Dependabot Auto-Merge ===" +echo " Repo: $REPO" +echo " App name: $APP_NAME" +echo "" + +# ── Preflight checks ───────────────────────────────────────────────────────── +for cmd in gh python3 openssl; do + command -v "$cmd" >/dev/null || { echo "Error: $cmd is required but not found"; exit 1; } +done + +gh auth status >/dev/null 2>&1 || { echo "Error: gh is not authenticated. Run: gh auth login"; exit 1; } + +# Determine if owner is an org or user (affects manifest URL) +OWNER_TYPE=$(gh api "users/$OWNER" -q '.type' 2>/dev/null || echo "User") +if [ "$OWNER_TYPE" = "Organization" ]; then + MANIFEST_URL="https://github.com/organizations/$OWNER/settings/apps/new" +else + MANIFEST_URL="https://github.com/settings/apps/new" +fi + +# ── Step 1: Create GitHub App via manifest flow ─────────────────────────────── +echo "Step 1/4: Creating GitHub App via manifest flow..." + +MANIFEST=$(python3 -c " +import json, sys +print(json.dumps({ + 'name': '$APP_NAME', + 'url': 'https://github.com/$REPO', + 'hook_attributes': {'active': False}, + 'redirect_url': 'http://localhost:$CALLBACK_PORT/callback', + 'public': False, + 'default_permissions': { + 'contents': 'write', + 'pull_requests': 'write' + }, + 'default_events': [] +})) +") + +# Base64-encode manifest so we can safely embed it in HTML without escaping issues +MANIFEST_B64=$(echo -n "$MANIFEST" | base64 | tr -d '\n') + +TMPHTML=$(mktemp /tmp/gh-app-manifest-XXXX.html) +cat > "$TMPHTML" < + +

Redirecting to GitHub to create the app...

+
+ +
+ + +HTMLEOF + +# Start a one-shot HTTP server to catch the OAuth callback +CODEFILE=$(mktemp /tmp/gh-app-code-XXXX.txt) +python3 -c " +import http.server, urllib.parse, threading + +class H(http.server.BaseHTTPRequestHandler): + def do_GET(self): + code = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query).get('code', [''])[0] + with open('$CODEFILE', 'w') as f: + f.write(code) + self.send_response(200) + self.end_headers() + self.wfile.write(b'

App created! You can close this tab.

') + threading.Thread(target=self.server.shutdown).start() + def log_message(self, *a): pass + +http.server.HTTPServer(('127.0.0.1', $CALLBACK_PORT), H).serve_forever() +" & +SERVER_PID=$! + +# Open browser +open_url() { + for opener in xdg-open open garcon-url-handler; do + if command -v "$opener" >/dev/null; then + "$opener" "$1" 2>/dev/null & + return + fi + done + echo " Please open this URL manually: $1" +} + +open_url "file://$TMPHTML" +echo " Browser opened. Click 'Create GitHub App' on GitHub." +echo " Waiting for callback..." + +wait "$SERVER_PID" 2>/dev/null || true +CODE=$(cat "$CODEFILE" 2>/dev/null || echo "") +rm -f "$TMPHTML" "$CODEFILE" + +if [ -z "$CODE" ]; then + echo "Error: No callback code received from GitHub." + exit 1 +fi + +# Exchange code for app credentials +echo " Exchanging code for credentials..." +CREDENTIALS=$(gh api "app-manifests/$CODE/conversions" --method POST) + +APP_ID=$(echo "$CREDENTIALS" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") +APP_SLUG=$(echo "$CREDENTIALS" | python3 -c "import sys,json; print(json.load(sys.stdin)['slug'])") +APP_PEM=$(echo "$CREDENTIALS" | python3 -c "import sys,json; print(json.load(sys.stdin)['pem'])") + +echo " Created: $APP_SLUG (ID: $APP_ID)" + +# ── Step 2: Store secrets ───────────────────────────────────────────────────── +echo "" +echo "Step 2/4: Storing secrets..." + +echo "$APP_ID" | gh secret set APP_ID --repo "$REPO" +echo " ✓ APP_ID" + +echo "$APP_PEM" | gh secret set APP_PRIVATE_KEY --repo "$REPO" +echo " ✓ APP_PRIVATE_KEY" + +# ── Step 3: Install app on repo ─────────────────────────────────────────────── +echo "" +echo "Step 3/4: Installing app on repository..." + +INSTALL_URL="https://github.com/apps/$APP_SLUG/installations/new" +open_url "$INSTALL_URL" +echo " Browser opened. Select 'Only select repositories' → $REPO_NAME, then click Install." +echo "" +read -rp " Press Enter after installing the app..." + +# Verify installation by generating a JWT and querying the API +echo " Verifying installation..." + +JWT=$(python3 -c " +import time, base64, json, subprocess, sys + +def b64url(data): + return base64.urlsafe_b64encode(data).rstrip(b'=').decode() + +now = int(time.time()) +header = b64url(json.dumps({'alg': 'RS256', 'typ': 'JWT'}).encode()) +payload = b64url(json.dumps({'iat': now - 60, 'exp': now + 600, 'iss': '$APP_ID'}).encode()) +signing_input = f'{header}.{payload}'.encode() + +result = subprocess.run( + ['openssl', 'dgst', '-sha256', '-sign', '/dev/stdin', '-binary'], + input='''$APP_PEM'''.encode(), + capture_output=True +) +signature = b64url(result.stdout) +print(f'{header}.{payload}.{signature}') +") + +INSTALL_COUNT=$(python3 -c " +import urllib.request, json +req = urllib.request.Request( + 'https://api.github.com/app/installations', + headers={ + 'Authorization': 'Bearer $JWT', + 'Accept': 'application/vnd.github+json' + } +) +data = json.loads(urllib.request.urlopen(req).read()) +print(len(data)) +" 2>/dev/null || echo "0") + +if [ "$INSTALL_COUNT" -gt 0 ]; then + echo " ✓ App is installed ($INSTALL_COUNT installation(s))" +else + echo " ⚠ Could not verify installation. The app may need to be installed manually at:" + echo " $INSTALL_URL" +fi + +# ── Step 4: Add app to ruleset bypass list ──────────────────────────────────── +echo "" +echo "Step 4/4: Adding app to ruleset bypass list..." + +# Find the ruleset ID by name +RULESET_ID=$(gh api "repos/$REPO/rulesets" -q ".[] | select(.name == \"$RULESET_NAME\") | .id" 2>/dev/null || echo "") + +if [ -z "$RULESET_ID" ]; then + echo " ⚠ Ruleset '$RULESET_NAME' not found. You may need to add the app to the bypass list manually." +else + # Get current ruleset, add bypass actor, and update + CURRENT_RULESET=$(gh api "repos/$REPO/rulesets/$RULESET_ID") + + UPDATED_RULESET=$(echo "$CURRENT_RULESET" | python3 -c " +import sys, json + +ruleset = json.load(sys.stdin) +app_id = int('$APP_ID') + +# Check if already in bypass list +bypass = ruleset.get('bypass_actors', []) +if not any(a.get('actor_id') == app_id and a.get('actor_type') == 'Integration' for a in bypass): + bypass.append({ + 'actor_id': app_id, + 'actor_type': 'Integration', + 'bypass_mode': 'always' + }) + +# Build update payload (only mutable fields) +update = { + 'name': ruleset['name'], + 'target': ruleset.get('target', 'branch'), + 'enforcement': ruleset['enforcement'], + 'conditions': ruleset.get('conditions', {}), + 'rules': ruleset.get('rules', []), + 'bypass_actors': bypass +} +print(json.dumps(update)) +") + + echo "$UPDATED_RULESET" | gh api "repos/$REPO/rulesets/$RULESET_ID" \ + --method PUT --input - >/dev/null 2>&1 + + echo " ✓ App (ID: $APP_ID) added to '$RULESET_NAME' bypass list" +fi + +# ── Done ────────────────────────────────────────────────────────────────────── +echo "" +echo "=== Setup complete ===" +echo "" +echo " App: $APP_SLUG (ID: $APP_ID)" +echo " Secrets: APP_ID, APP_PRIVATE_KEY" +echo " Bypass: added to $RULESET_NAME ruleset" +echo "" +echo " The Dependabot auto-merge workflow will now use this app to merge PRs." From 6480a9e6aa64aa5b110ee62bcab2e7e5d884a76d Mon Sep 17 00:00:00 2001 From: Don Petry Date: Mon, 23 Mar 2026 14:23:36 -0500 Subject: [PATCH 3/5] fix(ci): replace manifest-flow script with per-repo setup script The GitHub App manifest flow had HTML encoding issues, so the app was created manually. Replace the setup script with a practical per-repo version that stores secrets and updates the ruleset bypass list for any repo in the org where the app is already installed. Co-Authored-By: Claude Opus 4.6 (1M context) --- scripts/setup-dependabot-app.sh | 298 ++++++++++---------------------- 1 file changed, 89 insertions(+), 209 deletions(-) diff --git a/scripts/setup-dependabot-app.sh b/scripts/setup-dependabot-app.sh index b3620e3b..0018abca 100755 --- a/scripts/setup-dependabot-app.sh +++ b/scripts/setup-dependabot-app.sh @@ -1,248 +1,128 @@ #!/usr/bin/env bash # setup-dependabot-app.sh # -# Creates a GitHub App for Dependabot auto-merge, installs it on the repo, -# stores credentials as repo secrets, and adds it to the ruleset bypass list. +# Configures a repository to use the org-wide "dependabot-automerge-petry" +# GitHub App for Dependabot auto-merge. The app must already be created and +# installed on the org (see PR #71 for context). # -# Prerequisites: gh (authenticated), python3, openssl, xdg-open or open -# Usage: bash scripts/setup-dependabot-app.sh +# What this script does: +# 1. Stores APP_ID and APP_PRIVATE_KEY as repo secrets +# 2. Adds the app to the repo's ruleset bypass list (if a matching ruleset exists) +# 3. Copies the dependabot-automerge workflow into the repo +# +# Prerequisites: gh (authenticated), python3 +# Usage: +# bash scripts/setup-dependabot-app.sh # current repo +# bash scripts/setup-dependabot-app.sh owner/other-repo # specific repo +# APP_PRIVATE_KEY_FILE=path/to/key.pem bash scripts/setup-dependabot-app.sh set -euo pipefail # ── Configuration ───────────────────────────────────────────────────────────── -REPO="${REPO:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" -OWNER="${REPO%%/*}" -REPO_NAME="${REPO##*/}" -APP_NAME="${APP_NAME:-dependabot-merger-${REPO_NAME}}" -CALLBACK_PORT="${CALLBACK_PORT:-8976}" -RULESET_NAME="protect-branches" - -echo "=== GitHub App Setup for Dependabot Auto-Merge ===" -echo " Repo: $REPO" -echo " App name: $APP_NAME" -echo "" - -# ── Preflight checks ───────────────────────────────────────────────────────── -for cmd in gh python3 openssl; do - command -v "$cmd" >/dev/null || { echo "Error: $cmd is required but not found"; exit 1; } -done - -gh auth status >/dev/null 2>&1 || { echo "Error: gh is not authenticated. Run: gh auth login"; exit 1; } +APP_ID="${APP_ID:-3167543}" +APP_NAME="dependabot-automerge-petry" +RULESET_NAME="${RULESET_NAME:-protect-branches}" -# Determine if owner is an org or user (affects manifest URL) -OWNER_TYPE=$(gh api "users/$OWNER" -q '.type' 2>/dev/null || echo "User") -if [ "$OWNER_TYPE" = "Organization" ]; then - MANIFEST_URL="https://github.com/organizations/$OWNER/settings/apps/new" +# Target repo: first arg, or detect from current directory +if [ -n "${1:-}" ]; then + REPO="$1" else - MANIFEST_URL="https://github.com/settings/apps/new" + REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || true)" + if [ -z "$REPO" ]; then + echo "Usage: $0 [owner/repo]" + exit 1 + fi fi -# ── Step 1: Create GitHub App via manifest flow ─────────────────────────────── -echo "Step 1/4: Creating GitHub App via manifest flow..." - -MANIFEST=$(python3 -c " -import json, sys -print(json.dumps({ - 'name': '$APP_NAME', - 'url': 'https://github.com/$REPO', - 'hook_attributes': {'active': False}, - 'redirect_url': 'http://localhost:$CALLBACK_PORT/callback', - 'public': False, - 'default_permissions': { - 'contents': 'write', - 'pull_requests': 'write' - }, - 'default_events': [] -})) -") +echo "=== Dependabot Auto-Merge Setup ===" +echo " App: $APP_NAME (ID: $APP_ID)" +echo " Repo: $REPO" +echo "" -# Base64-encode manifest so we can safely embed it in HTML without escaping issues -MANIFEST_B64=$(echo -n "$MANIFEST" | base64 | tr -d '\n') - -TMPHTML=$(mktemp /tmp/gh-app-manifest-XXXX.html) -cat > "$TMPHTML" < - -

Redirecting to GitHub to create the app...

-
- -
- - -HTMLEOF - -# Start a one-shot HTTP server to catch the OAuth callback -CODEFILE=$(mktemp /tmp/gh-app-code-XXXX.txt) -python3 -c " -import http.server, urllib.parse, threading - -class H(http.server.BaseHTTPRequestHandler): - def do_GET(self): - code = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query).get('code', [''])[0] - with open('$CODEFILE', 'w') as f: - f.write(code) - self.send_response(200) - self.end_headers() - self.wfile.write(b'

App created! You can close this tab.

') - threading.Thread(target=self.server.shutdown).start() - def log_message(self, *a): pass - -http.server.HTTPServer(('127.0.0.1', $CALLBACK_PORT), H).serve_forever() -" & -SERVER_PID=$! - -# Open browser -open_url() { - for opener in xdg-open open garcon-url-handler; do - if command -v "$opener" >/dev/null; then - "$opener" "$1" 2>/dev/null & - return +# ── Preflight ───────────────────────────────────────────────────────────────── +for cmd in gh python3; do + command -v "$cmd" >/dev/null || { echo "Error: $cmd is required"; exit 1; } +done +gh auth status >/dev/null 2>&1 || { echo "Error: gh not authenticated"; exit 1; } + +# ── Step 1: Store secrets ───────────────────────────────────────────────────── +echo "Step 1/3: Storing secrets..." + +# Locate private key +KEY_FILE="${APP_PRIVATE_KEY_FILE:-}" +if [ -z "$KEY_FILE" ]; then + # Search common locations + for candidate in \ + "$HOME/dependabot-automerge-petry.pem" \ + "$HOME/dependabot-google-app-scripts"*.pem \ + "$HOME/.ssh/dependabot-automerge-petry.pem"; do + if [ -f "$candidate" ]; then + KEY_FILE="$candidate" + break fi done - echo " Please open this URL manually: $1" -} - -open_url "file://$TMPHTML" -echo " Browser opened. Click 'Create GitHub App' on GitHub." -echo " Waiting for callback..." - -wait "$SERVER_PID" 2>/dev/null || true -CODE=$(cat "$CODEFILE" 2>/dev/null || echo "") -rm -f "$TMPHTML" "$CODEFILE" +fi -if [ -z "$CODE" ]; then - echo "Error: No callback code received from GitHub." +if [ -z "$KEY_FILE" ] || [ ! -f "$KEY_FILE" ]; then + echo "Error: Private key file not found." + echo "Set APP_PRIVATE_KEY_FILE or place the .pem file in your home directory." exit 1 fi -# Exchange code for app credentials -echo " Exchanging code for credentials..." -CREDENTIALS=$(gh api "app-manifests/$CODE/conversions" --method POST) - -APP_ID=$(echo "$CREDENTIALS" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") -APP_SLUG=$(echo "$CREDENTIALS" | python3 -c "import sys,json; print(json.load(sys.stdin)['slug'])") -APP_PEM=$(echo "$CREDENTIALS" | python3 -c "import sys,json; print(json.load(sys.stdin)['pem'])") - -echo " Created: $APP_SLUG (ID: $APP_ID)" - -# ── Step 2: Store secrets ───────────────────────────────────────────────────── -echo "" -echo "Step 2/4: Storing secrets..." - echo "$APP_ID" | gh secret set APP_ID --repo "$REPO" echo " ✓ APP_ID" -echo "$APP_PEM" | gh secret set APP_PRIVATE_KEY --repo "$REPO" -echo " ✓ APP_PRIVATE_KEY" - -# ── Step 3: Install app on repo ─────────────────────────────────────────────── -echo "" -echo "Step 3/4: Installing app on repository..." - -INSTALL_URL="https://github.com/apps/$APP_SLUG/installations/new" -open_url "$INSTALL_URL" -echo " Browser opened. Select 'Only select repositories' → $REPO_NAME, then click Install." -echo "" -read -rp " Press Enter after installing the app..." - -# Verify installation by generating a JWT and querying the API -echo " Verifying installation..." - -JWT=$(python3 -c " -import time, base64, json, subprocess, sys - -def b64url(data): - return base64.urlsafe_b64encode(data).rstrip(b'=').decode() - -now = int(time.time()) -header = b64url(json.dumps({'alg': 'RS256', 'typ': 'JWT'}).encode()) -payload = b64url(json.dumps({'iat': now - 60, 'exp': now + 600, 'iss': '$APP_ID'}).encode()) -signing_input = f'{header}.{payload}'.encode() - -result = subprocess.run( - ['openssl', 'dgst', '-sha256', '-sign', '/dev/stdin', '-binary'], - input='''$APP_PEM'''.encode(), - capture_output=True -) -signature = b64url(result.stdout) -print(f'{header}.{payload}.{signature}') -") - -INSTALL_COUNT=$(python3 -c " -import urllib.request, json -req = urllib.request.Request( - 'https://api.github.com/app/installations', - headers={ - 'Authorization': 'Bearer $JWT', - 'Accept': 'application/vnd.github+json' - } -) -data = json.loads(urllib.request.urlopen(req).read()) -print(len(data)) -" 2>/dev/null || echo "0") - -if [ "$INSTALL_COUNT" -gt 0 ]; then - echo " ✓ App is installed ($INSTALL_COUNT installation(s))" -else - echo " ⚠ Could not verify installation. The app may need to be installed manually at:" - echo " $INSTALL_URL" -fi +gh secret set APP_PRIVATE_KEY --repo "$REPO" < "$KEY_FILE" +echo " ✓ APP_PRIVATE_KEY (from $KEY_FILE)" -# ── Step 4: Add app to ruleset bypass list ──────────────────────────────────── +# ── Step 2: Add app to ruleset bypass list ──────────────────────────────────── echo "" -echo "Step 4/4: Adding app to ruleset bypass list..." +echo "Step 2/3: Updating ruleset bypass list..." -# Find the ruleset ID by name RULESET_ID=$(gh api "repos/$REPO/rulesets" -q ".[] | select(.name == \"$RULESET_NAME\") | .id" 2>/dev/null || echo "") if [ -z "$RULESET_ID" ]; then - echo " ⚠ Ruleset '$RULESET_NAME' not found. You may need to add the app to the bypass list manually." + echo " ⏭ No '$RULESET_NAME' ruleset found — skipping" else - # Get current ruleset, add bypass actor, and update - CURRENT_RULESET=$(gh api "repos/$REPO/rulesets/$RULESET_ID") + CURRENT=$(gh api "repos/$REPO/rulesets/$RULESET_ID") - UPDATED_RULESET=$(echo "$CURRENT_RULESET" | python3 -c " + UPDATED=$(echo "$CURRENT" | python3 -c " import sys, json - -ruleset = json.load(sys.stdin) -app_id = int('$APP_ID') - -# Check if already in bypass list -bypass = ruleset.get('bypass_actors', []) +r = json.load(sys.stdin) +app_id = $APP_ID +bypass = r.get('bypass_actors', []) if not any(a.get('actor_id') == app_id and a.get('actor_type') == 'Integration' for a in bypass): - bypass.append({ - 'actor_id': app_id, - 'actor_type': 'Integration', - 'bypass_mode': 'always' - }) - -# Build update payload (only mutable fields) -update = { - 'name': ruleset['name'], - 'target': ruleset.get('target', 'branch'), - 'enforcement': ruleset['enforcement'], - 'conditions': ruleset.get('conditions', {}), - 'rules': ruleset.get('rules', []), - 'bypass_actors': bypass -} -print(json.dumps(update)) + bypass.append({'actor_id': app_id, 'actor_type': 'Integration', 'bypass_mode': 'always'}) + print(json.dumps({ + 'name': r['name'], 'target': r.get('target', 'branch'), + 'enforcement': r['enforcement'], 'conditions': r.get('conditions', {}), + 'rules': r.get('rules', []), 'bypass_actors': bypass + })) +else: + print('ALREADY_PRESENT') ") - echo "$UPDATED_RULESET" | gh api "repos/$REPO/rulesets/$RULESET_ID" \ - --method PUT --input - >/dev/null 2>&1 + if [ "$UPDATED" = "ALREADY_PRESENT" ]; then + echo " ✓ App already in bypass list" + else + echo "$UPDATED" | gh api "repos/$REPO/rulesets/$RULESET_ID" --method PUT --input - >/dev/null + echo " ✓ App added to '$RULESET_NAME' bypass list" + fi +fi + +# ── Step 3: Ensure workflow exists ──────────────────────────────────────────── +echo "" +echo "Step 3/3: Checking workflow..." - echo " ✓ App (ID: $APP_ID) added to '$RULESET_NAME' bypass list" +WORKFLOW_EXISTS=$(gh api "repos/$REPO/contents/.github/workflows/dependabot-automerge.yml" --jq '.name' 2>/dev/null || echo "") + +if [ -n "$WORKFLOW_EXISTS" ]; then + echo " ✓ dependabot-automerge.yml already exists" +else + echo " ⚠ dependabot-automerge.yml not found in $REPO" + echo " Copy .github/workflows/dependabot-automerge.yml into the repo." fi # ── Done ────────────────────────────────────────────────────────────────────── echo "" -echo "=== Setup complete ===" -echo "" -echo " App: $APP_SLUG (ID: $APP_ID)" -echo " Secrets: APP_ID, APP_PRIVATE_KEY" -echo " Bypass: added to $RULESET_NAME ruleset" -echo "" -echo " The Dependabot auto-merge workflow will now use this app to merge PRs." +echo "=== Setup complete for $REPO ===" From 1e07ec1b2c70fa70b39ab9620d7336360d3f3866 Mon Sep 17 00:00:00 2001 From: Don Petry Date: Mon, 23 Mar 2026 14:26:28 -0500 Subject: [PATCH 4/5] fix(ci): use --admin merge instead of --auto for ruleset bypass The ruleset bypass list grants the app admin-level merge permissions, but --auto still respects ruleset rules. Use --admin to bypass the ruleset directly, matching how the app's bypass_mode is configured. Tested: PRs #68 and #69 merged successfully via app token + --admin. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/dependabot-automerge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index a1b1536c..e5304d82 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -32,7 +32,7 @@ jobs: if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.dependency-type == 'indirect' run: | gh pr review --approve "$PR_URL" - gh pr merge --auto --squash "$PR_URL" + gh pr merge --squash --admin "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ steps.app-token.outputs.token }} From 5c7a12789a6fb51a2e4d4a4406add9639e6bf52a Mon Sep 17 00:00:00 2001 From: Don Petry Date: Mon, 23 Mar 2026 14:32:08 -0500 Subject: [PATCH 5/5] fix(ci): use pull_request_target for Dependabot secret access Dependabot-triggered pull_request events do not have access to repository secrets. Switch to pull_request_target which runs in the context of the base branch and can access APP_ID and APP_PRIVATE_KEY. This is safe because the workflow only targets dependabot[bot] PRs and does not checkout any PR code. Addresses Copilot review comment on PR #71. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/dependabot-automerge.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index e5304d82..f750d57d 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -1,7 +1,7 @@ name: Dependabot auto-merge on: - pull_request: + pull_request_target: branches: - main