From c0fd42e71bfd2923ac3f7f93ba8777da1cd52c8f Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <272174644+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 13:59:43 -0500 Subject: [PATCH 1/4] chore: dependabot PR hygiene (PR template + auto-CHANGELOG workflow) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the QA gap surfaced by PR #55 (the first Dependabot-authored PR on this repo): the Dependabot body had no `## QA` section and the auto-bump didn't produce the unconditional CHANGELOG entry that CLAUDE.md § "Adding a CHANGELOG entry on every PR" requires. Two pieces: 1. `.github/PULL_REQUEST_TEMPLATE.md` — neutral scaffold with `## Summary` / `## Test plan` / `## CHANGELOG` sections plus the project-standard test-plan checklist (`pytest`, `ruff check`, `ruff format --check`, `mypy`). GitHub auto-fills this on every human-authored PR open. Dependabot bypasses the template (it supplies its own body), so the next piece picks up the slack for bot PRs. 2. `.github/workflows/dependabot-changelog.yml` — runs on `pull_request_target` (so it has write perms; Dependabot's `pull_request` event runs read-only). Filters to `github.event.pull_request.user.login == 'dependabot[bot]'`. Steps: - Checkout PR head ref. - Loop guard: skip if last commit author is `github-actions[bot]` OR `(#)` already present in CHANGELOG.md. - `dependabot/fetch-metadata@v2` to get the bump list. - Inline Python: parse `updated-dependencies-json`, compose `- **Bump group: pkg vX→vY, ...** (#N)`, prepend under `## Unreleased` → `### Changed` (creating the section if missing). - Commit as `github-actions[bot]` and push to the Dependabot branch. Why `pull_request_target` over `pull_request`: Dependabot PRs run with restricted token permissions; `pull_request` workflows on a Dependabot PR cannot push back. `pull_request_target` runs in the base branch's context with full perms, which is what we need to push a follow-up commit. Standard pattern for Dependabot augmentation workflows. Why a Python script inline: the CHANGELOG insertion needs to find `## Unreleased` and the `### Changed` subsection within it (without crossing into the next `## ` release heading) and create the subsection if absent. That's awkward in awk/sed; Python keeps it readable. Verified against this repo's CHANGELOG.md by smoke-testing the insertion logic with a synthetic deps payload — entry lands at the correct line under `## Unreleased` → `### Changed`, immediately after the section header's blank-line separator. --- .github/PULL_REQUEST_TEMPLATE.md | 33 ++++ .github/workflows/dependabot-changelog.yml | 170 +++++++++++++++++++++ CHANGELOG.md | 4 + 3 files changed, 207 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/workflows/dependabot-changelog.yml diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..62055ed --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,33 @@ + + +## Summary + + + +## Test plan + + + +- [ ] `uv run pytest tests//test_.py::test_` — passes +- [ ] `uv run ruff check src/ tests/` — clean +- [ ] `uv run ruff format --check src/ tests/` — clean +- [ ] `uv run mypy src/` — clean +- [ ] Confirm no regression in the affected module + +## CHANGELOG + + + +- [ ] Added a `## Unreleased` entry to `CHANGELOG.md` + +Closes # diff --git a/.github/workflows/dependabot-changelog.yml b/.github/workflows/dependabot-changelog.yml new file mode 100644 index 0000000..d7b4129 --- /dev/null +++ b/.github/workflows/dependabot-changelog.yml @@ -0,0 +1,170 @@ +name: Dependabot CHANGELOG + +# Auto-appends a CHANGELOG entry to Dependabot-authored PRs so they +# satisfy the project rule documented in CLAUDE.md § "Adding a +# CHANGELOG entry on every PR" without manual intervention. +# +# Triggered on pull_request_target so the workflow runs in the base +# branch's context with write permissions (Dependabot's pull_request +# event runs with read-only GITHUB_TOKEN). +# +# Loop guard: skips if the latest commit on the head branch was +# authored by `github-actions[bot]` — i.e. our own previous run. +# +# Idempotency: skips if a CHANGELOG entry referencing the PR number +# already exists. + +on: + pull_request_target: + types: [opened, synchronize, reopened] + +permissions: + contents: write + pull-requests: read + +jobs: + changelog: + if: github.event.pull_request.user.login == 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + repository: ${{ github.event.pull_request.head.repo.full_name }} + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 2 + + - name: Loop guard — skip if last commit was ours + id: guard + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + LAST_AUTHOR=$(git log -1 --pretty=%an) + echo "last_author=$LAST_AUTHOR" >> "$GITHUB_OUTPUT" + if [ "$LAST_AUTHOR" = "github-actions[bot]" ]; then + echo "Last commit authored by github-actions[bot]; skipping to avoid loop." + echo "skip=true" >> "$GITHUB_OUTPUT" + elif grep -qE "\(#${PR_NUMBER}\)" CHANGELOG.md; then + echo "CHANGELOG.md already references (#${PR_NUMBER}); skipping." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Fetch Dependabot metadata + if: steps.guard.outputs.skip != 'true' + id: meta + uses: dependabot/fetch-metadata@v2 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Compose and prepend CHANGELOG entry + if: steps.guard.outputs.skip != 'true' + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + DEPS_JSON: ${{ steps.meta.outputs.updated-dependencies-json }} + ECOSYSTEM: ${{ steps.meta.outputs.package-ecosystem }} + UPDATE_TYPE: ${{ steps.meta.outputs.update-type }} + run: | + python3 - <<'PY' + import json + import os + import pathlib + + pr_number = os.environ["PR_NUMBER"] + deps_json = os.environ.get("DEPS_JSON") or "[]" + ecosystem = os.environ.get("ECOSYSTEM") or "deps" + update_type = os.environ.get("UPDATE_TYPE") or "version-update" + + deps = json.loads(deps_json) + if not deps: + # Nothing to record — exit cleanly. + print("No dependencies in metadata; skipping.") + raise SystemExit(0) + + parts = [ + f"{d['dependencyName']} {d.get('prevVersion', '?')}→{d.get('newVersion', '?')}" + for d in deps + ] + summary = ", ".join(parts) + + # Severity hint from update-type — security updates are the + # interesting category at release time. + tag = "" + if "security" in update_type: + tag = " — picks up upstream security advisory; see PR body for CVE links." + + entry = ( + f"- **Bump {ecosystem} group: {summary}** (#{pr_number}){tag}\n" + ) + + path = pathlib.Path("CHANGELOG.md") + text = path.read_text() + lines = text.splitlines(keepends=True) + + # Locate `## Unreleased` block and the `### Changed` subsection + # within it. If `### Changed` is absent, create it right after + # `## Unreleased`. + unreleased_idx = None + for i, line in enumerate(lines): + if line.strip() == "## Unreleased": + unreleased_idx = i + break + if unreleased_idx is None: + # Insert a fresh `## Unreleased` section after the title. + # CHANGELOG.md starts with `# Changelog\n\n`; place the + # block between the title and whatever comes next. + insert_at = 0 + for i, line in enumerate(lines): + if line.startswith("# "): + insert_at = i + 1 + break + new_block = ["\n", "## Unreleased\n", "\n", "### Changed\n", "\n", entry] + lines = lines[:insert_at] + new_block + lines[insert_at:] + else: + # Search for `### Changed` between Unreleased and next `## ` heading. + changed_idx = None + end_idx = len(lines) + for j in range(unreleased_idx + 1, len(lines)): + if lines[j].startswith("## "): + end_idx = j + break + if lines[j].strip() == "### Changed": + changed_idx = j + break + if changed_idx is not None: + # Insert entry directly after the `### Changed` header + # (after the blank line that follows it, if present). + insert_at = changed_idx + 1 + if insert_at < end_idx and lines[insert_at].strip() == "": + insert_at += 1 + lines.insert(insert_at, entry) + else: + # `### Changed` doesn't exist within Unreleased — create it. + insert_at = unreleased_idx + 1 + if insert_at < end_idx and lines[insert_at].strip() == "": + insert_at += 1 + block = ["### Changed\n", "\n", entry, "\n"] + for k, ln in enumerate(block): + lines.insert(insert_at + k, ln) + + path.write_text("".join(lines)) + print(f"Inserted CHANGELOG entry: {entry.strip()}") + PY + + - name: Commit and push + if: steps.guard.outputs.skip != 'true' + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + if git diff --quiet CHANGELOG.md; then + echo "No changes to commit." + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add CHANGELOG.md + git commit -m "chore(changelog): record dep bumps from #${PR_NUMBER}" + git push diff --git a/CHANGELOG.md b/CHANGELOG.md index af79069..00fbe56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- **Dependabot PR hygiene: `.github/PULL_REQUEST_TEMPLATE.md` + auto-CHANGELOG workflow** (#TBD) — addresses the QA gap surfaced by the first Dependabot PR (#55) where the Dependabot-generated body had no `## QA` section and the auto-bump didn't produce the unconditional CHANGELOG entry that `CLAUDE.md` § "Adding a CHANGELOG entry on every PR" requires. Two pieces: (1) `PULL_REQUEST_TEMPLATE.md` provides a `## Summary` / `## Test plan` / `## CHANGELOG` scaffold so human-authored PRs inherit the QA checklist GitHub auto-fills it on PR open. Dependabot bypasses the template (it supplies its own body), so the second piece picks up the slack. (2) `.github/workflows/dependabot-changelog.yml` runs on `pull_request_target` for Dependabot-authored PRs only, calls `dependabot/fetch-metadata@v2` to enumerate the bump set, composes a one-line `### Changed` entry under `## Unreleased`, and pushes a follow-up commit (`chore(changelog): record dep bumps from #N`) back to the Dependabot branch. Loop guard skips when the latest commit author is `github-actions[bot]`; idempotency check skips when `(#N)` is already present in CHANGELOG.md. + ### Fixed - **Harden `pr-labels-ci.yml` against shell injection via fork-PR branch names** (#53) — closes #52. Cascades the `env:` pattern from `cmeans/mcp-clipboard#88` into this repo's `.github/workflows/pr-labels-ci.yml`. Both the `on-ci-pass` and `on-ci-fail` jobs previously inlined `${{ github.event.workflow_run.head_branch }}` directly inside `run:` blocks. `head_branch` is contributor-controlled on fork PRs and git refnames allow shell metacharacters (`$`, backtick, `;`, `&`, `|`, etc.), so a malicious fork branch name would render as directly-executed shell once the expression was substituted. `REPO`, `RUN_ID`, and `HEAD_BRANCH` now come through step-level `env:` blocks and the shell references them as `$REPO` / `$RUN_ID` / `$HEAD_BRANCH`. Also avoids the latent parser trap documented in `cmeans/yt-dont-recommend#28`: GHA substitutes `${{ ... }}` inside `run:` blocks before the shell sees them *including within shell comments*, and the queue-time parser rejects an empty expression on `workflow_dispatch` — the explanatory comments therefore describe the concept ("not a direct GHA expression") rather than showing the literal sequence. Verified locally that `yaml.safe_load` parses cleanly and that no `${{ ... }}` substitution survives inside any `run:` body. From 2c8729f6a12084acc137944eae8b4f4788d20afa Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <272174644+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 14:00:53 -0500 Subject: [PATCH 2/4] chore: pin CHANGELOG entry to #58 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00fbe56..6dedc53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- **Dependabot PR hygiene: `.github/PULL_REQUEST_TEMPLATE.md` + auto-CHANGELOG workflow** (#TBD) — addresses the QA gap surfaced by the first Dependabot PR (#55) where the Dependabot-generated body had no `## QA` section and the auto-bump didn't produce the unconditional CHANGELOG entry that `CLAUDE.md` § "Adding a CHANGELOG entry on every PR" requires. Two pieces: (1) `PULL_REQUEST_TEMPLATE.md` provides a `## Summary` / `## Test plan` / `## CHANGELOG` scaffold so human-authored PRs inherit the QA checklist GitHub auto-fills it on PR open. Dependabot bypasses the template (it supplies its own body), so the second piece picks up the slack. (2) `.github/workflows/dependabot-changelog.yml` runs on `pull_request_target` for Dependabot-authored PRs only, calls `dependabot/fetch-metadata@v2` to enumerate the bump set, composes a one-line `### Changed` entry under `## Unreleased`, and pushes a follow-up commit (`chore(changelog): record dep bumps from #N`) back to the Dependabot branch. Loop guard skips when the latest commit author is `github-actions[bot]`; idempotency check skips when `(#N)` is already present in CHANGELOG.md. +- **Dependabot PR hygiene: `.github/PULL_REQUEST_TEMPLATE.md` + auto-CHANGELOG workflow** (#58) — addresses the QA gap surfaced by the first Dependabot PR (#55) where the Dependabot-generated body had no `## QA` section and the auto-bump didn't produce the unconditional CHANGELOG entry that `CLAUDE.md` § "Adding a CHANGELOG entry on every PR" requires. Two pieces: (1) `PULL_REQUEST_TEMPLATE.md` provides a `## Summary` / `## Test plan` / `## CHANGELOG` scaffold so human-authored PRs inherit the QA checklist GitHub auto-fills it on PR open. Dependabot bypasses the template (it supplies its own body), so the second piece picks up the slack. (2) `.github/workflows/dependabot-changelog.yml` runs on `pull_request_target` for Dependabot-authored PRs only, calls `dependabot/fetch-metadata@v2` to enumerate the bump set, composes a one-line `### Changed` entry under `## Unreleased`, and pushes a follow-up commit (`chore(changelog): record dep bumps from #N`) back to the Dependabot branch. Loop guard skips when the latest commit author is `github-actions[bot]`; idempotency check skips when `(#N)` is already present in CHANGELOG.md. ### Fixed From 8f1f0ae00524dc680df5418cc94f7b06c7afef9e Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <272174644+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 14:26:46 -0500 Subject: [PATCH 3/4] fix(workflow): switch dependabot-changelog to App-token push (round 2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses QA round-1 findings on PR #58. Finding 1 (BLOCKER) — `secrets.GITHUB_TOKEN`-authored pushes don't trigger downstream `pull_request` workflows (GitHub anti-loop policy). On a Dependabot PR, the workflow's CHANGELOG commit would land on the branch but `lint` / `typecheck` / `test (3.11/3.12/3.13)` / `version-sync` would not re-run on the new HEAD SHA. The main-branch ruleset requires those checks → PR blocks at merge. Fix: mint a token from the `cmeans-claude-dev` GitHub App via `actions/create-github-app-token@v3.1.1` and use it for both `actions/checkout` and the push. App-token pushes fire `pull_request` workflows normally. Adds two repo-secrets prereqs: - `BOT_APP_ID` (numeric App ID) - `BOT_APP_PRIVATE_KEY` (PEM contents) PR body documents the operator action. Loop guard updated to match `cmeans-claude-dev[bot]` instead of `github-actions[bot]` since the bot identity changes with the auth swap. Commit author/committer aligns to the bot via the numeric-user-id noreply format (`+cmeans-claude-dev[bot]@users.noreply. github.com`) per the convention captured in `claude-dev/github-app/config` — the comment there explains why APP_ID does NOT work in this position (commits fail to resolve back to the bot user, breaking `require_last_push_approval` rulesets). Observation 3 — `package-ecosystem` is the ecosystem identifier (`pip`, `github-actions`), not the named group from `dependabot.yml`. PR #55's title used "uv" (the configured group name); the v1 workflow would have rendered "pip group" instead. Fix: read `steps.meta.outputs.dependency-group` first, fall back to `package-ecosystem`. Output now matches the PR title. Observation 4 — SHA-pin third-party actions on this high-privilege workflow: - `actions/create-github-app-token@1b10c78c…` v3.1.1 - `dependabot/fetch-metadata@21025c705c…` v2.5.0 --- .github/workflows/dependabot-changelog.yml | 58 ++++++++++++++++------ 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/.github/workflows/dependabot-changelog.yml b/.github/workflows/dependabot-changelog.yml index d7b4129..e9397e2 100644 --- a/.github/workflows/dependabot-changelog.yml +++ b/.github/workflows/dependabot-changelog.yml @@ -8,8 +8,21 @@ name: Dependabot CHANGELOG # branch's context with write permissions (Dependabot's pull_request # event runs with read-only GITHUB_TOKEN). # +# IMPORTANT: pushes back via a GitHub App installation token rather +# than secrets.GITHUB_TOKEN. Pushes authenticated with GITHUB_TOKEN +# do NOT trigger downstream `pull_request` workflows (GitHub's +# anti-loop policy), which would leave required status checks +# (lint, typecheck, test, version-sync) unsatisfied on the bot's +# follow-up commit and block merge under the repo's main-branch +# ruleset. App-token-authored pushes do trigger those workflows +# normally. +# +# Required repo secrets (operator must configure once): +# BOT_APP_ID — GitHub App ID (numeric) +# BOT_APP_PRIVATE_KEY — PEM contents of the App's private key +# # Loop guard: skips if the latest commit on the head branch was -# authored by `github-actions[bot]` — i.e. our own previous run. +# authored by the bot itself — i.e. our own previous run. # # Idempotency: skips if a CHANGELOG entry referencing the PR number # already exists. @@ -19,7 +32,7 @@ on: types: [opened, synchronize, reopened] permissions: - contents: write + contents: read pull-requests: read jobs: @@ -27,12 +40,19 @@ jobs: if: github.event.pull_request.user.login == 'dependabot[bot]' runs-on: ubuntu-latest steps: + - name: Mint GitHub App installation token + id: app-token + uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 + with: + app-id: ${{ secrets.BOT_APP_ID }} + private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} + - name: Checkout PR branch uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.app-token.outputs.token }} fetch-depth: 2 - name: Loop guard — skip if last commit was ours @@ -42,8 +62,8 @@ jobs: run: | LAST_AUTHOR=$(git log -1 --pretty=%an) echo "last_author=$LAST_AUTHOR" >> "$GITHUB_OUTPUT" - if [ "$LAST_AUTHOR" = "github-actions[bot]" ]; then - echo "Last commit authored by github-actions[bot]; skipping to avoid loop." + if [ "$LAST_AUTHOR" = "cmeans-claude-dev[bot]" ]; then + echo "Last commit authored by the bot; skipping to avoid loop." echo "skip=true" >> "$GITHUB_OUTPUT" elif grep -qE "\(#${PR_NUMBER}\)" CHANGELOG.md; then echo "CHANGELOG.md already references (#${PR_NUMBER}); skipping." @@ -55,9 +75,9 @@ jobs: - name: Fetch Dependabot metadata if: steps.guard.outputs.skip != 'true' id: meta - uses: dependabot/fetch-metadata@v2 + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ steps.app-token.outputs.token }} - name: Compose and prepend CHANGELOG entry if: steps.guard.outputs.skip != 'true' @@ -65,6 +85,7 @@ jobs: PR_NUMBER: ${{ github.event.pull_request.number }} PR_TITLE: ${{ github.event.pull_request.title }} DEPS_JSON: ${{ steps.meta.outputs.updated-dependencies-json }} + DEPENDENCY_GROUP: ${{ steps.meta.outputs.dependency-group }} ECOSYSTEM: ${{ steps.meta.outputs.package-ecosystem }} UPDATE_TYPE: ${{ steps.meta.outputs.update-type }} run: | @@ -75,7 +96,12 @@ jobs: pr_number = os.environ["PR_NUMBER"] deps_json = os.environ.get("DEPS_JSON") or "[]" - ecosystem = os.environ.get("ECOSYSTEM") or "deps" + # Prefer the named Dependabot group (matches PR title: + # "Bump the group across N directories with M updates"). + # Fall back to the ecosystem identifier (pip / github-actions / ...). + group = (os.environ.get("DEPENDENCY_GROUP") or "").strip() + ecosystem = (os.environ.get("ECOSYSTEM") or "deps").strip() + label = group if group else ecosystem update_type = os.environ.get("UPDATE_TYPE") or "version-update" deps = json.loads(deps_json) @@ -97,7 +123,7 @@ jobs: tag = " — picks up upstream security advisory; see PR body for CVE links." entry = ( - f"- **Bump {ecosystem} group: {summary}** (#{pr_number}){tag}\n" + f"- **Bump {label} group: {summary}** (#{pr_number}){tag}\n" ) path = pathlib.Path("CHANGELOG.md") @@ -114,8 +140,6 @@ jobs: break if unreleased_idx is None: # Insert a fresh `## Unreleased` section after the title. - # CHANGELOG.md starts with `# Changelog\n\n`; place the - # block between the title and whatever comes next. insert_at = 0 for i, line in enumerate(lines): if line.startswith("# "): @@ -154,17 +178,23 @@ jobs: print(f"Inserted CHANGELOG entry: {entry.strip()}") PY - - name: Commit and push + - name: Commit and push (via App token so CI re-fires) if: steps.guard.outputs.skip != 'true' env: PR_NUMBER: ${{ github.event.pull_request.number }} + GH_BOT_USER_ID: '272174644' run: | if git diff --quiet CHANGELOG.md; then echo "No changes to commit." exit 0 fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + # Commit author/committer aligns with the App's bot account so + # commits are attributed to cmeans-claude-dev[bot]. The numeric + # user id is required for GitHub's noreply-email format to + # resolve back to the bot account; otherwise commits show up + # under the App ID rather than the bot user. + git config user.name "cmeans-claude-dev[bot]" + git config user.email "${GH_BOT_USER_ID}+cmeans-claude-dev[bot]@users.noreply.github.com" git add CHANGELOG.md git commit -m "chore(changelog): record dep bumps from #${PR_NUMBER}" git push From 851a44f76ed74a4f1847db1a213d826708b7ae77 Mon Sep 17 00:00:00 2001 From: "cmeans-claude-dev[bot]" <272174644+cmeans-claude-dev[bot]@users.noreply.github.com> Date: Sun, 26 Apr 2026 14:59:01 -0500 Subject: [PATCH 4/4] fix(workflow): close round-2 QA findings on PR #58 F1-r2 (substantive): CHANGELOG entry rewritten to reflect the round-2 implementation (App-token auth via actions/create-github-app-token@v3.1.1, dependency-group with package-ecosystem fallback, dependabot/fetch-metadata@v2.5.0, loop-guard against cmeans-claude-dev[bot], idempotency on (#N), required BOT_APP_ID + BOT_APP_PRIVATE_KEY secrets, preservation of hand-prepended human CHANGELOG entries). Previous wording described round 1 (@v2 floating tag, github-actions[bot] guard, no App-token mention) and would have shipped an inaccurate description into release notes. O2-r2 (observation): SHA-pin actions/checkout to 34e114876b0b11c390a56381ad16ebd13914f8d5 (v4.3.1) for consistency with the two third-party actions already SHA-pinned in this workflow. All three third-party actions on this high-privilege pull_request_target trigger are now pinned by SHA. PR body restoration of the ## Test plan section (O3-r2) is applied via gh pr edit, not via this commit. --- .github/workflows/dependabot-changelog.yml | 2 +- CHANGELOG.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependabot-changelog.yml b/.github/workflows/dependabot-changelog.yml index e9397e2..e5d6dda 100644 --- a/.github/workflows/dependabot-changelog.yml +++ b/.github/workflows/dependabot-changelog.yml @@ -48,7 +48,7 @@ jobs: private-key: ${{ secrets.BOT_APP_PRIVATE_KEY }} - name: Checkout PR branch - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 with: ref: ${{ github.event.pull_request.head.ref }} repository: ${{ github.event.pull_request.head.repo.full_name }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dedc53..cd20acc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- **Dependabot PR hygiene: `.github/PULL_REQUEST_TEMPLATE.md` + auto-CHANGELOG workflow** (#58) — addresses the QA gap surfaced by the first Dependabot PR (#55) where the Dependabot-generated body had no `## QA` section and the auto-bump didn't produce the unconditional CHANGELOG entry that `CLAUDE.md` § "Adding a CHANGELOG entry on every PR" requires. Two pieces: (1) `PULL_REQUEST_TEMPLATE.md` provides a `## Summary` / `## Test plan` / `## CHANGELOG` scaffold so human-authored PRs inherit the QA checklist GitHub auto-fills it on PR open. Dependabot bypasses the template (it supplies its own body), so the second piece picks up the slack. (2) `.github/workflows/dependabot-changelog.yml` runs on `pull_request_target` for Dependabot-authored PRs only, calls `dependabot/fetch-metadata@v2` to enumerate the bump set, composes a one-line `### Changed` entry under `## Unreleased`, and pushes a follow-up commit (`chore(changelog): record dep bumps from #N`) back to the Dependabot branch. Loop guard skips when the latest commit author is `github-actions[bot]`; idempotency check skips when `(#N)` is already present in CHANGELOG.md. +- **Dependabot PR hygiene: `.github/PULL_REQUEST_TEMPLATE.md` + auto-CHANGELOG workflow** (#58) — addresses the QA gap surfaced by the first Dependabot PR (#55) where the Dependabot-generated body had no `## QA` section and the auto-bump didn't produce the unconditional CHANGELOG entry that `CLAUDE.md` § "Adding a CHANGELOG entry on every PR" requires. Two artifacts: (1) `PULL_REQUEST_TEMPLATE.md` providing a `## Summary` / `## Test plan` / `## CHANGELOG` scaffold for human-authored PRs (Dependabot bypasses templates). (2) `.github/workflows/dependabot-changelog.yml` running on `pull_request_target` for `dependabot[bot]`-authored PRs only — mints a token via `actions/create-github-app-token` (SHA-pinned to v3.1.1) so pushes attribute to `cmeans-claude-dev[bot]` and re-fire the required `pull_request` checks (lint / typecheck / test 3.11/3.12/3.13 / version-sync), enumerates the bump set via `dependabot/fetch-metadata` (SHA-pinned to v2.5.0), prefers the named `dependency-group` output and falls back to `package-ecosystem`, and pushes a follow-up commit (`chore(changelog): record dep bumps from #N`) back to the Dependabot branch. Required repo secrets: `BOT_APP_ID`, `BOT_APP_PRIVATE_KEY`. Loop guard skips when the last commit author is `cmeans-claude-dev[bot]`; idempotency check skips when `(#N)` is already present in `CHANGELOG.md` so `@dependabot recreate`/`rebase` doesn't double-write and a hand-prepended human entry (e.g., the CVE callouts in PR #55) is preserved. ### Fixed