From e72324d780b2d3dee8de7ecfc359fe14179b8be9 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 11 May 2026 16:02:39 -0700 Subject: [PATCH 1/2] Document dotnet/nbgv@master exception and drop redundant secrets: inherit (#76) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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](https://github.com/ptr727/ProjectTemplate/pull/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](https://github.com/ptr727/ProjectTemplate/pull/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) --- .github/workflows/build-nugetlibrary-task.yml | 1 - .github/workflows/get-version-task.yml | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-nugetlibrary-task.yml b/.github/workflows/build-nugetlibrary-task.yml index 3bdc4f8b..a0b9413f 100644 --- a/.github/workflows/build-nugetlibrary-task.yml +++ b/.github/workflows/build-nugetlibrary-task.yml @@ -18,7 +18,6 @@ jobs: get-version: name: Get version information job uses: ./.github/workflows/get-version-task.yml - secrets: inherit build-nugetlibrary: name: Build NuGet library project job diff --git a/.github/workflows/get-version-task.yml b/.github/workflows/get-version-task.yml index 91305882..2677cc9f 100644 --- a/.github/workflows/get-version-task.yml +++ b/.github/workflows/get-version-task.yml @@ -36,6 +36,23 @@ jobs: with: fetch-depth: 0 + # `dotnet/nbgv` is intentionally floated on `master` rather than + # pinned to a commit SHA — a deliberate deviation from the + # AGENTS.md "pin third-party actions to a commit SHA" rule, + # documented here so the deviation isn't accidentally "fixed" by + # a future reviewer. Justification: + # - The upstream tag stream is effectively dormant — the latest + # tag `v0.5.1` lags well behind `master` and fixes accumulate + # on `master` between tag bumps. + # - Dependabot's GitHub Actions ecosystem tracks tagged + # releases. A SHA pinned to a post-`v0.5.1` `master` commit + # would either receive no Dependabot updates (silently stale) + # or get an attempted downgrade PR to `v0.5.1`'s SHA. Neither + # beats just floating on `master`. + # - Upstream owner is Microsoft (`dotnet/`), so the + # "tag-/branch-retargeting risk" the AGENTS.md rule guards + # against is materially lower than for a random author. + # Revisit if `dotnet/nbgv` resumes regular tagged releases. - name: Run Nerdbank.GitVersioning tool step id: nbgv uses: dotnet/nbgv@master From 1b1df869b1b59fb064a45935059fdb447e9757f7 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 11 May 2026 16:44:27 -0700 Subject: [PATCH 2/2] Forward-only develop with dual-target Dependabot + codegen; push-vs-pull docs (#78) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codifies the branching model AGENTS.md actually implies and aligns the bots/docs to match. Closes the gap that made me push two unnecessary admin-bypass back-merges (`ffb9e64` and `5ce95cf`) — those violated the develop squash-only ruleset and shouldn't have happened. ## What changes ### Workflow + config | File | Change | |---|---| | `.github/dependabot.yml` | Every ecosystem (`nuget`, `github-actions`, `uv`) now has **two entries** — one with `target-branch: "main"`, one with `target-branch: "develop"`. Dependabot opens parallel PRs against each branch independently. | | `.github/workflows/run-codegen-pull-request-task.yml` | Codegen runs as a **matrix** over `main` and `develop`. Branches `codegen-main` and `codegen-develop`; each opens a PR against its own base. `fail-fast: false` so a per-branch failure doesn't block the other. | | `.github/workflows/merge-bot-pull-request.yml` `merge-codegen` | Accepts both head/base pairings (`codegen-main → main`, `codegen-develop → develop`) with strict pairing to prevent cross-targeting. Merge step uses the same `case` statement as `merge-dependabot` to dispatch `--squash` (develop) vs `--merge` (main) per base. | ### Documentation - **AGENTS.md "Branching Model"** — new bullets codifying forward-only develop (no main → develop back-merges; develop squash-only ruleset blocks them) and the dual-target bot pattern with rationale. - **README.md "Template - Branching Workflow"** — rewritten to mirror AGENTS.md. Drops the stale "Squash and merge from develop to main" and "bots merge into main directly" lines. - **README.md "Template - Release Distribution Model: Push vs. Pull"** (new section) — documents the default push-on-merge release model (every commit on main → release) and walks through 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"** — codegen auto-merge condition updated for the new head/base pairing + actor check + dual-target pointer. Rulesets section split into Develop (squash-only) and Main (merge-only) with shared settings called out. ## Why dual-target, not develop-only Considered develop-only-with-flow-through (codegen and Dependabot target develop only; main picks up changes through develop → main releases). Two problems with that model: 1. **Push-distribution channels consume `main` directly** — HACS, distros that vendor from main, Linux package managers. If main only got dep bumps via the next develop → main release and the release cadence is tied to feature completion, main runs stale code for weeks. 2. **Codegen content can be production-critical.** This template's codegen is trivial demo content (quote-of-the-day, date stamps), but derived projects can use codegen for live API-derived data (language lists, build catalogs, license databases). Stale codegen on main isn't merely cosmetic. Dual-target keeps both branches current on their own cadence, no back-merges, forward-only invariant preserved. ## Test plan - [ ] CI passes on this PR. - [ ] After merge to develop, the next Dependabot scheduled run opens **two** PRs per ecosystem (one against main, one against develop). - [ ] Next codegen run opens **two** PRs (one against main, one against develop). - [ ] Each PR auto-merges via the merge-bot using the correct method (`--squash` for develop, `--merge` for main). - [ ] No more back-merges land on develop. --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .github/dependabot.yml | 78 +++++++++++++++---- .github/workflows/merge-bot-pull-request.yml | 65 +++++++++++----- .../run-codegen-pull-request-task.yml | 27 ++++++- AGENTS.md | 4 +- README.md | 72 ++++++++++++----- 5 files changed, 186 insertions(+), 60 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 0c3e9331..15b75796 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,25 +1,47 @@ # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file # -# `target-branch: "develop"` on every ecosystem entry routes Dependabot's -# scheduled version-update PRs to the integration branch instead of the -# repo default (`main`). That keeps dep bumps in the -# `feature → develop → main` flow described in AGENTS.md: each bump joins -# the bot-triggered develop prerelease for early-warning testing, then -# bundles into the next develop → main merge-commit alongside feature work. -# This is also what keeps develop from falling behind main — bumps land on -# develop first. +# Every ecosystem appears **twice**: once with `target-branch: "main"` +# and once with `target-branch: "develop"`. Dependabot will open +# parallel PRs against each branch, so both stay current on +# dependency versions independently of the develop → main release +# cadence. # -# Caveat: `target-branch` only redirects scheduled version updates. -# Dependabot *security update PRs* (the CVE-driven ones Dependabot opens -# in response to security alerts) are opened against the default branch -# (`main`) and do not honor `target-branch`. The merge-bot's `case` -# statement in .github/workflows/merge-bot-pull-request.yml handles -# either base correctly (squash for develop, merge for main), and a -# maintainer can retarget manually from the PR UI if a one-off needs -# the other branch. +# Why dual-target and not develop-only: +# - `develop` is the integration branch and ships content forward to +# `main` through merge-commit releases, but the time between releases +# can be long (a feature branch may sit on develop for weeks). +# - Push-distribution channels (e.g. HACS for Home Assistant +# integrations, distros that pull from main) consume `main` directly. +# If `main` only got dependency bumps via the next develop → main +# release, those channels would ship outdated code in the interim. +# - Codegen workflows take the same dual-target shape for the same +# reason — see .github/workflows/run-codegen-pull-request-task.yml. +# +# The merge-bot's `case` statement in +# .github/workflows/merge-bot-pull-request.yml dispatches the merge +# method per base ref (squash on develop, merge on main) so both bases +# auto-merge cleanly. `develop` remains strictly forward-only: there +# are no main → develop back-merges; each branch absorbs its own +# Dependabot PRs and codegen PRs independently. +# +# Security update PRs (CVE-driven) are opened by Dependabot against +# the repo default branch (`main`) regardless of any `target-branch` +# config — the `case` statement handles them in the same code path. version: 2 updates: + # ----- nuget ----- + + - package-ecosystem: "nuget" + target-branch: "main" + directory: "/" + schedule: + interval: "daily" + groups: + nuget-deps: + patterns: + - "*" + - package-ecosystem: "nuget" target-branch: "develop" directory: "/" @@ -30,6 +52,18 @@ updates: patterns: - "*" + # ----- github-actions ----- + + - package-ecosystem: "github-actions" + target-branch: "main" + directory: "/" + schedule: + interval: "daily" + groups: + actions-deps: + patterns: + - "*" + - package-ecosystem: "github-actions" target-branch: "develop" directory: "/" @@ -40,6 +74,18 @@ updates: patterns: - "*" + # ----- uv (PyPiLibrary) ----- + + - package-ecosystem: "uv" + target-branch: "main" + directory: "/PyPiLibrary" + schedule: + interval: "daily" + groups: + pypi-deps: + patterns: + - "*" + - package-ecosystem: "uv" target-branch: "develop" directory: "/PyPiLibrary" diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 29d58ca7..7aaaf7c2 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -11,14 +11,16 @@ name: Merge bot pull request action # PRs created under `GITHUB_TOKEN` to nudge the auto-merge workflow. # # Merge method: -# Each merge step picks `--squash` or `--merge` from the PR's base ref so -# the form matches that branch's ruleset (`develop` allows only squash, -# `main` allows only merge commits — see AGENTS.md "Branching Model"). -# A mismatch fails `enablePullRequestAutoMerge` with "Merge method ... is -# not allowed on this repository". Codegen PRs always target `main` so -# they always merge-commit; Dependabot PRs default to `develop` but -# security update PRs open against `main`, so the `case` statement -# handles both bases. +# Each merge step picks `--squash` or `--merge` from the PR's base ref +# so the form matches that branch's ruleset (`develop` allows only +# squash, `main` allows only merge commits — see AGENTS.md "Branching +# Model"). A mismatch fails `enablePullRequestAutoMerge` with "Merge +# method ... is not allowed on this repository". Both Dependabot and +# codegen open parallel PRs against `main` and `develop` (see the +# AGENTS.md "Branching Model" dual-target bot section), so both jobs +# below use a `case` statement to dispatch the merge method by base +# ref. Dependabot security update PRs (always against `main`) flow +# through the same code path. on: pull_request: @@ -84,20 +86,33 @@ jobs: merge-codegen: name: Merge codegen pull request job runs-on: ubuntu-latest - # Restrict to codegen PRs that originate from the App in this repository. - # Codegen always opens PRs against `main` from the `codegen` branch. - # 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. + # Restrict to codegen PRs that originate from the App in this + # repository. Codegen runs in a matrix over `main` and `develop`, + # so two head refs are valid: `codegen-main` (always targets `main`) + # and `codegen-develop` (always targets `develop`). The head/base + # pairing is enforced strictly so a misconfigured workflow can't, + # for example, sneak a `codegen-develop` branch into `main`. + # Both the PR author AND the event actor must be the App: the + # author check stops human-opened PRs that happen to target a + # `codegen-*` branch from auto-merging; the actor check stops + # this job from re-invoking `gh pr merge --auto` on a + # `synchronize` event a maintainer triggered. + # + # Limitation worth knowing: the actor check does NOT disable + # auto-merge if it was already enabled by the initial bot-driven + # `opened` event. Once auto-merge is on, every commit that + # passes CI will land — including a maintainer's. To edit a + # codegen PR safely, run `gh pr merge --disable-auto ` (or + # click "Disable auto-merge" in the GitHub UI) BEFORE pushing, + # then re-enable it manually when ready. 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 + github.event.pull_request.head.repo.full_name == github.repository && + ( + (github.event.pull_request.head.ref == 'codegen-main' && github.event.pull_request.base.ref == 'main') || + (github.event.pull_request.head.ref == 'codegen-develop' && github.event.pull_request.base.ref == 'develop') + ) permissions: contents: write pull-requests: write @@ -112,7 +127,17 @@ jobs: private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} - name: Merge pull request step - run: gh pr merge --auto --merge "$PR_URL" + run: | + set -euo pipefail + case "${{ github.event.pull_request.base.ref }}" in + develop) method=--squash ;; + main) method=--merge ;; + *) + echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}" + exit 1 + ;; + esac + gh pr merge --auto "$method" "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/run-codegen-pull-request-task.yml b/.github/workflows/run-codegen-pull-request-task.yml index 35083cdd..4e284c34 100644 --- a/.github/workflows/run-codegen-pull-request-task.yml +++ b/.github/workflows/run-codegen-pull-request-task.yml @@ -1,5 +1,14 @@ name: Run codegen and pull request task +# Runs codegen against `main` and `develop` in parallel via a matrix, +# opens a PR against each base (`codegen-main` branch → main, +# `codegen-develop` branch → develop). The merge-bot auto-merges +# either PR independently. This keeps both branches current on +# generated content (date stamps, API-derived data, etc.) without +# either branch falling behind the other and without main → develop +# back-merges (see AGENTS.md "Branching Model" for the forward-only +# develop invariant). + on: workflow_call: secrets: @@ -15,11 +24,21 @@ on: jobs: codegen: - name: Run codegen and pull request job + name: Run ${{ matrix.target.ref }} codegen and pull request job runs-on: ubuntu-latest permissions: contents: write pull-requests: write + strategy: + # Each branch gets its own parallel codegen run + PR. If one + # branch's PR fails (CI, conflicts, etc.) the other is unaffected. + fail-fast: false + matrix: + target: + - ref: main + branch: codegen-main + - ref: develop + branch: codegen-develop steps: @@ -42,7 +61,7 @@ jobs: - name: Checkout code step uses: actions/checkout@v6 with: - ref: main + ref: ${{ matrix.target.ref }} token: ${{ steps.app-token.outputs.token }} - name: Run codegen step @@ -65,8 +84,8 @@ jobs: with: # App token: triggers pull_request workflow events directly, creates verified commits as the app token: ${{ steps.app-token.outputs.token }} - base: main - branch: codegen + base: ${{ matrix.target.ref }} + branch: ${{ matrix.target.branch }} title: 'Update codegen files' body: 'This PR updates the codegen files.' commit-message: 'Update codegen files' diff --git a/AGENTS.md b/AGENTS.md index 22466044..cd737d41 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,9 @@ Treat this file as authoritative for everything else; don't restate its rules el - `develop` is the integration branch. Feature branches → `develop` is **squash-only**; develop is kept linear. - `develop` → `main` is **merge-commit only** (no squash, no rebase). Merge commits preserve develop's commit list as a real second-parent reference on main, which is what makes the "release on every push" model attribute releases to the develop commits that produced them. Branch protection enforces this: the develop ruleset allows only `squash`, the main ruleset allows only `merge`. - All commits on both branches must be cryptographically signed (SSH or GPG). Squash and merge commits created via the GitHub UI are signed by GitHub's web-flow key. -- **Dependabot scheduled updates target `develop`** (see `target-branch` in [`.github/dependabot.yml`](./.github/dependabot.yml)) so develop stays ahead of main; bumps land on develop first and bundle into the next develop → main merge-commit. Security update PRs from Dependabot open against `main` directly (Dependabot doesn't honor `target-branch` for those). The merge-bot ([`.github/workflows/merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml)) picks `--squash` or `--merge` from each PR's base ref so the form matches the ruleset on either base. +- **`develop` is forward-only — no `main → develop` back-merges.** The develop ruleset's squash-only setting physically blocks merge commits on develop. Historical back-merge commits visible in `git log` (`b9b0447`, `410ba56`, `ffb9e64`, `5ce95cf`, etc.) predate this rule and must not be repeated. +- **Bots (Dependabot and codegen) target both `main` and `develop` in parallel.** [`.github/dependabot.yml`](./.github/dependabot.yml) duplicates every ecosystem entry (one per branch) and [`.github/workflows/run-codegen-pull-request-task.yml`](./.github/workflows/run-codegen-pull-request-task.yml) runs as a matrix over both branches with branch names `codegen-main` and `codegen-develop`. Each branch absorbs its own bot PRs independently, so neither falls behind, and the forward-only rule still holds (nothing is back-merged from main to develop — both branches receive their updates directly). The merge-bot ([`.github/workflows/merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml)) dispatches `--squash` or `--merge` from each PR's base ref via a `case` statement so the form matches the ruleset on either base. Dependabot **security** PRs (CVE-driven) always open against the repo default branch (`main`) regardless of `target-branch` — the same `case` statement covers them. +- **Why parallel dual-target rather than develop-only with eventual flow-through:** push-distribution channels (HACS for Home Assistant integrations, Linux distros that vendor from `main`, etc.) consume `main` directly. A develop-only model would leave `main` running stale code during long-running develop features. Codegen content can also be production-critical (live API-derived data, language lists, build catalogs) rather than just sample/demo content, so both branches need fresh codegen on their own cadence. ## Pull Request Title and Commit Message Conventions diff --git a/README.md b/README.md index 27475790..9c5e6584 100644 --- a/README.md +++ b/README.md @@ -457,9 +457,11 @@ Licensed under the [MIT License][license-link]\ - The App token is used by **both** the codegen workflow (`run-codegen-pull-request-task.yml`) **and** every job in `merge-bot-pull-request.yml`. App-authored pushes/PRs trigger downstream `pull_request` and `push` workflow events directly — unlike `GITHUB_TOKEN`-authored events, which are blocked by GitHub's recursion guard. This is why `publish-release.yml` fires on the merge commit after Dependabot or codegen auto-merge, and why the codegen workflow no longer needs the legacy close/reopen dance to trigger auto-merge. - The codegen auto-merge condition in `merge-bot-pull-request.yml` (`merge-codegen` job) requires: - `github.event.pull_request.user.login == 'ptr727-codegen[bot]'` — PR was opened by the App. - - `github.event.pull_request.head.ref == 'codegen'` — source branch is `codegen`. - - `github.event.pull_request.base.ref == 'main'` — PR targets `main`. + - `github.actor == 'ptr727-codegen[bot]'` — the event was triggered by the App. This stops the job from **re-invoking `gh pr merge --auto`** on a maintainer-triggered `synchronize`, but **does not disable auto-merge once it was already enabled by the initial bot-driven `opened` event**. If a maintainer pushes commits to a codegen PR with auto-merge already on, the next CI pass will merge them. To edit a codegen PR safely, disable auto-merge first via `gh pr merge --disable-auto ` (or the GitHub UI button) before pushing. - `github.event.pull_request.head.repo.full_name == github.repository` — PR is from this repo (not a fork). + - **Strict head/base pairing** — `(head.ref == 'codegen-main' && base.ref == 'main') || (head.ref == 'codegen-develop' && base.ref == 'develop')`. Codegen runs as a matrix opening one PR per branch; this pairing prevents a misconfigured workflow from sneaking a `codegen-develop` branch into `main` or vice versa. + + Codegen targets `main` AND `develop` in parallel (matrix in `run-codegen-pull-request-task.yml`), so generated content lands on both branches independently without any back-merging. See [AGENTS.md "Branching Model"](./AGENTS.md#branching-model) for why this dual-target pattern beats develop-only-with-flow-through. **Codegen workflow schedule**: @@ -469,21 +471,28 @@ Licensed under the [MIT License][license-link]\ - General: - Default branch: `main` - - Pull requests: - - `Allow squash merging` - - [TODO:](https://github.com/orgs/community/discussions/184410): Disable merge and rebase merging, ruleset merge rules do not currently work. + - Pull requests — **both** merge methods enabled at the repo level so each branch ruleset can pick the right one (develop = `Squash`, main = `Merge`): + - `Allow merge commits` ✓ (required for develop → main releases) + - `Allow squash merging` ✓ (required for feature → develop merges) + - `Allow rebase merging` — disabled (no flow uses it; the develop ruleset forbids it anyway) - `Always suggest updating pull request branches` - `Allow auto-merge` -- Rules / Rulesets: - - "Main and Develop": - - Target branches: `main`, `develop`. +- Rules / Rulesets — **separate rulesets per branch** so allowed merge methods differ (develop = squash-only; main = merge-commit-only, per AGENTS.md). Everything else is shared. + - "Develop": + - Target branches: `develop`. + - Allowed merge methods: `Squash` + - `Require linear history` (develop is kept linear; main carries merge commits by design, so this setting belongs to develop only) + - Plus shared settings (below). + - "Main": + - Target branches: `main`. + - Allowed merge methods: `Merge` + - Plus shared settings (below). + - Shared settings (apply to both rulesets): - `Restrict deletions` - - `Require linear history` - `Require signed commits` - `Require a pull request before merging` - `Dismiss stale pull request approvals when new commits are pushed` - `Require conversation resolution before merging` - - Allowed merge methods: `Squash` - `Require status checks to pass` - `Require branches to be up to date before merging` - Status checks that are required: `Check pull request workflow status` @@ -496,15 +505,40 @@ Licensed under the [MIT License][license-link]\ ### Template - Branching Workflow -- Create persistent `main` and `develop` branches. -- Protect `main` and `develop` branches with branch protection rules. -- Make sure that `main` and `develop` are always building error free. -- Create feature branches from the `develop` branch. -- Only commit to feature branches, do not commit directly to `develop` or to `main`. -- Always "Squash and merge" from feature branches to the `develop` branch to minimize change history. -- Always "Squash and merge" from `develop` to `main` to maintain a linear history. -- Bot generated pull requests (codegen, dependabot) always checkout from and merge into `main` directly. -- If `develop` falls behind after a bot merge, re-run codegen or rebase `develop` on `main` before merging `develop` to `main`. +See [AGENTS.md "Branching Model"](./AGENTS.md#branching-model) for the authoritative definition. Summary: + +- Persistent `main` and `develop` branches, each with its own ruleset (above). Both must always be building error free. +- Feature branches off `develop`. Only commit on feature branches, never directly to `develop` or `main`. +- Feature → `develop`: **squash-merge** (develop ruleset enforces this; develop is kept linear). +- `develop` → `main`: **merge-commit** (preserves develop's commit list as a real second-parent reference on main; main ruleset enforces this). +- **`develop` is forward-only.** No `main → develop` back-merges. The develop squash-only ruleset physically blocks merge commits. +- **Bots open parallel PRs against both branches.** [`.github/dependabot.yml`](./.github/dependabot.yml) duplicates each ecosystem entry per branch, and [`.github/workflows/run-codegen-pull-request-task.yml`](./.github/workflows/run-codegen-pull-request-task.yml) runs as a matrix (branch names `codegen-main` and `codegen-develop`). Each branch absorbs its own bot PRs independently — neither falls behind, no back-merges needed. + +### Template - Release Distribution Model: Push vs. Pull + +This template ships with a **push-on-merge release model** — every commit on `main` triggers [`.github/workflows/publish-release.yml`](./.github/workflows/publish-release.yml) which publishes a GitHub release, NuGet/PyPI uploads, Docker tags, and platform executables. With the dual-target bot model (Dependabot/codegen targeting both branches), this means every Dependabot bump that lands on `main` produces a new release. That's the right default for projects whose consumers **pull** at their own cadence (Docker pulls, NuGet/PyPI installs, manual binary downloads) — releases are cheap and frequent, consumers update on their own schedule. + +For projects whose consumers are **pushed** updates (HACS for Home Assistant, package managers that auto-update integrations, Linux distros that vendor from `main`), every release is a forced update to all users. Frequent bot-driven releases become noise. To switch to a **manual main-release model** while keeping the rest of the dual-target dual-channel flow: + +1. Edit [`.github/workflows/publish-release.yml`](./.github/workflows/publish-release.yml) and change the trigger: + + ```diff + on: + - push: + - branches: [ main, develop ] + - workflow_dispatch: + + push: + + branches: [ develop ] + + workflow_dispatch: + ``` + + Result: `develop` pushes still publish dev releases automatically (PEP 440 `.dev0` to PyPI, NBGV-prerelease tags on NuGet, prerelease GitHub releases). `main` pushes no longer auto-publish; you trigger the release manually via the GitHub Actions UI (`workflow_dispatch`) when a real release is wanted. + +2. **(Optional)** narrow what flows into `main` automatically. If a sea of Dependabot PRs on `main` is noisy without auto-release, either: + - Drop the `main`-target Dependabot entries from `.github/dependabot.yml` (so deps update on `develop` only, and reach `main` through the next develop → main release the maintainer triggers — closer to a pure develop-only flow with manual cadence), or + - Keep dual-target Dependabot and let the merge-bot auto-merge them silently into `main`; main always has fresh code, but ships only when the maintainer dispatches a release. + +For an example of the manual-release model in production, see [homeassistant-purpleair](https://github.com/ptr727/homeassistant-purpleair) — that integration ships through HACS (push distribution) and uses `workflow_dispatch` for actual releases.