Publish PyPI on develop as PEP 440 dev releases (prerelease channel) - #74
Conversation
Mirror the NuGet / Docker / GitHub-release pattern where main pushes ship release versions and develop pushes ship prerelease versions — PyPI was the odd one out, only publishing on main. Workflow changes - build-pypilibrary-task.yml: new "Compute PyPI version step" formats the version from NBGV `AssemblyFileVersion` per branch ref: refs/heads/main -> `Major.Minor.Patch.BuildNumber` (PEP 440 release; what `pip install` picks by default). refs/heads/develop -> `Major.Minor.Patch.devBuildNumber` (PEP 440 dev release; filtered out by `pip` unless `--pre` is passed, matching NuGet beta tags / GitHub prerelease flag). other refs (PR validation via test-release-task, feature branches) -> `AssemblyFileVersion` as-is. These refs never publish; this is just so `uv build` accepts the version string during build validation. The "Write version into _version.py step" now consumes `steps.pypiver.outputs.version` instead of NBGV's `AssemblyFileVersion` directly. - publish-release.yml: remove the `if: github.ref == 'refs/heads/main'` guard on `publish-pypi`. The job now runs on every push to main and develop, just like the NuGet / Docker / executable publish jobs do. Comment rewritten to describe the dual-channel model and point at the env Deployment branch rule as the security boundary. Operator action (done out-of-band; documented in PyPiLibrary/README.md) - `pypi` GitHub environment's Deployment branch rule now allows both `main` and `develop`. Without this, develop pushes would be blocked at the env gate. Documentation - PyPiLibrary/README.md: stack-table "Version" entry expanded to describe the branch-aware version format. "Publishing" section gains a new "Two-channel publishing" paragraph explaining the `pip install <pkg>` vs `pip install --pre <pkg>` flow. First-time setup step 2 updated to list both `main` and `develop` in the env Deployment branch rule. - PyPiLibrary/src/.../_version.py docstring updated to describe both branches' PEP 440 forms. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates the PyPI publishing pipeline to publish from both main (release channel) and develop (prerelease/dev channel), aligning PyPI behavior with the repo’s existing NuGet/Docker/GitHub release model. It also updates documentation to describe the new two-channel publishing scheme and the branch-aware version formatting.
Changes:
- Add a “Compute PyPI version” step in the reusable PyPI build workflow and use its output when rewriting
_version.pybeforeuv build. - Enable the
publish-pypijob to run on bothmainanddeveloppushes (relying on thepypienvironment branch gate for protection). - Update PyPiLibrary docs (
README.mdand_version.pydocstring) to describe the dual-channel publishing/versioning behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
PyPiLibrary/src/ptr727_projecttemplate_library/_version.py | Docstring updated to describe branch-aware PEP 440 versioning. |
PyPiLibrary/README.md | Documentation updated for two-channel publishing and branch-aware versions. |
.github/workflows/publish-release.yml | Removes main-only job gating; comments updated to describe dual-channel publishing. |
.github/workflows/build-pypilibrary-task.yml | Adds computed PyPI version output and uses it when rewriting _version.py. |
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.
Copilot review on PR #74 caught a real ordering bug across four locations: the develop version `Major.Minor.Patch.devBuildNumber` won't behave as the docs claim because PEP 440 compares release segments first. `1.0.47.devX` has release `[1,0,47]`, while main's `1.0.47.53309` has release `[1,0,47,53309]` — main wins on the release-segment comparison, so `pip install --pre <pkg>` still resolves to main, never to the develop dev build. Fix: keep the BuildNumber in the release segment and put the dev marker after it as `.dev0`: main: Major.Minor.Patch.BuildNumber (e.g., 1.0.47.53309) develop: Major.Minor.Patch.BuildNumber.dev0 (e.g., 1.0.47.53400.dev0) Develop's BuildNumber grows past main's after every new commit on develop, so develop's release segment `[1,0,47,53400]` compares higher than main's `[1,0,47,53309]`, and the `.dev0` suffix marks develop as a prerelease. Result: - `pip install <pkg>` → main (dev filtered out by default) - `pip install --pre <pkg>` → develop (highest release segment, dev) Each develop push has a unique BuildNumber (NBGV increments it per commit), so a constant `.dev0` is enough to disambiguate uploads — no second counter needed in the dev segment. Edge case documented: in the window between a release merge to main and the next commit on develop, develop's BuildNumber equals main's (or is one lower), so `--pre` will still resolve to the main release until a new develop commit lands. Self-healing. Updates in four places: - .github/workflows/build-pypilibrary-task.yml: version computation now produces `${AFV}.dev0` on develop instead of splitting AFV and rebuilding with `.dev${build}`. Comment block rewritten to spell out the PEP 440 ordering argument and the edge case. - .github/workflows/publish-release.yml: publish-pypi comment rewritten to match. - PyPiLibrary/README.md: stack-table "Version" entry, Publishing section's "Two-channel publishing" paragraph, and Template Adoption section all rewritten. - PyPiLibrary/src/.../_version.py docstring rewritten to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
The "Write version into _version.py step" comment block still referenced the discarded `M.N.P.devB` scheme that f475ed8 replaced with `M.N.P.B.dev0`. Update the prose to match the actual workflow behaviour. No code change; comment only. 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 caught that _version.py's docstring claims `pip install --pre` always prefers develop over main, but doesn't note the post-release window where develop's BuildNumber can be equal to or lower than main's. Add the edge-case note here for consistency with the workflow comment and PyPiLibrary/README.md, which both already document it. 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.
Copilot review on afc3949 caught that the docs in three places still say all four artifact families correspond to the same NBGV "release commit", but the two-channel publishing model means develop publishes artifacts for ordinary (non-release) commits too. Rewording: - PyPiLibrary/README.md: both occurrences ("Version" stack-table entry and "Template Adoption" section) now say "per commit". - PyPiLibrary/src/.../_version.py docstring: rewrites the closing sentence to say "the same commit (main pushes publish release versions; develop pushes publish PEP 440 dev releases / NBGV prereleases)" so the distinction is explicit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Final occurrence of "per release commit" that I missed in 92d20b0. Same fix: reword to "per commit (main pushes publish release versions; develop pushes publish PEP 440 dev releases / NBGV prereleases)" to match the README and _version.py docstring. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
…75) Release merge: brings one squashed PR from develop into main, plus a routine main → develop back-merge that catches develop up to main's last release commit. ## Squashed PR included - **#74 — Publish PyPI on develop as PEP 440 dev releases (prerelease channel).** Aligns PyPI publish behavior with the existing NuGet / Docker / GitHub-release pattern — develop pushes now publish a PEP 440 dev release alongside the main stable release on a single PyPI project. ### What changed - `publish-pypi` job in `publish-release.yml` no longer has the `if: github.ref == 'refs/heads/main'` guard — runs on both `main` and `develop` pushes. - New "Compute PyPI version step" in `build-pypilibrary-task.yml` formats the version per branch: - `main` → `Major.Minor.Patch.BuildNumber` (PEP 440 release) - `develop` → `Major.Minor.Patch.BuildNumber.dev0` (PEP 440 dev release) - The BuildNumber stays in the release segment on develop so develop's release segments climb past main's per commit, giving `pip install --pre <pkg>` the correct ordering to resolve to develop. - Docs updated (PyPiLibrary/README.md "Two-channel publishing" + "Version" stack entry; `_version.py` docstring; inline workflow comments). - Through five rounds of Copilot review on the source PR. ## Back-merge included - **`ffb9e64` — Merge remote-tracking branch 'origin/main' into develop.** Per AGENTS.md release flow, brings main's last release merge commit (`d03d810`) into develop's history so develop is no longer "BEHIND" main. No content change; the second parent reference is what matters. Also bumps develop's NBGV gitHeight by 1, which activates the PyPI `--pre` channel from PR #74 (the back-merge commit publishes `1.0.48.<B>.dev0`, which is unambiguously higher than main's `1.0.47.53309`). ## Operator action items (already completed out-of-band) - `CODEGEN_APP_ID` / `CODEGEN_APP_PRIVATE_KEY` already in both Actions and Dependabot secret stores. - `WORKFLOW_PAT` secret + PAT revoked. - `pypi` GitHub environment Deployment branch rule already allows both `main` and `develop`. ## Notes - Merge method: **merge-commit** (per [AGENTS.md branching model](https://github.com/ptr727/ProjectTemplate/blob/develop/AGENTS.md#branching-model)). ## Test plan - [ ] CI passes on the merge commit. - [ ] After merge, the publish-release run on main publishes `1.0.48.<B>` as the next stable release (Patch bumped from 47 to 48 because of the back-merge contributing one to gitHeight). - [ ] `pip install ptr727-projecttemplate-library` (no `--pre`) still resolves to a release version (`1.0.48.<B>` after this merge, was `1.0.47.53309` before). - [ ] `pip install --pre ptr727-projecttemplate-library` resolves to the latest develop dev release (`1.0.48.<B'>.dev0` from develop's tip, where `B' >= B`).
…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>
…nups (#77) Release merge: brings two squashed PRs and one unnecessary back-merge commit from `develop` into `main`. ## Squashed PRs included - **#76 — Document `dotnet/nbgv@master` exception and drop redundant `secrets: inherit`.** - `.github/workflows/get-version-task.yml`: inline comment carves out `dotnet/nbgv@master` as the deliberate deviation from the AGENTS.md SHA-pinning rule (upstream tag stream is dormant, Dependabot would stall or attempt a downgrade if we pinned, upstream owner is Microsoft so retargeting risk is low). - `.github/workflows/build-nugetlibrary-task.yml`: dropped `secrets: inherit` from the `get-version` job call (same fix PR #74 applied to `build-pypilibrary-task.yml`). - **#78 — Forward-only develop with dual-target Dependabot + codegen.** The substantive change in this release: - **AGENTS.md "Branching Model"** explicitly codifies forward-only develop (no `main → develop` back-merges; develop squash-only ruleset blocks them) and the dual-target bot model with rationale. - **`.github/dependabot.yml`** duplicates every ecosystem entry per branch (six entries total) so Dependabot opens parallel PRs against both `main` and `develop` independently. Both branches stay current on dep versions without back-merges. - **`.github/workflows/run-codegen-pull-request-task.yml`** runs as a matrix over `main` and `develop`. Branch names `codegen-main` and `codegen-develop`; each opens a PR against its own base. - **`.github/workflows/merge-bot-pull-request.yml`** `merge-codegen` job uses strict head/base pairing and dispatches `--squash` (develop) vs `--merge` (main) per base, same `case` statement pattern as `merge-dependabot`. - **README.md "Template - Release Distribution Model: Push vs. Pull"** (new section) — documents the default push-on-merge model and the manual-release alternative for HACS / distro-vendored projects, referencing [homeassistant-purpleair](https://github.com/ptr727/homeassistant-purpleair) as the working example. - **README.md "Template - GitHub Setup"** rulesets section split into separate Develop (squash-only + `Require linear history`) and Main (merge-commit-only) rulesets with shared settings extracted. Repo-level Pull Requests block now correctly shows **both** `Allow merge commits` and `Allow squash merging` enabled (the prior wording suggested merge would be disabled, which contradicted the actual main ruleset). ## Unnecessary back-merge `5ce95cf` (acknowledged misstep) I opened this release with a `Merge remote-tracking branch 'origin/main' into develop` commit assuming the back-merge pattern was the standard cycle close. **It wasn't.** That model conflicts with AGENTS.md's squash-only develop rule (the push only succeeded via admin bypass). PR #78's AGENTS.md update now explicitly forbids future back-merges and routes both bots to update both branches independently — closing the gap that made this pattern feel necessary. The back-merge commit is left in place: reverting requires a destructive force-push to develop. Behavioral effect is a one-time gitHeight bump, which fixes the `--pre` channel ordering (develop's next dev publish is now unambiguously higher than main's last release). ## Operator action items (already completed) - `CODEGEN_APP_ID` / `CODEGEN_APP_PRIVATE_KEY` in both Actions and Dependabot secret stores. ✓ - `WORKFLOW_PAT` secret + PAT revoked. ✓ - `pypi` GitHub environment Deployment branch rule allows `main` and `develop`. ✓ ## Open follow-up (tracked separately, not in this release) PR #78 documented a limitation in the actor-check guardrail on `merge-codegen` / `merge-dependabot`: it stops the merge-bot from re-invoking `gh pr merge --auto` on a maintainer-triggered `synchronize`, but **does not** disable auto-merge that's already enabled. Once auto-merge is on a bot PR, maintainer commits will land. The honest workaround is documented (`gh pr merge --disable-auto <PR>` before pushing). The real safeguard — a `synchronize`-triggered job that disables auto-merge automatically when the actor isn't the bot — is a follow-up PR I'll open after this release lands. ## Notes - Merge method: **merge-commit** (per [AGENTS.md branching model](https://github.com/ptr727/ProjectTemplate/blob/develop/AGENTS.md#branching-model)). ## Test plan - [ ] CI passes on the merge commit. - [ ] `publish-release.yml` on main publishes `1.0.<N>.<B>` as the next stable release. - [ ] After release, `pip install ptr727-projecttemplate-library` resolves to the new stable; `pip install --pre ptr727-projecttemplate-library` resolves to the latest develop dev (assuming develop's gitHeight exceeds main's, which the back-merge ensures). - [ ] Next Dependabot scheduled run opens **two** PRs per ecosystem (one against main, one against develop). - [ ] Next codegen weekly run opens **two** PRs (`codegen-main` against main, `codegen-develop` against develop). - [ ] Each bot PR auto-merges via the merge-bot with the correct method (`--squash` for develop, `--merge` for main).
Summary
Make PyPI behave like NuGet / Docker / GitHub releases — main pushes ship release versions, develop pushes ship prerelease versions. PyPI was the odd one out, only publishing on main.
Workflow changes
build-pypilibrary-task.ymlNew "Compute PyPI version step" before "Write version into _version.py":
github.refpip installbehaviorrefs/heads/mainMajor.Minor.Patch.BuildNumber(e.g.1.0.47.53309)refs/heads/developMajor.Minor.Patch.BuildNumber.dev0(e.g.1.0.47.53400.dev0)--preis passed.AssemblyFileVersionas-isuv build.Why
.dev0not.devB(Copilot caught this on the first review): PEP 440 compares release segments first.M.N.P.devNhas release[M,N,P]while main'sM.N.P.Bhas[M,N,P,B]— main wins on segments even with--pre. Keeping the BuildNumber in the release segment (M.N.P.B.dev0) makes develop's segment grow past main's per commit, which is what lets--preactually prefer develop. NBGV BuildNumber is unique per commit, so a constant.dev0is enough to disambiguate uploads.Edge case (documented inline): in the window between a release merge to main and the next commit on develop, develop's BuildNumber equals main's (or is one lower), so
--prewill still resolve to the main release until a new develop commit lands. Self-healing.The sed step now consumes
steps.pypiver.outputs.versioninstead ofneeds.get-version.outputs.AssemblyFileVersiondirectly.publish-release.ymlif: github.ref == 'refs/heads/main'onpublish-pypi. The job now runs on every push tomainANDdevelop, like its NuGet / Docker / executable siblings.Operator action (already done out-of-band)
pypiGitHub environment's Deployment branch rule now allows bothmainanddevelop. Without this, develop pushes would be blocked at the env gate. Confirmed done by repo owner.Documentation
PyPiLibrary/README.md:pip install <pkg>(release channel) vspip install --pre <pkg>(dev channel), the BuildNumber-in-release-segment rationale, and the edge case.mainanddevelopas required entries in the env Deployment branch rule._version.pydocstring updated to describe both branches' PEP 440 forms and the post-release edge case.Test plan
publish-release.yml,publish-pypienters thepypienvironment (no longer blocked byif:or env rule), and uploadsM.N.P.B.dev0to PyPI.pip install ptr727-projecttemplate-librarystill resolves to the latest main release (unchanged);pip install --pre ptr727-projecttemplate-libraryresolves to the latest develop dev release.mainships PyPI atM.N.P.Bas before.