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
37 changes: 26 additions & 11 deletions .github/workflows/dependabot-auto-merge.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
name: Dependabot Auto-Merge

# Safely auto-merges Dependabot PRs after CI passes. Scope is narrow:
# - Only runs when github.actor == 'dependabot[bot]' (not spoofable;
# GitHub sets this from the authenticated user).
# - Only runs when github.event.pull_request.user.login ==
# 'dependabot[bot]' (who opened the PR). github.actor is not used
# here: it reflects the last actor to touch the ref, not who opened
# the PR, so it's spoofable by pushing a commit that makes Dependabot
# the last committer on an attacker-controlled branch (confirmed
# against zizmor's bot-conditions audit docs). The user.login check
# still means a human pushing to an existing Dependabot PR
# (`synchronize`) can trigger this job, but the only action taken is
# `gh pr merge --auto`, which requires all status checks to pass
# first — see smartwatermelon/dev-env#40.
# - Patch/minor: always auto-merged.
# - Major: only auto-merged when EVERY listed dependency belongs to a
# trusted namespace (dependabot/, actions/, smartwatermelon/). One
Expand All @@ -16,15 +24,16 @@ name: Dependabot Auto-Merge
# 2026-04-28, fetch-metadata@v2 mislabeled a 2.0.1 -> 3.0.0
# reusable-workflow bump as semver-patch; trust math, not labels.
#
# `gh pr review --approve` satisfies branch-protection rules that
# require review. `--auto` means the merge only happens after all
# status checks pass; failing CI leaves the PR open indefinitely.
# No approving review is required by branch protection on this repo
# (confirmed via fleet audit, dev-env#21), so the job only enables
# auto-merge; `--auto` means the merge only happens after all status
# checks pass, and failing CI leaves the PR open indefinitely.
#
# Provisioned 2026-04-18 (v2.0.1 / Phase 5). Hardened 2026-04-28 with
# the trusted-namespace allowlist + version-comparison defense. See
# docs/plans/2026-04-28-dependabot-auto-merge-c2.md.

on:
on: # zizmor: ignore[dangerous-triggers] intentional: dependabot-only via user.login check, minimal permissions, no PR code execution
pull_request_target:
types: [opened, synchronize, reopened]

Expand All @@ -34,12 +43,12 @@ permissions:

jobs:
auto-merge:
if: github.actor == 'dependabot[bot]'
if: github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Fetch Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v3
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3

