Standardize bots on GitHub App token, align merge methods, version PyPI via NBGV - #70
Conversation
Mirrors the pattern from homeassistant-purpleair where we discovered `gh pr merge --squash` against `main` would fail enablePullRequestAutoMerge once a stricter ruleset is in place: the develop ruleset allows only squash, the main ruleset allows only merge commits. Workflow changes - merge-bot-pull-request.yml `merge-dependabot`: switch to App token (`actions/create-github-app-token`) so the resulting merge push fires `publish-release.yml` on develop (GITHUB_TOKEN-driven pushes are blocked by GitHub's recursion guard). Replace the unconditional `--squash` with a `case` on `pull_request.base.ref` that picks `--squash` for develop and `--merge` for main, with an explicit failure for unknown bases. - merge-bot-pull-request.yml `merge-codegen` and `merge-codegen-app`: both target `main` exclusively, so switch `--squash` → `--merge` to match the main ruleset. Stay on GITHUB_TOKEN — these merge routine generated-content updates and deliberately should not fire a release on every codegen tick. - merge-bot-pull-request.yml header comment: rewritten to describe the current token strategy (App token vs GITHUB_TOKEN per job) and the merge-method dispatch rule. Dependabot routing - dependabot.yml: switch `target-branch: "main"` → `"develop"` on all three ecosystem entries (nuget, github-actions, uv). Scheduled bumps now land on develop first, then bundle into the next develop → main merge-commit alongside feature work — so develop cannot fall behind main. Security update PRs from Dependabot still open against `main` directly (Dependabot doesn't honor `target-branch` for those); the new `case` statement in the merge-bot handles either base. Documentation - AGENTS.md "Branching Model": new bullet describing Dependabot target routing and the merge-bot's base-aware method dispatch. - .github/copilot-instructions.md "Commit Messages and Pull Request Titles": short paragraph about picking `--squash` vs `--merge` when running `gh pr merge` so AI agents asked to merge a PR pick the form that matches the base branch's ruleset. - PyPiLibrary/README.md "Publishing": expanded the first-time PyPI Trusted Publishing setup with the lessons from today's release session — 2FA prerequisite, the pending-publisher vs add-new-publisher distinction, the mandatory `pypi` environment deployment branch rule for `main`, optional required reviewer, troubleshooting for both `invalid-publisher` and the `manifest unknown` Docker pull failure we hit before today's release, and an API-token fallback recipe. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates the repo’s automation and documentation to respect branch-specific merge rules (squash-only on develop, merge-commit-only on main) and to route scheduled Dependabot updates to develop so dependency bumps flow through the integration branch before being merged to main.
Changes:
- Make the merge-bot choose
gh pr merge --squashvs--mergebased on the PR base branch; switch codegen merges to--mergeformain. - Retarget scheduled Dependabot updates (
nuget,github-actions,uv) frommaintodevelop. - Document the updated branching/merge-bot behavior and expand PyPI Trusted Publishing setup guidance.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
PyPiLibrary/README.md | Expands PyPI Trusted Publishing setup/troubleshooting guidance. |
AGENTS.md | Adds a branching-model bullet documenting Dependabot targeting develop and merge-bot base-aware merge method dispatch. |
.github/workflows/merge-bot-pull-request.yml | Updates merge-bot token strategy and selects merge method based on PR base branch. |
.github/dependabot.yml | Routes scheduled Dependabot PRs to develop via target-branch. |
.github/copilot-instructions.md | Documents the required gh pr merge flag selection by base branch and points to merge-bot as the reference pattern. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
… App token Two follow-ups on this branch: NBGV-driven PyPI version - build-pypilibrary-task.yml: add an inner `get-version` job (mirrors `build-nugetlibrary-task.yml`) and a new "Write version into _version.py step" that overwrites `_version.py` with NBGV's `AssemblyFileVersion` (Major.Minor.Patch.BuildNumber — always numeric, PEP 440 valid) just before `uv build`. Wheel and sdist carry the rewritten version, so the PyPI upload matches the NuGet/Docker/executable artifacts for the same release commit. - PyPiLibrary/src/.../_version.py: docstring expanded to explain the `0.0.0` placeholder is a local-development convenience and that CI rewrites the file from NBGV. - PyPiLibrary/README.md: stack table now lists Version alongside the other tooling; "Template Adoption" section now treats NBGV as the default and frames hatch-vcs / manual as alternatives. Consolidate codegen and merge-bot on the GitHub App token - Delete `run-codegen-app-pull-request-task.yml` and `run-periodic-codegen-app-pull-request.yml`. The App-based variant is now the only codegen path. - Rewrite `run-codegen-pull-request-task.yml` with App-token logic (formerly only in the deleted *app* variant). No PAT, no close / reopen dance — App tokens trigger `pull_request` workflow events directly when opening a PR. - `run-periodic-codegen-pull-request.yml`: drop the Monday/Thursday alternation note; weekly Monday cron + `workflow_dispatch` remain. - `merge-bot-pull-request.yml`: delete the `merge-codegen-app` job and rewrite `merge-codegen` to use the App token, match `ptr727-codegen[bot]`, and skip the legacy `(reopened & owner) || (not reopened & github-actions[bot])` actor dance. Both jobs in the file now use App tokens; header comment rewritten to reflect the new single-strategy model. - README.md "Template - GitHub Setup": drop the WORKFLOW_PAT section, make the App setup required (was framed as an alternative), and collapse the dual-codegen-schedule section into a single weekly entry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…f-gate Two fixes for unresolved Copilot threads on the latest head (604fd27): 1. Dependabot can't see Actions secrets (`merge-bot-pull-request.yml:28`). Dependabot-triggered `pull_request` workflows run with a restricted secret context — `secrets.CODEGEN_APP_ID` and `CODEGEN_APP_PRIVATE_KEY` stored only in the Actions store aren't visible. README updated to instruct saving the App credentials in **both** the Actions and the Dependabot secret stores. No workflow change needed. 2. `publish-pypi` would stall on develop pushes (`PyPiLibrary/README.md:61` thread). `publish-release.yml` triggers on push to both `main` and `develop`. The `pypi` environment now restricts deployments to `main` only, so on a develop push the `publish-pypi` job would queue against an env it can't enter — visible as a stalled / blocked deployment on every develop release. Add `if: github.ref == 'refs/heads/main'` so the job is cleanly skipped on develop and the env rule remains as the security boundary. PyPiLibrary/README.md updated to explain the two-layer restriction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Copilot review on 6e5b400 caught that the docstring in _version.py and the "Template Adoption" paragraph in PyPiLibrary/README.md both claimed PyPI artifacts share the version of "matching NuGet, Docker, and executable artifacts" — that overstates the parity. PyPI now uses NBGV's `AssemblyFileVersion` (PEP 440 valid, Major.Minor.Patch.Build), which matches the .NET assemblies' `FileVersion` / `AssemblyVersion` stamp, but NuGet packages and Docker tags use NBGV's `SemVer2` (with prerelease and build-metadata suffixes that PEP 440 doesn't accept). Reword in three places (_version.py docstring, the Stack table entry, and the Template Adoption paragraph) to spell out: - PyPI string == .NET FileVersion/AssemblyVersion string (byte-equal). - PyPI string != NuGet PackageVersion or Docker tag (different format). - All four are derived from the same NBGV computation against version.json + git history, so they correspond to the same release commit even when the strings differ. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Third Copilot pass on PR #70 caught one more occurrence of the overstated parity claim — this time in the inline comment above the "Write version into _version.py step" in `build-pypilibrary-task.yml`. Same fix as 55c9d86 applied here: PyPI equals .NET `FileVersion` / `AssemblyVersion` (both = NBGV `AssemblyFileVersion`); NuGet `PackageVersion` and Docker tags use NBGV `SemVer2` (different format, not byte-identical); all four still derive from the same NBGV computation per release commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Per AGENTS.md workflow conventions, third-party actions used in workflows being modified should be pinned to a commit SHA with a trailing `# vX.Y.Z` comment. Two pins applied here: - merge-bot-pull-request.yml: dependabot/fetch-metadata@v2 → 21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 (same commit as the `v2` floating tag currently resolves to). - run-codegen-pull-request-task.yml: peter-evans/create-pull-request@v8 → 5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 (same commit as the `v8` floating tag currently resolves to). The other third-party action flagged by Copilot — dotnet/nbgv@master in get-version-task.yml — is declined here because AGENTS.md scopes opportunistic pinning to workflows "being touched for other reasons" and get-version-task.yml is unmodified in this PR. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
…place Copilot review on c8a38ef caught two issues: (1) AssemblyVersion != AssemblyFileVersion. NBGV produces both as separate outputs and the build workflows wire them to distinct .NET properties (Version / AssemblyVersion = NBGV AssemblyVersion; FileVersion = NBGV AssemblyFileVersion). My earlier wording claimed PyPI matches both FileVersion *and* AssemblyVersion — that's only half-true. Rewording in four places (_version.py docstring, README "Version" stack entry, README "Template Adoption" section, workflow comment) to limit the parity claim to FileVersion only. (2) `printf > _version.py` was truncating the file, so the published wheel would have lost the module docstring and any future metadata stored in `_version.py`. Switched the "Write version into _version.py step" to `sed -i` so only the `__version__` line is rewritten; the rest of the module is preserved into the build artifact. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary One-character US-English typo fix in [.github/workflows/merge-bot-pull-request.yml](.github/workflows/merge-bot-pull-request.yml) (the new dependabot-merge step comment landed in PR #70): ```diff - # runtime behaviour, so they should land via human review. Other + # runtime behavior, so they should land via human review. Other ``` Flagged by Copilot review on [PR #71](#71) (the develop → main release). AGENTS.md "Documentation Style Conventions" requires US English. ## Test plan - [ ] CI passes on this PR. - [ ] After merge to develop, PR #71's diff absorbs the fix and the Copilot thread on #71 can be resolved. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…#73) ## Summary Two additional Copilot findings on [PR #71](#71) (develop → main release) that PR #72 was supposed to carry alongside the `behaviour → behavior` fix. Auto-merge on PR #72 fired before the second commit on its branch landed, so PR #72's squash captured only the spelling fix — these two land here separately. ### 1. `secrets: inherit` removed from `get-version` job in `build-pypilibrary-task.yml` The job calls [`get-version-task.yml`](.github/workflows/get-version-task.yml) which declares no required secrets. `secrets: inherit` was widening the secret blast radius for no benefit. (Same pattern exists in `build-nugetlibrary-task.yml`, untouched here per AGENTS.md "Workflow YAML Conventions" — *"existing workflows are migrated opportunistically when they're being touched for other reasons"*. Easy follow-up PR later.) ### 2. `merge-codegen` `if:` gate now requires App-actor too The current gate checks PR author/branch/base but not the event actor. A maintainer pushing extra commits to the App's `codegen` branch fires a `synchronize` event the job would happily auto-merge — folding human changes into a release through the App PR. Restored `github.actor == 'ptr727-codegen[bot]'` (which the pre-consolidation `merge-codegen-app` job carried in PR #70-era code) alongside the existing PR author check. Comment expanded to explain why both checks matter. ```diff + # Both the PR author AND the event actor must be the App: the author + # check stops human-opened PRs that happen to target the `codegen` + # branch from auto-merging; the actor check stops a maintainer + # pushing extra commits to the App's `codegen` branch (a + # `synchronize` event the human triggered) from auto-merging + # unintended changes through the App PR. if: >- github.event.pull_request.user.login == 'ptr727-codegen[bot]' && + github.actor == 'ptr727-codegen[bot]' && github.event.pull_request.head.ref == 'codegen' && github.event.pull_request.base.ref == 'main' && github.event.pull_request.head.repo.full_name == github.repository ``` ## Test plan - [ ] CI passes on this PR. - [ ] After merge to develop, PR #71's two remaining Copilot threads (lines 25 and 93) can be resolved. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…PI via NBGV (#71) Release merge: brings one squashed PR from develop into main. ## Squashed PR included - **#70 — Standardize bots on GitHub App token, align merge methods, version PyPI via NBGV.** A consolidation PR rolling up three threads that overlap (all touch merge-bot / codegen / PyPI release): 1. **All bot workflows on a single GitHub App token.** Deleted `run-codegen-app-pull-request-task.yml` and `run-periodic-codegen-app-pull-request.yml`. Rewrote `run-codegen-pull-request-task.yml` with App-token logic — no PAT, no close/reopen dance. Consolidated `merge-codegen-app` into `merge-codegen` in the merge-bot; both jobs in `merge-bot-pull-request.yml` now use the App token so the resulting push fires downstream workflows directly. 2. **Merge method per base branch.** `case` on `pull_request.base.ref` picks `--squash` for develop and `--merge` for main, with an explicit failure for unknown bases. Bot PRs now match each ruleset. 3. **Dependabot routes to develop.** All three ecosystems (nuget, github-actions, uv) set `target-branch: "develop"` so develop is the leading edge for dep bumps. Security update PRs still open against `main`; the merge-bot's `case` handles either base. 4. **PyPI versioning via NBGV.** Inner `get-version` job added to `build-pypilibrary-task.yml`; a new "Write version into _version.py step" uses `sed -i` to replace the `__version__` line with NBGV's `AssemblyFileVersion` (PEP 440 valid, `Major.Minor.Patch.BuildNumber`) before `uv build`. PyPI's version string therefore equals the .NET assemblies' `FileVersion` stamp (a separate NBGV output from `AssemblyVersion` and `SemVer2`). 5. **`publish-pypi` gated to `main` only.** Added `if: github.ref == 'refs/heads/main'` so develop pushes don't stall at the env gate (defense in depth alongside the `pypi` environment's deployment branch rule). 6. **Third-party action SHA pins** in workflows touched by this PR: `dependabot/fetch-metadata@v2` → `21025c70…# v2.5.0` and `peter-evans/create-pull-request@v8` → `5f6978fa…# v8.1.1`. `dotnet/nbgv@master` in `get-version-task.yml` was declined (out of scope per AGENTS.md opportunistic-pin rule). 7. **Docs** — AGENTS.md branching model now notes Dependabot routing + merge-bot base-aware dispatch; copilot-instructions.md adds a paragraph about picking `--squash` vs `--merge` based on base; PyPiLibrary/README.md "Publishing" section expanded with the lessons from the prior release (2FA prereq, pending-publisher vs add-new-publisher distinction, the mandatory `pypi` env deployment branch rule on `main`, troubleshooting for `invalid-publisher` and `manifest unknown`, API-token fallback recipe); README.md "Template - GitHub Setup" drops the WORKFLOW_PAT block and makes the App setup the single required path. ## Operator action items already completed (out-of-band) - `CODEGEN_APP_ID` and `CODEGEN_APP_PRIVATE_KEY` added to the **Dependabot** secret store (was already in Actions). Required because Dependabot-triggered runs don't see Actions secrets. - `WORKFLOW_PAT` secret revoked from the repo and the underlying PAT revoked on github.com. - The `pypi` GitHub environment has a Deployment branch rule restricting to `main` (added during the prior release session). ## Notes - Merge method: **merge-commit** (per [AGENTS.md branching model](https://github.com/ptr727/ProjectTemplate/blob/develop/AGENTS.md#branching-model)). Squash and rebase are blocked by the main ruleset. ## Test plan - [ ] CI passes on the merge commit. - [ ] Next codegen PR auto-merges to `main` as a merge-commit (not a squash) under the App identity. - [ ] Next Dependabot scheduled run opens its PRs against `develop`; the auto-merge succeeds and the resulting develop push fires `publish-release.yml`. - [ ] Next release on `main` ships PyPI at the same `Major.Minor.Patch.BuildNumber` as the .NET `FileVersion` (not `0.0.0`), and `publish-pypi` is cleanly skipped on develop pushes.
…erit (#76) Two small workflow follow-ups that have been outstanding through PR #70 and PR #74 reviews. ## 1. `get-version-task.yml` — carve out the `dotnet/nbgv@master` exception in comments `dotnet/nbgv` is the only third-party action in this repo that's not SHA-pinned. Copilot has flagged this twice now ([PR #70](#70) and again in passing). Adding an inline comment that explicitly documents it as the AGENTS.md exception so future reviewers / agents don't keep flagging it. Reasoning recorded in the comment: - Upstream tag stream is dormant — latest tag `v0.5.1` (`3cf2d96c…`) lags `master` (`b944774b…` as of today). Real fixes accumulate on `master` between tag bumps. - Dependabot's GitHub Actions ecosystem tracks tagged releases. If we pin to a post-`v0.5.1` `master` SHA, Dependabot will either silently stall (no new tags to bump to) or attempt a **downgrade** PR to `v0.5.1`. Neither beats just floating on `master`. - Upstream owner is Microsoft (`dotnet/`), so the tag-/branch-retargeting risk the rule guards against is materially lower than for a random-author action. ## 2. `build-nugetlibrary-task.yml` — drop redundant `secrets: inherit` `get-version-task.yml` declares no required secrets in its `on: workflow_call` block, so `secrets: inherit` on the `get-version` job call was passing the caller's secret context to a job that consumes none. Same fix [PR #74](#74) applied to `build-pypilibrary-task.yml`; flagged by Copilot there but declined here for opportunistic-pin scope. This PR closes the gap. ## Test plan - [ ] CI passes on this PR. - [ ] No behavior change on either workflow (NBGV invocation unchanged, version outputs identical, no secrets actually consumed by `get-version`). --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
A consolidation PR. Three threads land together because they overlap (all touch the merge-bot + codegen + PyPI release flow):
pull_request/pushevents directly (no recursion-guard skips), sopublish-release.ymlfires on bot-driven merges to develop and main exactly the same way it does on human merges.caseonpull_request.base.refso the form matches either base.AssemblyFileVersionand overwriting_version.pyin CI beforeuv build, so PyPI and NuGet ship at matching versions per release.Standardize on GitHub App token
Codegen
.github/workflows/run-codegen-app-pull-request-task.ymland.github/workflows/run-periodic-codegen-app-pull-request.yml..github/workflows/run-codegen-pull-request-task.ymlwith the App-token-based logic (formerly in the deleted app variant). No PAT, no close/reopen dance — App tokens triggerpull_requestworkflow events directly when opening a PR..github/workflows/run-periodic-codegen-pull-request.yml: drop the Monday/Thursday alternation note; the weekly Monday cron +workflow_dispatchremain.Merge bot
merge-codegen-appjob (the App-token-based duplicate ofmerge-codegen).merge-codegento use the App token, matchptr727-codegen[bot], and skip the legacy(reopened & owner) || (!reopened & github-actions[bot])actor dance that existed only becauseGITHUB_TOKEN-created PRs needed a close/reopen nudge.merge-dependabot— switched to App token (was already on the chopping block for the publish-release trigger problem). Replaces unconditional--squashwith acaseonpull_request.base.refthat picks--squashfor develop and--mergefor main; explicit failure for unknown bases.Align merge method per base branch
Bot PRs targeting
mainpreviously usedgh pr merge --auto --squash— fine while the main ruleset allowed squash, but fragile once main locks to merge-only (the purpleair experience). The dispatcher inmerge-bot-pull-request.ymlnow lifts the form frombase.ref:Route Dependabot to develop
.github/dependabot.yml:target-branch: "main"→"develop"on all three ecosystem entries (nuget,github-actions,uv). Scheduled bumps now land on develop first, then bundle into the next develop → main merge-commit alongside feature work. Security update PRs still open againstmaindirectly — Dependabot doesn't honortarget-branchfor those — but the newcasestatement in the merge-bot handles either base. Long comment in the file explains the trade-off and links the merge-bot.NBGV-driven PyPI version
.github/workflows/build-pypilibrary-task.yml— add an innerget-versionjob (mirroringbuild-nugetlibrary-task.yml) and a new "Write version into _version.py step" that overwrites_version.pywith NBGV'sAssemblyFileVersion(Major.Minor.Patch.BuildNumber— always numeric, PEP 440 valid) just beforeuv build. The wheel + sdist therefore carry the rewritten version, so the PyPI upload matches the NuGet, Docker, and executable artifacts for the same release commit.PyPiLibrary/src/ptr727_projecttemplate_library/_version.py— docstring expanded to explain that0.0.0is a local-development convenience and CI overwrites the file beforeuv build.PyPiLibrary/README.md— stack table lists Version alongside the rest of the tooling; "Template Adoption" section now frames NBGV as the default with hatch-vcs / manual as the documented forks.Documentation
AGENTS.md→ Branching Model — new bullet noting Dependabot'starget-branch: developrouting and the merge-bot's base-aware method dispatch..github/copilot-instructions.md→ Commit Messages and Pull Request Titles — short paragraph telling AI agents to pick--squashfor develop and--mergefor main when invokinggh pr merge.PyPiLibrary/README.md→ Publishing — expanded the first-time PyPI Trusted Publishing setup with everything we learned during today's release session (the 2FA prereq, pending-publisher vs add-new-publisher distinction, the mandatorypypienvironment deployment branch rule onmain, optional required reviewer, troubleshooting forinvalid-publisherandmanifest unknown, and an API-token fallback recipe).README.md→ Template - GitHub Setup — drop the WORKFLOW_PAT block, make App setup required (was an alternative), collapse the dual codegen schedule into one entry.Test plan
develop(currently opens againstmain).publish-release.yml.mainas a merge-commit (not a squash).mainpublishes the PyPI library at the sameM.N.P.Bversion as the NuGet package (no more0.0.0placeholder).