From c10419c203d5dd02f013d62e8d66560bee0bd0f2 Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Wed, 29 Apr 2026 14:29:07 -0700 Subject: [PATCH] chore(ci): sync workflow versions to current canonical MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two security/hygiene updates flagged by audit on 2026-04-29: 1. claude-blocking-review.yml: bump @v2.0.2 → @v3 to align with all other repos. v3 dropped the --max-turns cap (replaced by wall-clock timeout-only bound). 2. dependabot-auto-merge.yml: replace pre-2026-04-28 version with current canonical (sha256: a16674dd3fc9...). Adds: - Trusted-namespace allowlist (dependabot/, actions/, smartwatermelon/) gating major-version bumps - Version-comparison defense computing patch-vs-major from previous-version/new-version directly, bypassing the fetch-metadata@v2 mislabeling that hit on 2026-04-28 (2.0.1 → 3.0.0 returned semver-patch). This is the same workflow file already deployed in 25 of 26 sister repos; mac-server-setup was the lone outlier still on the older version. Audit context: - Article: nesbitt.io/2026/04/28/github-actions-is-the-weakest-link - Pattern: drift in safety-critical shared workflow Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/claude-blocking-review.yml | 4 +- .github/workflows/dependabot-auto-merge.yml | 64 +++++++++++++++++--- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/.github/workflows/claude-blocking-review.yml b/.github/workflows/claude-blocking-review.yml index eb9d4e5..44b278b 100644 --- a/.github/workflows/claude-blocking-review.yml +++ b/.github/workflows/claude-blocking-review.yml @@ -12,8 +12,8 @@ permissions: jobs: claude-review: - uses: smartwatermelon/github-workflows/.github/workflows/claude-blocking-review.yml@v2.0.2 + uses: smartwatermelon/github-workflows/.github/workflows/claude-blocking-review.yml@v3 with: pr_number: ${{ github.event.pull_request.number }} secrets: - claude_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} \ No newline at end of file + claude_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 42e3544..c53c7e3 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -3,20 +3,26 @@ 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 auto-merges patch + minor updates; major-version bumps are -# left open for manual review. +# - Patch/minor: always auto-merged. +# - Major: only auto-merged when EVERY listed dependency belongs to a +# trusted namespace (dependabot/, actions/, smartwatermelon/). One +# untrusted dep in a grouped update blocks the whole group. # - Uses pull_request_target so the BASE-branch workflow runs, not # the PR branch's — a PR modifying this file cannot bypass itself. # - Never executes PR code; the only action taken is `gh pr merge # --auto`, which is a GitHub-side API call. +# - Computes patch-vs-major from previous-version/new-version itself +# rather than trusting steps.metadata.outputs.update-type. On +# 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. # -# Provisioned 2026-04-18 as part of the v2.0.1 / Dependabot rollout -# (Phase 5). See the playbook at -# smartwatermelon/github-workflows/docs/plans/2026-04-18-v2-rollout-playbook.md +# 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: pull_request_target: @@ -35,8 +41,52 @@ jobs: id: metadata uses: dependabot/fetch-metadata@v3 - - name: Approve and enable auto-merge (patch/minor only) - if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' + - name: Decide if PR is auto-mergeable + id: policy + env: + PREV_VERSION: ${{ steps.metadata.outputs.previous-version }} + NEW_VERSION: ${{ steps.metadata.outputs.new-version }} + DEP_NAMES: ${{ steps.metadata.outputs.dependency-names }} + run: | + set -euo pipefail + extract_major() { + printf '%s' "$1" | sed -E 's@^v?([0-9]+).*@\1@' | grep -E '^[0-9]+$' || echo "" + } + prev_major=$(extract_major "$PREV_VERSION") + new_major=$(extract_major "$NEW_VERSION") + if [ -z "$prev_major" ] || [ -z "$new_major" ]; then + echo "decision=skip" >> "$GITHUB_OUTPUT" + echo "::notice::Cannot parse versions ($PREV_VERSION -> $NEW_VERSION); leaving for manual review" + exit 0 + fi + if [ "$prev_major" = "$new_major" ]; then + echo "decision=merge" >> "$GITHUB_OUTPUT" + echo "::notice::Patch/minor bump within major v$prev_major; auto-merging" + exit 0 + fi + # Major bump path: fail closed if dependency-names is missing — + # we cannot apply the trusted-namespace allowlist without it. + if [ -z "${DEP_NAMES:-}" ]; then + echo "decision=skip" >> "$GITHUB_OUTPUT" + 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" \ + | tr ',' '\n' \ + | sed -E 's@^[[:space:]]+|[[:space:]]+$@@g' \ + | grep -v '^$' \ + | grep -vE '^(dependabot|actions|smartwatermelon)/' \ + || true) + if [ -z "$remainder" ]; then + echo "decision=merge" >> "$GITHUB_OUTPUT" + echo "::notice::Major bump v$prev_major -> v$new_major in trusted namespace; auto-merging" + else + echo "decision=skip" >> "$GITHUB_OUTPUT" + echo "::notice::Major bump v$prev_major -> v$new_major; non-allowlisted deps present: $remainder" + fi + + - name: Approve and enable auto-merge + if: steps.policy.outputs.decision == 'merge' env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}