From f154b783a45b367283c2ed4c01333a8e72c0b6ad Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 11 May 2026 13:12:06 -0700 Subject: [PATCH 1/3] Standardize bots on GitHub App token, align merge methods, version PyPI via NBGV (#70) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary A consolidation PR. Three threads land together because they overlap (all touch the merge-bot + codegen + PyPI release flow): 1. **Standardize all bot workflows on the GitHub App token.** Drop the dual codegen path (PAT + App) — App was already the better one. Drop the merge-bot's GITHUB_TOKEN-based codegen merge path. Result: every bot-authored push/PR fires downstream `pull_request` / `push` events directly (no recursion-guard skips), so `publish-release.yml` fires on bot-driven merges to develop and main exactly the same way it does on human merges. 2. **Align merge method per base branch.** Mirrors the homeassistant-purpleair pattern: develop ruleset allows only squash, main ruleset allows only merge commits. The merge-bot dispatches via a `case` on `pull_request.base.ref` so the form matches either base. 3. **Route Dependabot to develop and version the PyPI library via NBGV.** Closes "develop falls behind main" because scheduled bumps land on develop first. Picks the long-deferred PyPI versioning question by reading NBGV's `AssemblyFileVersion` and overwriting `_version.py` in CI before `uv build`, so PyPI and NuGet ship at matching versions per release. ## Standardize on GitHub App token ### Codegen - **Delete** `.github/workflows/run-codegen-app-pull-request-task.yml` and `.github/workflows/run-periodic-codegen-app-pull-request.yml`. - **Rewrite** `.github/workflows/run-codegen-pull-request-task.yml` with the App-token-based logic (formerly in the deleted *app* variant). No PAT, no close/reopen dance — App tokens trigger `pull_request` workflow events directly when opening a PR. - **Update** `.github/workflows/run-periodic-codegen-pull-request.yml`: drop the Monday/Thursday alternation note; the weekly Monday cron + `workflow_dispatch` remain. ### Merge bot - **Delete** `merge-codegen-app` job (the App-token-based duplicate of `merge-codegen`). - **Rewrite** the remaining `merge-codegen` to use the App token, match `ptr727-codegen[bot]`, and skip the legacy `(reopened & owner) || (!reopened & github-actions[bot])` actor dance that existed only because `GITHUB_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 `--squash` with a `case` on `pull_request.base.ref` that picks `--squash` for develop and `--merge` for main; explicit failure for unknown bases. - Header comment rewritten to reflect the new single-strategy model (App token everywhere; matched merge method per base). ## Align merge method per base branch Bot PRs targeting `main` previously used `gh pr merge --auto --squash` — fine while the main ruleset allowed squash, but fragile once main locks to merge-only (the purpleair experience). The dispatcher in `merge-bot-pull-request.yml` now lifts the form from `base.ref`: ```yaml 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" ``` ## 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 against `main` directly** — Dependabot doesn't honor `target-branch` for those — but the new `case` statement 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 inner `get-version` job (mirroring `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`. 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 that `0.0.0` is a local-development convenience and CI overwrites the file before `uv 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's `target-branch: develop` routing 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 `--squash` for develop and `--merge` for main when invoking `gh 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 mandatory `pypi` environment deployment branch rule on `main`, optional required reviewer, troubleshooting for `invalid-publisher` and `manifest 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 - [ ] CI passes on the PR. - [ ] After merge, the next Dependabot scheduled run opens its PRs against `develop` (currently opens against `main`). - [ ] When that Dependabot PR auto-merges, the resulting develop push fires `publish-release.yml`. - [ ] Next codegen PR auto-merges to `main` as a merge-commit (not a squash). - [ ] Next release on `main` publishes the PyPI library at the same `M.N.P.B` version as the NuGet package (no more `0.0.0` placeholder). --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .github/copilot-instructions.md | 2 + .github/dependabot.yml | 24 +- .github/workflows/build-pypilibrary-task.yml | 29 +++ .github/workflows/merge-bot-pull-request.yml | 213 +++++++++--------- .github/workflows/publish-release.yml | 8 + .../run-codegen-app-pull-request-task.yml | 70 ------ .../run-codegen-pull-request-task.yml | 145 ++++++------ .../run-periodic-codegen-app-pull-request.yml | 22 -- .../run-periodic-codegen-pull-request.yml | 44 ++-- AGENTS.md | 1 + PyPiLibrary/README.md | 33 ++- .../_version.py | 22 +- README.md | 61 ++--- 13 files changed, 332 insertions(+), 342 deletions(-) delete mode 100644 .github/workflows/run-codegen-app-pull-request-task.yml delete mode 100644 .github/workflows/run-periodic-codegen-app-pull-request.yml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b1d48d8c..7b9e51b0 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -17,6 +17,8 @@ Do not duplicate language-specific rules here. Feature → develop PRs squash-merge — the PR title becomes the single commit on develop. Develop → main PRs merge-commit — main's history shows one merge commit per release with develop's tip as the second parent. Titles are descriptive and have no versioning effect — versioning is handled by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) reading [version.json](../version.json) and git history, not by parsing commit messages. +Branch protection enforces the merge method on both bases (develop allows only squash, main allows only merge). When running `gh pr merge` against either base, pick the matching flag (`--squash` for develop, `--merge` for main); a mismatch fails with "Merge method ... is not allowed on this repository". The merge-bot workflow (`.github/workflows/merge-bot-pull-request.yml`) does this dispatch automatically for Dependabot and codegen PRs via a `case` on `base.ref` — keep that pattern when adding new auto-merge jobs. + ### Format - Imperative subject summarizing the change, ≤ 72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 66d6bd2b..0c3e9331 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,9 +1,27 @@ # 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. +# +# 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. version: 2 updates: - package-ecosystem: "nuget" - target-branch: "main" + target-branch: "develop" directory: "/" schedule: interval: "daily" @@ -13,7 +31,7 @@ updates: - "*" - package-ecosystem: "github-actions" - target-branch: "main" + target-branch: "develop" directory: "/" schedule: interval: "daily" @@ -23,7 +41,7 @@ updates: - "*" - package-ecosystem: "uv" - target-branch: "main" + target-branch: "develop" directory: "/PyPiLibrary" schedule: interval: "daily" diff --git a/.github/workflows/build-pypilibrary-task.yml b/.github/workflows/build-pypilibrary-task.yml index 71477b32..2a483877 100644 --- a/.github/workflows/build-pypilibrary-task.yml +++ b/.github/workflows/build-pypilibrary-task.yml @@ -19,9 +19,15 @@ on: jobs: + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + build-pypilibrary: name: Build PyPI library project job runs-on: ubuntu-latest + needs: [get-version] defaults: run: working-directory: ./PyPiLibrary @@ -60,6 +66,29 @@ jobs: - name: Run pytest step run: uv run pytest + # Replace the `__version__` line in `_version.py` (which ships + # hardcoded "0.0.0" so local `uv build` works without CI) with + # NBGV's `AssemblyFileVersion` — always Major.Minor.Patch.BuildNumber, + # all numeric, PEP 440 valid. `sed -i` replaces the line in place so + # the module docstring and any future metadata in the file survive + # into the published wheel / sdist. `_version.py` is the single + # source `hatchling` reads via the `[tool.hatch.version]` path in + # pyproject.toml. Done AFTER tests so the test that asserts + # `__version__` is a non-empty string isn't affected. The PyPI + # version string therefore equals the .NET assemblies' `FileVersion` + # stamp (= NBGV `AssemblyFileVersion`). .NET's `AssemblyVersion` + # is a separate NBGV output and NuGet `PackageVersion` / Docker + # tags use NBGV `SemVer2` (PEP 440 rejects its prerelease / + # build-metadata suffixes), so those strings are not byte-identical + # to PyPI's; all four still derive from the same NBGV computation + # per release commit. + - name: Write version into _version.py step + run: | + set -euo pipefail + sed -i 's/^__version__ = .*/__version__ = "'"$VERSION"'"/' src/ptr727_projecttemplate_library/_version.py + env: + VERSION: ${{ needs.get-version.outputs.AssemblyFileVersion }} + - name: Build sdist and wheel step run: uv build diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index c35b1576..1a431ed5 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -1,102 +1,111 @@ -name: Merge bot pull request action - -# Token strategy: -# GitHub's recursion guard blocks pushes authored by `GITHUB_TOKEN` from -# triggering further workflow runs. When `gh pr merge --auto --squash` runs -# under `secrets.GITHUB_TOKEN`, the resulting squash-merge push therefore -# does NOT fire `publish-release.yml`. -# -# All three jobs below merge bot PRs targeting `main` (per the per-job `if:` -# conditions). Releases on `main` are dispatched manually via -# `workflow_dispatch`, so the missing trigger is acceptable for all three -# paths. If a future bot PR targets `develop` (where releases auto-fire on -# push), this merge action would need to switch to an App token so the -# resulting push is authored by an App identity not blocked by the -# recursion guard. - -on: - pull_request: - types: [opened, reopened, synchronize] - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - merge-dependabot: - name: Merge dependabot pull request job - runs-on: ubuntu-latest - # To prevent abuse, the PR must come from Dependabot and the PR must originate from this repository. - if: >- - github.actor == 'dependabot[bot]' && - github.event.pull_request.head.repo.full_name == github.repository - permissions: - contents: write - pull-requests: write - - steps: - - - name: Get dependabot metadata step - id: metadata - uses: dependabot/fetch-metadata@v2 - with: - github-token: "${{ secrets.GITHUB_TOKEN }}" - - # Merge any non-NuGet update, e.g. GitHub Actions often updates v1 to v2. - # Merge NuGet only for non-major updates, e.g. major updates may build but break functionality. - - name: Merge pull request step - if: >- - (steps.metadata.outputs.package-ecosystem != 'nuget') || - (steps.metadata.outputs.update-type != 'version-update:semver-major') - run: gh pr merge --auto --squash "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - merge-codegen: - name: Merge codegen pull request job - runs-on: ubuntu-latest - # To prevent abuse, the PR must come from the codegen workflow, and the PR must originate from this repository. - if: >- - github.event.pull_request.user.login == 'github-actions[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.action == 'reopened' && github.actor == github.repository_owner) || - (github.event.action != 'reopened' && github.actor == 'github-actions[bot]') - ) - permissions: - contents: write - pull-requests: write - - steps: - - - name: Merge pull request step - run: gh pr merge --auto --squash "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} - - merge-codegen-app: - name: Merge codegen app pull request job - runs-on: ubuntu-latest - # To prevent abuse, the PR must come from the codegen app workflow, and the PR must originate from this repository. - if: >- - github.actor == 'ptr727-codegen[bot]' && - github.event.pull_request.user.login == '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 - permissions: - contents: write - pull-requests: write - - steps: - - - name: Merge pull request step - run: gh pr merge --auto --squash "$PR_URL" - env: - PR_URL: ${{github.event.pull_request.html_url}} - GH_TOKEN: ${{secrets.GITHUB_TOKEN}} +name: Merge bot pull request action + +# Token strategy: +# Every merge job in this workflow uses the GitHub App token +# (`actions/create-github-app-token`). The resulting merge push is +# committed by the App, which fires downstream workflows on develop and +# main. Pushes authored by `GITHUB_TOKEN` are blocked from triggering +# further workflow runs by GitHub's recursion guard, which would +# silently skip `publish-release.yml` on the merge commit. The App-token +# path also removes the close/reopen dance previously used by codegen +# 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. + +on: + pull_request: + types: [opened, reopened, synchronize] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + merge-dependabot: + name: Merge dependabot pull request job + runs-on: ubuntu-latest + # Restrict to Dependabot PRs that originate from this repository, not a + # fork. Check the PR author rather than the event actor so maintainer + # repair commits on Dependabot branches can still auto-merge after CI + # passes. + if: >- + github.event.pull_request.user.login == 'dependabot[bot]' && + github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: write + pull-requests: write + + steps: + + - name: Generate GitHub App token step + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.CODEGEN_APP_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Get dependabot metadata step + id: metadata + uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + # Skip semver-major NuGet bumps: majors can build cleanly but break + # runtime behaviour, so they should land via human review. Other + # ecosystems' majors (github-actions, uv) are usually safe and merge. + - name: Merge pull request step + if: >- + (steps.metadata.outputs.package-ecosystem != 'nuget') || + (steps.metadata.outputs.update-type != 'version-update:semver-major') + 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 }} + + 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. + if: >- + github.event.pull_request.user.login == '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 + permissions: + contents: write + pull-requests: write + + steps: + + - name: Generate GitHub App token step + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.CODEGEN_APP_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Merge pull request step + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 9914784a..edafc722 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -25,6 +25,14 @@ jobs: publish-pypi: name: Publish PyPI library job + # Restrict PyPI upload to `main` pushes. The `pypi` GitHub environment + # also has a Deployment branch rule allowing only `main` as defense in + # depth, but without this `if:` the job would still attempt to run on + # develop pushes and be blocked at the env gate — visible as a stalled + # / failed job on every develop release. PyPI tracks releases, not the + # prerelease channel; NuGet/Docker/executables already publish on + # develop with NBGV prerelease versions. + if: github.ref == 'refs/heads/main' needs: [create-release] runs-on: ubuntu-latest environment: diff --git a/.github/workflows/run-codegen-app-pull-request-task.yml b/.github/workflows/run-codegen-app-pull-request-task.yml deleted file mode 100644 index 31dfcb53..00000000 --- a/.github/workflows/run-codegen-app-pull-request-task.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Run codegen app and pull request task - -on: - workflow_call: - secrets: - # GitHub App credentials to generate an installation token - CODEGEN_APP_ID: - required: true - CODEGEN_APP_PRIVATE_KEY: - required: true - # API Ninjas API key - NINJA_API_KEY: - required: true - -jobs: - - codegen: - name: Run codegen app and pull request job - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - - name: Generate GitHub App token step - uses: actions/create-github-app-token@v1 - id: app-token - with: - app-id: ${{ secrets.CODEGEN_APP_ID }} - private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} - - - name: Setup .NET SDK step - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.x - - - name: Checkout code step - uses: actions/checkout@v6 - with: - ref: main - token: ${{ steps.app-token.outputs.token }} - - - name: Run codegen step - run: | - set -euo pipefail - dotnet run --project ./CodeGen/CodeGen.csproj -- \ - --codepath ./CodeGen \ - --apikey "${{ secrets.NINJA_API_KEY }}" - - - name: Format code step - run: | - set -euo pipefail - dotnet tool restore - dotnet csharpier format --log-level=debug . - git status - - - name: Create pull request step - uses: peter-evans/create-pull-request@v8 - id: cpr - with: - # Use app token: triggers pull_request workflow events directly, creates verified commits as the app - token: ${{ steps.app-token.outputs.token }} - base: main - branch: codegen - title: 'Update codegen files' - body: 'This PR updates the codegen files.' - commit-message: 'Update codegen files' - delete-branch: true - sign-commits: true diff --git a/.github/workflows/run-codegen-pull-request-task.yml b/.github/workflows/run-codegen-pull-request-task.yml index 77b99b52..35083cdd 100644 --- a/.github/workflows/run-codegen-pull-request-task.yml +++ b/.github/workflows/run-codegen-pull-request-task.yml @@ -1,71 +1,74 @@ -name: Run codegen and pull request task - -on: - workflow_call: - secrets: - # Use PAT to trigger workflows - WORKFLOW_PAT: - required: true - # API Ninjas API key - NINJA_API_KEY: - required: true - -jobs: - - codegen: - name: Run codegen and pull request job - runs-on: ubuntu-latest - permissions: - contents: write - pull-requests: write - - steps: - - - name: Setup .NET SDK step - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.x - - - name: Checkout code step - uses: actions/checkout@v6 - with: - ref: main - - - name: Run codegen step - run: | - set -euo pipefail - dotnet run --project ./CodeGen/CodeGen.csproj -- \ - --codepath ./CodeGen \ - --apikey "${{ secrets.NINJA_API_KEY }}" - - - name: Format code step - run: | - set -euo pipefail - dotnet tool restore - dotnet csharpier format --log-level=debug . - git status - - - name: Create pull request step - uses: peter-evans/create-pull-request@v8 - id: cpr - with: - # Use GITHUB_TOKEN to sign the commit, but will not trigger workflows - token: ${{ secrets.GITHUB_TOKEN }} - base: main - branch: codegen - title: 'Update codegen files' - body: 'This PR updates the codegen files.' - commit-message: 'Update codegen files' - delete-branch: true - sign-commits: true - - - name: Trigger PR workflows step - if: steps.cpr.outputs.pull-request-number != '' - run: | - set -euo pipefail - PR="${{ steps.cpr.outputs.pull-request-number }}" - gh pr close "$PR" - gh pr reopen "$PR" - env: - # Use PAT to trigger workflows - GH_TOKEN: ${{ secrets.WORKFLOW_PAT }} +name: Run codegen and pull request task + +on: + workflow_call: + secrets: + # GitHub App credentials to generate an installation token + CODEGEN_APP_ID: + required: true + CODEGEN_APP_PRIVATE_KEY: + required: true + # API Ninjas API key + NINJA_API_KEY: + required: true + +jobs: + + codegen: + name: Run codegen and pull request job + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + + - name: Generate GitHub App token step + # The App-token-driven PR open fires `pull_request` workflow events + # directly. `GITHUB_TOKEN`-driven PR opens do not (GitHub's recursion + # guard), which previously required a close/reopen dance under a PAT + # to nudge the auto-merge workflow — that dance is gone. + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.CODEGEN_APP_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@v6 + with: + ref: main + token: ${{ steps.app-token.outputs.token }} + + - name: Run codegen step + run: | + set -euo pipefail + dotnet run --project ./CodeGen/CodeGen.csproj -- \ + --codepath ./CodeGen \ + --apikey "${{ secrets.NINJA_API_KEY }}" + + - name: Format code step + run: | + set -euo pipefail + dotnet tool restore + dotnet csharpier format --log-level=debug . + git status + + - name: Create pull request step + uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 + id: cpr + 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 + title: 'Update codegen files' + body: 'This PR updates the codegen files.' + commit-message: 'Update codegen files' + delete-branch: true + sign-commits: true diff --git a/.github/workflows/run-periodic-codegen-app-pull-request.yml b/.github/workflows/run-periodic-codegen-app-pull-request.yml deleted file mode 100644 index 4701ab12..00000000 --- a/.github/workflows/run-periodic-codegen-app-pull-request.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Run weekly CodeGen App and Pull Request action - -on: - workflow_dispatch: - schedule: - # Run weekly on Thursdays at 02:00 UTC (PAT workflow runs on Mondays) - - cron: '0 2 * * THU' - -concurrency: - # Workflow always checks out and targets main/codegen - group: codegen-main - cancel-in-progress: true - -jobs: - - run-codegen-app: - name: Run codegen app and pull request job - uses: ./.github/workflows/run-codegen-app-pull-request-task.yml - secrets: inherit - permissions: - contents: write - pull-requests: write diff --git a/.github/workflows/run-periodic-codegen-pull-request.yml b/.github/workflows/run-periodic-codegen-pull-request.yml index 2c01b9c3..0623b651 100644 --- a/.github/workflows/run-periodic-codegen-pull-request.yml +++ b/.github/workflows/run-periodic-codegen-pull-request.yml @@ -1,22 +1,22 @@ -name: Run weekly CodeGen and Pull Request action - -on: - workflow_dispatch: - schedule: - # Run weekly on Mondays at 02:00 UTC (PAT workflow; app workflow runs on Thursdays) - - cron: '0 2 * * MON' - -concurrency: - # Workflow always checks out and targets main/codegen - group: codegen-main - cancel-in-progress: true - -jobs: - - run-codegen: - name: Run codegen and pull request job - uses: ./.github/workflows/run-codegen-pull-request-task.yml - secrets: inherit - permissions: - contents: write - pull-requests: write +name: Run weekly CodeGen and Pull Request action + +on: + workflow_dispatch: + schedule: + # Run weekly on Mondays at 02:00 UTC. + - cron: '0 2 * * MON' + +concurrency: + # Workflow always checks out and targets main/codegen + group: codegen-main + cancel-in-progress: true + +jobs: + + run-codegen: + name: Run codegen and pull request job + uses: ./.github/workflows/run-codegen-pull-request-task.yml + secrets: inherit + permissions: + contents: write + pull-requests: write diff --git a/AGENTS.md b/AGENTS.md index 97a8cdc5..22466044 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,6 +21,7 @@ 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. ## Pull Request Title and Commit Message Conventions diff --git a/PyPiLibrary/README.md b/PyPiLibrary/README.md index f099a2b7..e6294d4b 100644 --- a/PyPiLibrary/README.md +++ b/PyPiLibrary/README.md @@ -10,6 +10,7 @@ Python PyPI template — companion to the .NET `NuGetLibrary` in this repo. Publ - **Type checker** — [`pyright`](https://microsoft.github.io/pyright/) - **Tests** — [`pytest`](https://docs.pytest.org/) - **Publish** — [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) via `pypa/gh-action-pypi-publish` (no API token in repo secrets) +- **Version** — [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) (NBGV) shared with the .NET side. CI replaces the `__version__` line in `_version.py` (in place) with NBGV's `AssemblyFileVersion` (`Major.Minor.Patch.BuildNumber`, PEP 440 valid) before `uv build`; that matches the .NET assemblies' `FileVersion` stamp. .NET's `AssemblyVersion` (a separate NBGV output) and NuGet/Docker (NBGV `SemVer2`) carry different strings, but all four derive from the same NBGV computation per release commit. ## Layout @@ -48,10 +49,25 @@ Releases are produced by `.github/workflows/build-pypilibrary-task.yml` (called First-time setup (one-time, on PyPI): -1. PyPI → **Account settings** → **Publishing** → **Add a new pending publisher**. -2. Project name: `ptr727-projecttemplate-library`. Owner: `ptr727`. Repo: `ProjectTemplate`. Workflow: `publish-release.yml`. Environment: `pypi`. -3. GitHub repo → **Settings** → **Environments** → create `pypi` environment (optionally with required reviewers). -4. The first successful release converts the pending publisher to a real publisher. +Prerequisite: enable **2FA** on the PyPI account (TOTP or hardware key). PyPI requires it before any trusted publisher can be registered. + +1. **PyPI** → **Account settings** → **Publishing** → **Add a new pending publisher** ([direct link](https://pypi.org/manage/account/publishing/)). If the project already exists on PyPI, go to the project page → **Manage** → **Publishing** → **Add a new publisher** instead — the "pending" form is only for projects that don't exist yet. Fields: + - **PyPI project name**: `ptr727-projecttemplate-library` + - **Owner**: `ptr727` + - **Repository name**: `ProjectTemplate` + - **Workflow filename**: `publish-release.yml` + - **Environment name**: `pypi` +2. **GitHub repo** → **Settings** → **Environments** → **New environment** → `pypi`. The environment owns deploy-time guardrails: + - **Deployment branch rule** → **Selected branches and tags** → add `main`. **This step is mandatory — Trusted Publishing without a branch restriction is a documented security anti-pattern.** Defense in depth: the `publish-pypi` job in `.github/workflows/publish-release.yml` *also* has `if: github.ref == 'refs/heads/main'` so develop pushes don't even attempt to enter the environment gate (they'd otherwise stall as blocked deployments). The `if:` is the operational gate; the env branch rule is the security boundary that holds even if the `if:` gets misconfigured. + - (Optional) add yourself as a **required reviewer** so each publish requires a click — useful belt-and-suspenders against an accidental release. +3. The first successful release converts the pending publisher to a real publisher. After that the same OIDC exchange validates against the real publisher on every release. + +Troubleshooting: + +- `invalid-publisher: ... Publisher with matching claims was not found` — the publisher hasn't been registered yet, or one of the five claim fields (owner, repo, workflow filename, environment name, project name) doesn't match. Re-check step 1. +- `manifest unknown` from `docker:` pulling `ghcr.io/pypa/gh-action-pypi-publish` — the SHA pinned in `publish-release.yml` doesn't correspond to a release tag with a published GHCR image. Pin to the SHA that the upstream tag (`# vX.Y.Z` comment) actually points at on `pypa/gh-action-pypi-publish`. + +Fallback (API token instead of Trusted Publishing): drop the `id-token: write` permission from the `publish-pypi` job, add `password: ${{ secrets.PYPI_API_TOKEN }}` to the `pypa/gh-action-pypi-publish` step, and store the token as a repo secret. Also pass `attestations: false` since attestations require the OIDC token. The OIDC path is preferred — no long-lived secret in the repo — so use the token method only when Trusted Publishing isn't an option. ## Template Adoption @@ -60,9 +76,10 @@ When deriving a new project from this template: - Replace the package name `ptr727-projecttemplate-library` (in `pyproject.toml`, this README, and CI) with your name. - Rename `src/ptr727_projecttemplate_library/` to your import name. - Re-register the trusted publisher on PyPI under the new project name. -- **Wire up a versioning scheme before the first publish.** `_version.py` ships with `__version__ = "0.0.0"` as a placeholder. The publish workflow uses `skip-existing: true` so the workflow won't fail on duplicate uploads — but **no new versions will land on PyPI** until you replace `0.0.0` with something that increments. Common options: - - [`hatch-vcs`](https://github.com/ofek/hatch-vcs) — derive the version from git tags. Add it to `[build-system].requires` and switch `[tool.hatch.version]` to `source = "vcs"`. Pairs well with tag-driven releases. - - **Read from `version.json`** — the .NET side uses Nerdbank.GitVersioning which reads from `version.json`. A small custom Hatchling plugin or a CI step can pull the version into `_version.py` so .NET and Python ship with matching versions. - - **Manual bumps** — edit `_version.py` in each release PR. Simplest, but easy to forget. +- **Pick a versioning scheme.** The template defaults to **NBGV-driven** versioning shared with the .NET side: `_version.py` holds `__version__ = "0.0.0"` as a local-development placeholder, and the CI step **"Write version into _version.py step"** in [`build-pypilibrary-task.yml`](../.github/workflows/build-pypilibrary-task.yml) replaces the `__version__` line (in place, preserving the docstring) with NBGV's `AssemblyFileVersion` (always `Major.Minor.Patch.BuildNumber`, all numeric, PEP 440 valid) just before `uv build`. PyPI therefore ships the same version string that's stamped into the .NET assemblies as `FileVersion`. .NET's `AssemblyVersion` (the binary-compat identity — a separate NBGV output) and the **NuGet package version** / **Docker tags** (which use NBGV's `SemVer2` — PEP 440 doesn't accept its prerelease / build-metadata suffixes) all carry different strings; but all four derive from the same NBGV computation against `version.json` + git history and correspond to the same release commit. If you want a different scheme, replace both `_version.py` and the workflow step. Two common alternatives: + - [`hatch-vcs`](https://github.com/ofek/hatch-vcs) — derive the version from git tags. Add it to `[build-system].requires` and switch `[tool.hatch.version]` to `source = "vcs"`. Drop the CI overwrite step. Pairs well with tag-driven releases and removes the NBGV dependency. + - **Manual bumps** — edit `_version.py` in each release PR. Simplest, but easy to forget. Drop the CI overwrite step. + + The publish workflow uses `skip-existing: true` so a re-upload of the same version is a no-op instead of a failure — useful when iterating on releases without bumping NBGV. If you don't want a Python project at all, delete the `PyPiLibrary/` folder, the `build-pypilibrary-task.yml` workflow, the `build-pypilibrary` job in `build-release-task.yml`, the `publish-pypi` job in `publish-release.yml`, and the `uv` block in `.github/dependabot.yml`. diff --git a/PyPiLibrary/src/ptr727_projecttemplate_library/_version.py b/PyPiLibrary/src/ptr727_projecttemplate_library/_version.py index 66b584a9..65c7de50 100644 --- a/PyPiLibrary/src/ptr727_projecttemplate_library/_version.py +++ b/PyPiLibrary/src/ptr727_projecttemplate_library/_version.py @@ -1,8 +1,26 @@ """Single-source-of-truth for the package version. Hatchling reads ``__version__`` from this module via ``[tool.hatch.version]``. -For tag-driven versioning, swap this for ``hatch-vcs`` and configure the build -backend to derive the version from git tags. + +The ``0.0.0`` value below is a local-development placeholder so ``uv build`` +works outside CI. The release pipeline replaces the ``__version__`` line +(in place, preserving this docstring) with NBGV's ``AssemblyFileVersion`` +(``Major.Minor.Patch.BuildNumber`` — always numeric, PEP 440 valid) just +before ``uv build``, so the wheel and sdist uploaded to PyPI carry the same +version string that's stamped into the .NET assembly metadata as +``FileVersion``. .NET's ``AssemblyVersion`` (the binary-compat identity, a +separate NBGV output) and NuGet ``PackageVersion`` / Docker tags (NBGV +``SemVer2`` — PEP 440 doesn't accept its prerelease / build-metadata +suffixes) all carry different strings. All four artifacts still derive from +the same NBGV computation against ``version.json`` + git history and +correspond to the same release commit. See +``.github/workflows/build-pypilibrary-task.yml`` (the "Write version into +_version.py step"). + +If you fork this template and want a different versioning scheme, replace +both this file's contents and the workflow step that rewrites it. Two common +alternatives: ``hatch-vcs`` (git-tag-driven, no NBGV dependency) or manual +edits per release. """ __version__ = "0.0.0" diff --git a/README.md b/README.md index b732d7ae..27475790 100644 --- a/README.md +++ b/README.md @@ -442,51 +442,28 @@ Licensed under the [MIT License][license-link]\ - Save the PAT as `DOCKER_HUB_ACCESS_TOKEN` and `DOCKER_HUB_USERNAME` in: - GitHub project security Settings / Secrets / Actions. - GitHub project security Settings / Secrets / Dependabot. -- Create a [GitHub Personal Access Token](https://github.com/settings/personal-access-tokens). - - Save the PAT as `WORKFLOW_PAT`. - - Permissions: - - Pull requests: Read & write — to close and reopen the PR, triggering `pull_request` workflow events under the PAT owner's identity - - Workflows: Read & write — required for the PAT to trigger `pull_request` events in other workflows - - Metadata: Read-only (auto-required) - - The codegen workflow uses `GITHUB_TOKEN` to create a signed commit and open the PR as `github-actions[bot]`. It then uses `WORKFLOW_PAT` to close and reopen the PR so the `pull_request` event fires under the repository owner's identity (`github.repository_owner`), which triggers the auto-merge workflow. PRs created or updated by `GITHUB_TOKEN` alone do not trigger other workflows, hence the close/reopen step. - - The auto-merge condition in `merge-bot-pull-request.yml` requires all of the following to be true: - - `github.event.pull_request.user.login == 'github-actions[bot]'` — the PR was created by the Actions bot (via `GITHUB_TOKEN`) - - `github.event.pull_request.head.ref == 'codegen'` — the source branch is `codegen` - - `github.event.pull_request.base.ref == 'main'` — the PR targets `main` - - `github.event.pull_request.head.repo.full_name == github.repository` — the PR is from the same repository (not a fork) - - For `reopened` events: `github.actor == github.repository_owner` — the reopen was triggered by the repository owner account - - For all other events: `github.actor == 'github-actions[bot]'` — triggered by normal workflow activity - - Save the PAT as `WORKFLOW_PAT` in: - - GitHub project security Settings / Secrets / Actions. -- Create a [GitHub App](https://github.com/settings/apps) as an alternative to the PAT workflow. - - App name: `ptr727-codegen` - - The app bot user will be `ptr727-codegen[bot]`. - - Permissions required: - - Repository permissions: - - Contents: Read & write — to push commits to the `codegen` branch - - Pull requests: Read & write — to open and update pull requests - - Metadata: Read-only (auto-required) - - Note the App ID from the app's settings page. - - Generate a private key (downloads a `.pem` file). - - [Install the app](https://github.com/settings/apps) on your account and grant it access to the repository. The app must be both created **and** installed — creating the app alone is not sufficient. The `actions/create-github-app-token` action will fail with a `Not Found` error if the app is not installed on the repository. - - Save the App ID as `CODEGEN_APP_ID` and the private key contents as `CODEGEN_APP_PRIVATE_KEY` in: - - GitHub project security Settings / Secrets / Actions. - - If the codegen workflows require additional secrets (e.g. third-party API keys), register them in the same location and reference them in the reusable workflow task files. - - Unlike the PAT workflow, the GitHub App token triggers `pull_request` workflow events directly when opening a PR. No close/reopen step is required. - - The auto-merge condition in `merge-bot-pull-request.yml` for the app workflow requires all of the following to be true: - - `github.actor == 'ptr727-codegen[bot]'` — the event was triggered by the app - - `github.event.pull_request.user.login == 'ptr727-codegen[bot]'` — the PR was created by the app - - `github.event.pull_request.head.ref == 'codegen'` — the source branch is `codegen` - - `github.event.pull_request.base.ref == 'main'` — the PR targets `main` - - `github.event.pull_request.head.repo.full_name == github.repository` — the PR is from the same repository (not a fork) +- Create a [GitHub App](https://github.com/settings/apps) for the codegen and merge-bot workflows. + - App name: `ptr727-codegen`. Bot user: `ptr727-codegen[bot]`. + - Permissions required (repository scope): + - Contents: Read & write — push commits to the `codegen` branch and merge bot PRs. + - Pull requests: Read & write — open, update, and merge pull requests. + - Metadata: Read-only (auto-required). + - Note the App ID from the app's settings page; generate a private key (downloads a `.pem` file). + - [Install the app](https://github.com/settings/apps) on your account and grant it access to the repository. The app must be both created **and** installed — creating it alone is not sufficient (`actions/create-github-app-token` fails with `Not Found` if the app isn't installed on the repository). + - Save the App ID as `CODEGEN_APP_ID` and the private key contents as `CODEGEN_APP_PRIVATE_KEY` in **both** of: + - GitHub project security Settings / Secrets / Actions — for the codegen workflow and the codegen merge job. + - GitHub project security Settings / Secrets / Dependabot — **required** because Dependabot-triggered `pull_request` workflow runs use a separate, restricted secret context that doesn't see Actions secrets. Without the App secrets in the Dependabot store, the `merge-dependabot` job in `merge-bot-pull-request.yml` can't mint an App token and the PR will never auto-merge. + - If the codegen workflows require additional secrets (e.g. third-party API keys), register them in the Actions store; if a Dependabot-triggered workflow ever needs them, register them in the Dependabot store too. + - 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.event.pull_request.head.repo.full_name == github.repository` — PR is from this repo (not a fork). **Codegen workflow schedule**: -- The PAT-based codegen workflow (`run-periodic-codegen-pull-request.yml`) runs every **Monday** at 02:00 UTC. - - Uses `WORKFLOW_PAT` to close and reopen the PR after creation so that the `reopened` `pull_request` event is triggered by the repository owner account, which is required by the auto-merge condition (`github.actor == github.repository_owner`). Therefore, `WORKFLOW_PAT` must belong to the repository owner account. -- The App-based codegen workflow (`run-periodic-codegen-app-pull-request.yml`) runs every **Thursday** at 02:00 UTC. - - Uses `CODEGEN_APP_ID` and `CODEGEN_APP_PRIVATE_KEY` to generate a GitHub App installation token. The app token triggers `pull_request` events directly when the PR is opened — no close/reopen step needed. -- The two workflows alternate through the week as independent verification that both authentication paths continue to work. +- `run-periodic-codegen-pull-request.yml` runs every **Monday** at 02:00 UTC, plus on-demand via `workflow_dispatch`. It uses the App token (`CODEGEN_APP_ID` + `CODEGEN_APP_PRIVATE_KEY`) to commit, open the PR as `ptr727-codegen[bot]`, and let the merge-bot auto-merge once CI passes. No PAT, no close/reopen dance. **GitHub project settings**: From 4ddc6b9a5e6649f149f8ecf4fc5c21c219c122de Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 11 May 2026 13:30:08 -0700 Subject: [PATCH 2/3] Spell behavior, not behaviour, in merge-bot comment (#72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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](https://github.com/ptr727/ProjectTemplate/pull/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) --- .github/workflows/merge-bot-pull-request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 1a431ed5..536f4dbe 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -60,7 +60,7 @@ jobs: github-token: "${{ secrets.GITHUB_TOKEN }}" # Skip semver-major NuGet bumps: majors can build cleanly but break - # runtime behaviour, so they should land via human review. Other + # runtime behavior, so they should land via human review. Other # ecosystems' majors (github-actions, uv) are usually safe and merge. - name: Merge pull request step if: >- From b67737eab3ad8e9333650dfcc8e136a4a6525379 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 11 May 2026 13:47:47 -0700 Subject: [PATCH 3/3] Drop unnecessary secrets: inherit and lock codegen merge to App actor (#73) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Two additional Copilot findings on [PR #71](https://github.com/ptr727/ProjectTemplate/pull/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) --- .github/workflows/build-pypilibrary-task.yml | 1 - .github/workflows/merge-bot-pull-request.yml | 7 +++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-pypilibrary-task.yml b/.github/workflows/build-pypilibrary-task.yml index 2a483877..fdb05c08 100644 --- a/.github/workflows/build-pypilibrary-task.yml +++ b/.github/workflows/build-pypilibrary-task.yml @@ -22,7 +22,6 @@ jobs: get-version: name: Get version information job uses: ./.github/workflows/get-version-task.yml - secrets: inherit build-pypilibrary: name: Build PyPI library project job diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 536f4dbe..29d58ca7 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -86,8 +86,15 @@ jobs: 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. 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