- name: Decide if PR is auto-mergeable
id: policy
Expand Down Expand Up @@ -71,10 +80,17 @@ jobs:
echo "::notice::Major bump v$prev_major -> v$new_major with empty dependency-names; leaving for manual review"
exit 0
fi
remainder=$(printf '%s' "$DEP_NAMES" \
all_names=$(printf '%s' "$DEP_NAMES" \
| tr ',' '\n' \
| sed -E 's@^[[:space:]]+|[[:space:]]+$@@g' \
| grep -v '^$' \
|| true)
if [ -z "$all_names" ]; then
echo "decision=skip" >> "$GITHUB_OUTPUT"
echo "::notice::Major bump v$prev_major -> v$new_major with no parseable dependency names; leaving for manual review"
exit 0
fi
remainder=$(printf '%s' "$all_names" \
| grep -vE '^(dependabot|actions|smartwatermelon)/' \
|| true)
if [ -z "$remainder" ]; then
Expand All @@ -85,11 +101,10 @@ jobs:
echo "::notice::Major bump v$prev_major -> v$new_major; non-allowlisted deps present: $remainder"
fi

- name: Approve and enable auto-merge
- name: Enable auto-merge
if: steps.policy.outputs.decision == 'merge'
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr review --approve "$PR_URL"
gh pr merge --auto --squash --delete-branch "$PR_URL"
78 changes: 41 additions & 37 deletions scripts/drop-noop-approve-step.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,46 @@ printf 'repo\tstatus\tdetail\n' >"${RESULTS_LOG}"

trap 'cp "${RESULTS_LOG}" "${FINAL_LOG}" 2>/dev/null || true; rm -rf "${SCRATCH_ROOT}"' EXIT

commit_msg_file="${SCRATCH_ROOT}/commit-msg.txt"
cat >"${commit_msg_file}" <<'EOF'
chore(ci): drop no-op gh pr review --approve step

No repo in either org currently requires an approving review
(required_approving_review_count is 0 fleet-wide), so this step never
did anything -- gh pr merge --auto already proceeds once required
status checks pass. Removing the dead step; auto-merge behavior is
unchanged.

Ref: smartwatermelon/dev-env#21
EOF

pr_body_file="${SCRATCH_ROOT}/pr-body.txt"
cat >"${pr_body_file}" <<'EOF'
Removes the `gh pr review --approve` line from `dependabot-auto-merge.yml`.

No repo in either org currently requires an approving review
(`required_approving_review_count` is 0 fleet-wide, confirmed by audit
in smartwatermelon/dev-env#21), so this step has never done anything —
`gh pr merge --auto` already merges once required status checks pass.
Auto-merge behavior is unchanged; this only removes dead weight. The
`pull-requests: write` permission stays as-is — `gh pr merge --auto`
also requires it, independent of the removed approve call.

Part of the fleet-wide remediation tracked in smartwatermelon/dev-env#21.
This PR is not merged automatically — merge requires separate explicit
authorization, same as any other PR.
EOF

for repo in "${REPOS[@]}"; do
echo "=== ${repo} ==="
clone_dir="${SCRATCH_ROOT}/${repo//\//-}"
clone_err_log="${SCRATCH_ROOT}/clone-err.log"
: >"${clone_err_log}"

if ! git clone --quiet "git@github.com:${repo}.git" "${clone_dir}" 2>"${SCRATCH_ROOT}/clone-err.log"; then
echo " clone failed, skipping"
printf '%s\tskipped\tclone failed\n' "${repo}" >>"${RESULTS_LOG}"
if ! git clone --quiet "git@github.com:${repo}.git" "${clone_dir}" 2>"${clone_err_log}"; then
clone_err_detail=$(tr '\n\t' ' ' <"${clone_err_log}")
echo " clone failed: ${clone_err_detail}"
printf '%s\tskipped\tclone failed: %s\n' "${repo}" "${clone_err_detail}" >>"${RESULTS_LOG}"
continue
fi

Expand All @@ -105,19 +138,6 @@ for repo in "${REPOS[@]}"; do
continue
fi

commit_msg_file="${SCRATCH_ROOT}/commit-msg.txt"
cat >"${commit_msg_file}" <<'EOF'
chore(ci): drop no-op gh pr review --approve step

No repo in either org currently requires an approving review
(required_approving_review_count is 0 fleet-wide), so this step never
did anything -- gh pr merge --auto already proceeds once required
status checks pass. Removing the dead step; auto-merge behavior is
unchanged.

Ref: smartwatermelon/dev-env#21
EOF

git_err_log="${SCRATCH_ROOT}/git-err.log"
git_err_detail=""

Expand All @@ -133,10 +153,10 @@ EOF
printf '%s\tfailed\tgit add failed: %s\n' "${repo}" "${git_err_detail}" >>"${RESULTS_LOG}"
continue
fi
# Note: git hooks are NOT cloned with a repo, so no local pre-commit
# review hook runs here regardless of what's installed in a normal
# working copy of these repos -- a commit failure below is a plain
# git-mechanics problem, not a hook rejection.
# Note: core.hooksPath is a machine-global git config, so the local
# pre-commit hook DOES run here even though hooks aren't cloned with
# the repo -- a commit failure below may be a hook rejection (see
# retry-noop-approve-zizmor-fixes.sh, written to handle exactly that).
if ! git -C "${clone_dir}" commit -F "${commit_msg_file}" >"${git_err_log}" 2>&1; then
git_err_detail=$(tr '\n\t' ' ' <"${git_err_log}")
echo " git commit failed: ${git_err_detail}"
Expand All @@ -155,22 +175,6 @@ EOF
continue
fi

pr_body_file="${SCRATCH_ROOT}/pr-body.txt"
cat >"${pr_body_file}" <<'EOF'
Removes the `gh pr review --approve` line from `dependabot-auto-merge.yml`.

No repo in either org currently requires an approving review
(`required_approving_review_count` is 0 fleet-wide, confirmed by audit
in smartwatermelon/dev-env#21), so this step has never done anything —
`gh pr merge --auto` already merges once required status checks pass.
Auto-merge behavior is unchanged; this only removes dead weight and
the associated `pull-requests: write` usage for the approve call.

Part of the fleet-wide remediation tracked in smartwatermelon/dev-env#21.
This PR is not merged automatically — merge requires separate explicit
authorization, same as any other PR.
EOF

if pr_url=$(gh pr create --repo "${repo}" \
--head "${BRANCH_NAME}" \
--title "chore(ci): drop no-op gh pr review --approve step" \
Expand All @@ -180,7 +184,7 @@ EOF
printf '%s\topened\t%s\n' "${repo}" "${pr_url}" >>"${RESULTS_LOG}"
else
pr_err_detail=$(tr '\n\t' ' ' <"${SCRATCH_ROOT}/pr-err.log")
echo " gh pr create failed, see log"
echo " gh pr create failed: ${pr_err_detail}"
printf '%s\tfailed\t%s\n' "${repo}" "${pr_err_detail}" >>"${RESULTS_LOG}"
fi
done
Expand Down
Loading
Loading