From 18897899374cb7d3024e61126f43f4157f2a39b1 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 11 May 2026 18:00:38 -0700 Subject: [PATCH 1/7] Disable auto-merge on maintainer push to bot PR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #78 documented a limitation in the merge-bot's actor-check guardrail: it stopped the merge jobs from re-invoking `gh pr merge --auto` on a maintainer-triggered `synchronize`, but did NOT disable auto-merge that was already enabled by the initial bot-driven `opened` event. Once auto-merge was on a bot PR, any maintainer commit pushed to the bot's branch would be auto-merged when CI passed. The honest workaround was "remember to run `gh pr merge --disable-auto ` first" — fragile. This PR adds the real safeguard. Workflow changes (`.github/workflows/merge-bot-pull-request.yml`) - New job `disable-auto-merge-on-maintainer-push`. Fires on `pull_request.synchronize` events against bot-authored PRs (`dependabot[bot]` or `ptr727-codegen[bot]`) when the event actor is NOT the same bot. Calls `gh pr merge --disable-auto` (idempotent — safe to call repeatedly). Uses an App token because Dependabot PRs run the workflow with restricted secrets regardless of who triggered the event. - `merge-dependabot` and `merge-codegen` `if:` blocks now require `github.event.action == 'opened' || github.event.action == 'reopened'`. Skipping `synchronize` is what keeps the maintainer-triggered disable sticky — without this, the next bot-triggered `synchronize` (e.g. a Dependabot rebase) would re-enable auto-merge and undo the safeguard. - `merge-codegen` `github.actor == 'ptr727-codegen[bot]'` line dropped: it was a partial safeguard against the same case that the new disable job now handles properly. The remaining checks (PR author, head/base pairing, opened/reopened filter) are sufficient. - Header comment rewritten to describe the new three-job model (enable on open, disable on maintainer sync, dispatch method by base). Documentation - `AGENTS.md` Branching Model: new bullet describing the maintainer-push-disables-auto-merge invariant. - `README.md` Template - GitHub Setup: codegen auto-merge condition list now leads with the `opened`/`reopened` event filter, drops the obsolete `github.actor` warning that PR #78 had to soften, and adds a dedicated bullet for the `disable-auto-merge-on-maintainer-push` job. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/merge-bot-pull-request.yml | 125 +++++++++++++------ AGENTS.md | 1 + README.md | 3 +- 3 files changed, 90 insertions(+), 39 deletions(-) diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 7aaaf7c2..dae8a0c1 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -1,26 +1,37 @@ 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. +# Three-job model: +# 1. `merge-dependabot` / `merge-codegen` run on `opened` and `reopened` +# events only. They enable auto-merge via `gh pr merge --auto` once +# per PR. Restricting to open/reopen (skipping `synchronize`) is what +# makes step 3 below stick — if these jobs re-ran on every +# `synchronize`, they'd undo a maintainer-triggered disable. +# 2. The merge method (`--squash` vs `--merge`) is dispatched by a +# `case` statement on `pull_request.base.ref` so the form matches +# each branch's ruleset (develop = squash-only, main = merge-only, +# see AGENTS.md "Branching Model"). Both Dependabot and codegen +# open parallel PRs against both branches; Dependabot security +# updates always target `main` and flow through the same code path. +# 3. `disable-auto-merge-on-maintainer-push` runs on `synchronize` +# events against bot-authored PRs when the event actor is NOT the +# same bot — i.e. a maintainer pushed commits to a bot PR. It +# calls `gh pr merge --disable-auto` so the maintainer's commits +# don't auto-merge along with the bot's content. The maintainer +# re-enables auto-merge manually (UI or `gh pr merge --auto`) +# when ready. # -# 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". 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. +# Token strategy: +# Every job uses an App token (`actions/create-github-app-token`). +# The resulting push is committed by the App, which fires downstream +# workflows on develop and main. `GITHUB_TOKEN`-authored pushes 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. The disable job needs an App token too: +# even though the event actor is a maintainer, the workflow context +# on a Dependabot PR runs with Dependabot's restricted secrets +# regardless of actor, so plain `GITHUB_TOKEN` would be read-only. on: pull_request: @@ -35,11 +46,14 @@ 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. + # Restrict to Dependabot PRs that originate from this repository, not + # a fork. Only runs on `opened` / `reopened` events so the auto-merge + # enable happens once per PR; the `disable-auto-merge-on-maintainer-push` + # job below is what disables auto-merge when a maintainer pushes to a + # Dependabot branch. Skipping `synchronize` here is what keeps that + # disable sticky. if: >- + (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login == 'dependabot[bot]' && github.event.pull_request.head.repo.full_name == github.repository permissions: @@ -92,22 +106,14 @@ jobs: # 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. + # Only runs on `opened` / `reopened` events so the auto-merge enable + # happens once per PR; the `disable-auto-merge-on-maintainer-push` + # job below is what disables auto-merge when a maintainer pushes to a + # codegen branch. Skipping `synchronize` here is what keeps that + # disable sticky. if: >- + (github.event.action == 'opened' || github.event.action == 'reopened') && github.event.pull_request.user.login == 'ptr727-codegen[bot]' && - github.actor == 'ptr727-codegen[bot]' && 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') || @@ -141,3 +147,46 @@ jobs: env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ steps.app-token.outputs.token }} + + disable-auto-merge-on-maintainer-push: + name: Disable auto-merge on maintainer push job + runs-on: ubuntu-latest + # Fires on `synchronize` events against bot-authored PRs (Dependabot + # or codegen) when the event actor is NOT the same bot — i.e. a + # maintainer pushed commits to the bot's branch. Disables auto-merge + # so the maintainer's commits don't auto-merge along with the bot's + # content. The maintainer re-enables auto-merge manually when ready + # (UI button, or `gh pr merge --auto `). + # + # `gh pr merge --disable-auto` is idempotent — calling it on a PR + # that already has auto-merge disabled is a no-op. + if: >- + github.event.action == 'synchronize' && + github.event.pull_request.head.repo.full_name == github.repository && + ( + github.event.pull_request.user.login == 'dependabot[bot]' || + github.event.pull_request.user.login == 'ptr727-codegen[bot]' + ) && + github.actor != github.event.pull_request.user.login + permissions: + pull-requests: write + + steps: + + - name: Generate GitHub App token step + # App token rather than GITHUB_TOKEN: on a Dependabot PR the + # workflow context runs with Dependabot's restricted secrets + # regardless of who triggered the event (GitHub gates by PR + # origin, not by event actor), and the restricted GITHUB_TOKEN + # is read-only. Same App token pattern as the other merge jobs. + 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: Disable auto-merge step + run: gh pr merge --disable-auto "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/AGENTS.md b/AGENTS.md index cd737d41..ef94c7d0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,7 @@ Treat this file as authoritative for everything else; don't restate its rules el - 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. - **`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. +- **Maintainer-pushed commits on a bot PR auto-disable auto-merge.** The merge-bot's `merge-dependabot` and `merge-codegen` jobs only fire on `opened` / `reopened` events (auto-merge is enabled exactly once per PR). When a maintainer pushes commits to a bot's branch (a `synchronize` event with an actor that isn't the same bot), the merge-bot's `disable-auto-merge-on-maintainer-push` job fires and calls `gh pr merge --disable-auto`. The maintainer's commits stay in the PR but won't auto-merge with the bot's content; re-enable auto-merge manually (`gh pr merge --auto ` or the GitHub UI) when ready. - **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 9c5e6584..271eb94f 100644 --- a/README.md +++ b/README.md @@ -456,10 +456,11 @@ Licensed under the [MIT License][license-link]\ - 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: + - **Event is `opened` or `reopened`** — auto-merge is enabled once per PR at open time; subsequent `synchronize` events do not re-enable. This is what lets the `disable-auto-merge-on-maintainer-push` safeguard (below) stick. - `github.event.pull_request.user.login == 'ptr727-codegen[bot]'` — PR was opened by the App. - - `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. + - The `disable-auto-merge-on-maintainer-push` job in `merge-bot-pull-request.yml` runs on `synchronize` events against bot-authored PRs (Dependabot or codegen) when the event actor is NOT the same bot — i.e. a maintainer pushed commits. It calls `gh pr merge --disable-auto` so the maintainer's commits don't auto-merge along with the bot's content. Re-enable auto-merge manually (`gh pr merge --auto ` or the GitHub UI) when ready. 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. From 7b330e70861801a308574fb792720b703acf362d Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 11 May 2026 18:06:41 -0700 Subject: [PATCH 2/7] Disable cancel-in-progress to serialize merge-bot runs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review on PR #79 caught a race: with `cancel-in-progress: true` and our new `opened`/`reopened` filter on the merge-enable jobs, a fast follow-up `synchronize` (e.g. Dependabot rebase right after PR open) would cancel the in-flight `opened` run before it reached `gh pr merge --auto`. The new `synchronize` run skips the enable jobs (per the new filter), so auto-merge would never be enabled on the PR. Fix: `cancel-in-progress: false`. Runs in the same concurrency group queue instead of cancel, so opened completes (enables auto-merge), then synchronize runs in arrival order (disables if maintainer, no-ops if bot). End state is deterministic regardless of event arrival timing. Considered Copilot's other suggestion (include `github.event.action` in the concurrency group): that introduces its own race — if opened runs longer than a maintainer synchronize, opened would re-enable auto-merge AFTER the disable job already disabled it. Single group with cancel disabled avoids both races. Header comment block updated to explain why the cancel setting is load-bearing here. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/merge-bot-pull-request.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index dae8a0c1..1e91d6d1 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -37,9 +37,22 @@ on: pull_request: types: [opened, reopened, synchronize] +# `cancel-in-progress: false` is load-bearing. The three-job model +# (enable on opened/reopened, disable on maintainer-triggered +# synchronize) relies on those events running to completion in arrival +# order. With cancel-in-progress: true, a fast follow-up synchronize +# (e.g. a Dependabot rebase right after PR open) would cancel the +# in-flight `opened` run before it reached `gh pr merge --auto`, and +# the new synchronize run skips the enable jobs (opened/reopened +# filter), leaving auto-merge never enabled. Queueing instead of +# cancelling makes the final state deterministic: opened enables, +# then any subsequent synchronize disables (if maintainer) or no-ops +# (if bot). Action-aware grouping has its own race (opened finishing +# after a maintainer synchronize would re-enable auto-merge), so we +# keep a single group and just disable cancellation. concurrency: group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: From 5daf12fcec979bc2335dee24a8b88de102b0bf38 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 11 May 2026 18:21:33 -0700 Subject: [PATCH 3/7] SHA-pin all first-party actions and tighten AGENTS.md pinning rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review on PR #79 flagged the unpinned `actions/create-github- app-token@v1` and I initially declined, citing AGENTS.md's clause that "first-party `actions/*` are encouraged but not required" to be SHA-pinned. The maintainer corrected: that softening was meant narrowly for `dotnet/nbgv@master` (where tag-tracking would propose a downgrade), not as a blanket first-party exemption. Every other action must be SHA-pinned. Rule change in AGENTS.md "Workflow YAML Conventions": - Old: third-party actions must be SHA-pinned; first-party `actions/*` are encouraged but not required. - New: every action (first- or third-party) must be SHA-pinned. The only documented exception is `dotnet/nbgv@master`, whose rationale is recorded inline in get-version-task.yml. - Also updated the meta-note from "Don't open a PR purely to apply these rules across the repo" to "Sweep PRs that apply a rule everywhere are welcome when a rule changes" — which is exactly what this commit does. Sweep applied to every workflow file: - `actions/checkout@v6` → `de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2` - `actions/setup-dotnet@v5` → `c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0` - `actions/create-github-app-token@v1` → `d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0` (4 occurrences across merge-bot + run-codegen) - `actions/upload-artifact@v6` → `b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0` - `actions/download-artifact@v7` → `37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0` Every SHA is the current target of the floating tag it replaces, so behaviour is unchanged at the moment of the pin; only the defence-in-depth against tag retargeting is added. Future bumps come through Dependabot's GitHub Actions ecosystem (already configured for both `main` and `develop` per PR #78). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/build-datebadge-task.yml | 54 ++--- .github/workflows/build-docker-task.yml | 134 ++++++------ .github/workflows/build-executable-task.yml | 156 +++++++------- .github/workflows/build-nugetlibrary-task.yml | 138 ++++++------ .github/workflows/build-pypilibrary-task.yml | 4 +- .github/workflows/build-release-task.yml | 196 +++++++++--------- .github/workflows/get-version-task.yml | 116 +++++------ .github/workflows/merge-bot-pull-request.yml | 6 +- .../publish-periodic-docker-release.yml | 58 +++--- .github/workflows/publish-release.yml | 168 +++++++-------- .../run-codegen-pull-request-task.yml | 6 +- .github/workflows/test-pull-request.yml | 74 +++---- .github/workflows/test-release-task.yml | 88 ++++---- AGENTS.md | 4 +- 14 files changed, 601 insertions(+), 601 deletions(-) diff --git a/.github/workflows/build-datebadge-task.yml b/.github/workflows/build-datebadge-task.yml index 6f12a28a..fc3355a6 100644 --- a/.github/workflows/build-datebadge-task.yml +++ b/.github/workflows/build-datebadge-task.yml @@ -1,27 +1,27 @@ -name: Build BYOB date badge task - -on: - workflow_call: - -jobs: - - date-badge: - name: Build BYOB date badge job - runs-on: ubuntu-latest - - steps: - - - name: Get current date step - id: date - run: echo "date=$(date)" >> $GITHUB_OUTPUT - - - name: Build BYOB date badge step - if: ${{ github.ref_name == 'main' }} - uses: RubbaBoy/BYOB@v1 - with: - name: lastbuild - label: "Last Build" - icon: "github" - status: ${{ steps.date.outputs.date }} - color: "blue" - github_token: ${{ secrets.GITHUB_TOKEN }} +name: Build BYOB date badge task + +on: + workflow_call: + +jobs: + + date-badge: + name: Build BYOB date badge job + runs-on: ubuntu-latest + + steps: + + - name: Get current date step + id: date + run: echo "date=$(date)" >> $GITHUB_OUTPUT + + - name: Build BYOB date badge step + if: ${{ github.ref_name == 'main' }} + uses: RubbaBoy/BYOB@v1 + with: + name: lastbuild + label: "Last Build" + icon: "github" + status: ${{ steps.date.outputs.date }} + color: "blue" + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index f43a9022..a379c291 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -1,67 +1,67 @@ -name: Build Docker image task - -on: - workflow_call: - inputs: - # Input to control whether to push the Docker image to Docker Hub - push: - required: false - type: boolean - default: false - -jobs: - - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - secrets: inherit - - build-docker: - name: Build Docker image job - runs-on: ubuntu-latest - needs: [get-version] - - steps: - - - name: Checkout step - uses: actions/checkout@v6 - - - name: Setup QEMU step - uses: docker/setup-qemu-action@v3 - with: - platforms: linux/amd64,linux/arm64 - - - name: Setup Buildx step - uses: docker/setup-buildx-action@v3 - with: - platforms: linux/amd64,linux/arm64 - - # Always login to Docker Hub, not just on push, to benefit from - # higher rate limits with a Docker subscription for pulls and cache - - name: Login to Docker Hub step - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Docker build and push step - uses: docker/build-push-action@v6 - with: - context: . - push: ${{ inputs.push }} - file: ./Docker/Dockerfile - tags: | - docker.io/ptr727/projecttemplate:${{ github.ref_name == 'main' && 'latest' || 'develop' }} - docker.io/ptr727/projecttemplate:${{ needs.get-version.outputs.SemVer2 }} - platforms: linux/amd64,linux/arm64 - cache-from: type=registry,ref=docker.io/ptr727/projecttemplate:buildcache - cache-to: type=registry,ref=docker.io/ptr727/projecttemplate:buildcache,mode=max - build-args: | - LABEL_VERSION=${{ needs.get-version.outputs.SemVer2 }} - BUILD_CONFIGURATION=${{ github.ref_name == 'main' && 'Release' || 'Debug' }} - BUILD_VERSION=${{ needs.get-version.outputs.AssemblyVersion }} - BUILD_FILE_VERSION=${{ needs.get-version.outputs.AssemblyFileVersion }} - BUILD_ASSEMBLY_VERSION=${{ needs.get-version.outputs.AssemblyVersion }} - BUILD_INFORMATION_VERSION=${{ needs.get-version.outputs.AssemblyInformationalVersion }} - BUILD_PACKAGE_VERSION=${{ needs.get-version.outputs.SemVer2 }} - +name: Build Docker image task + +on: + workflow_call: + inputs: + # Input to control whether to push the Docker image to Docker Hub + push: + required: false + type: boolean + default: false + +jobs: + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + + build-docker: + name: Build Docker image job + runs-on: ubuntu-latest + needs: [get-version] + + steps: + + - name: Checkout step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup QEMU step + uses: docker/setup-qemu-action@v3 + with: + platforms: linux/amd64,linux/arm64 + + - name: Setup Buildx step + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64,linux/arm64 + + # Always login to Docker Hub, not just on push, to benefit from + # higher rate limits with a Docker subscription for pulls and cache + - name: Login to Docker Hub step + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Docker build and push step + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ inputs.push }} + file: ./Docker/Dockerfile + tags: | + docker.io/ptr727/projecttemplate:${{ github.ref_name == 'main' && 'latest' || 'develop' }} + docker.io/ptr727/projecttemplate:${{ needs.get-version.outputs.SemVer2 }} + platforms: linux/amd64,linux/arm64 + cache-from: type=registry,ref=docker.io/ptr727/projecttemplate:buildcache + cache-to: type=registry,ref=docker.io/ptr727/projecttemplate:buildcache,mode=max + build-args: | + LABEL_VERSION=${{ needs.get-version.outputs.SemVer2 }} + BUILD_CONFIGURATION=${{ github.ref_name == 'main' && 'Release' || 'Debug' }} + BUILD_VERSION=${{ needs.get-version.outputs.AssemblyVersion }} + BUILD_FILE_VERSION=${{ needs.get-version.outputs.AssemblyFileVersion }} + BUILD_ASSEMBLY_VERSION=${{ needs.get-version.outputs.AssemblyVersion }} + BUILD_INFORMATION_VERSION=${{ needs.get-version.outputs.AssemblyInformationalVersion }} + BUILD_PACKAGE_VERSION=${{ needs.get-version.outputs.SemVer2 }} + diff --git a/.github/workflows/build-executable-task.yml b/.github/workflows/build-executable-task.yml index 9ea6dfd3..27e0268e 100644 --- a/.github/workflows/build-executable-task.yml +++ b/.github/workflows/build-executable-task.yml @@ -1,78 +1,78 @@ -name: Build executable task - -on: - workflow_call: - outputs: - # Output of the uploaded artifact id - artifact-id: - value: ${{ jobs.upload-build-artifacts.outputs.artifact-id }} - -jobs: - - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - secrets: inherit - - build-executable-matrix: - name: Build executable project matrix job - runs-on: ubuntu-latest - needs: [get-version] - strategy: - matrix: - runtime: [ win-x64, linux-x64, linux-musl-x64, linux-arm, linux-arm64, osx-x64, osx-arm64 ] - - steps: - - - name: Setup .NET SDK step - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.x - - - name: Checkout code step - uses: actions/checkout@v6 - - - name: Build executable project step - run: | - dotnet publish ./Console/Console.csproj \ - --runtime ${{ matrix.runtime }} \ - -property:PublishDir=${{ runner.temp }}/publish/${{ matrix.runtime }}/ \ - --configuration ${{ github.ref_name == 'main' && 'Release' || 'Debug' }} \ - -property:PublishAot=false \ - -property:Version=${{ needs.get-version.outputs.AssemblyVersion }} \ - -property:FileVersion=${{ needs.get-version.outputs.AssemblyFileVersion }} \ - -property:AssemblyVersion=${{ needs.get-version.outputs.AssemblyVersion }} \ - -property:InformationalVersion=${{ needs.get-version.outputs.AssemblyInformationalVersion }} \ - -property:PackageVersion=${{ needs.get-version.outputs.SemVer2 }} - - - name: Upload matrix build artifacts step - uses: actions/upload-artifact@v6 - with: - name: publish-${{ matrix.runtime }} - path: ${{ runner.temp }}/publish - - upload-build-artifacts: - name: Upload matrix build artifacts job - outputs: - artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }} - runs-on: ubuntu-latest - needs: [ build-executable-matrix ] - - steps: - - - name: Download matrix build artifacts step - uses: actions/download-artifact@v7 - with: - pattern: publish-* - merge-multiple: true - path: ${{ runner.temp }}/publish - - - name: Zip build output step - run: 7z a -t7z ${{ runner.temp }}/Console.7z ${{ runner.temp }}/publish/* - - - name: Upload build artifacts step - id: artifact-upload-step - uses: actions/upload-artifact@v6 - with: - name: executable-build - path: ${{ runner.temp }}/Console.7z +name: Build executable task + +on: + workflow_call: + outputs: + # Output of the uploaded artifact id + artifact-id: + value: ${{ jobs.upload-build-artifacts.outputs.artifact-id }} + +jobs: + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + + build-executable-matrix: + name: Build executable project matrix job + runs-on: ubuntu-latest + needs: [get-version] + strategy: + matrix: + runtime: [ win-x64, linux-x64, linux-musl-x64, linux-arm, linux-arm64, osx-x64, osx-arm64 ] + + steps: + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Build executable project step + run: | + dotnet publish ./Console/Console.csproj \ + --runtime ${{ matrix.runtime }} \ + -property:PublishDir=${{ runner.temp }}/publish/${{ matrix.runtime }}/ \ + --configuration ${{ github.ref_name == 'main' && 'Release' || 'Debug' }} \ + -property:PublishAot=false \ + -property:Version=${{ needs.get-version.outputs.AssemblyVersion }} \ + -property:FileVersion=${{ needs.get-version.outputs.AssemblyFileVersion }} \ + -property:AssemblyVersion=${{ needs.get-version.outputs.AssemblyVersion }} \ + -property:InformationalVersion=${{ needs.get-version.outputs.AssemblyInformationalVersion }} \ + -property:PackageVersion=${{ needs.get-version.outputs.SemVer2 }} + + - name: Upload matrix build artifacts step + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: publish-${{ matrix.runtime }} + path: ${{ runner.temp }}/publish + + upload-build-artifacts: + name: Upload matrix build artifacts job + outputs: + artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }} + runs-on: ubuntu-latest + needs: [ build-executable-matrix ] + + steps: + + - name: Download matrix build artifacts step + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + pattern: publish-* + merge-multiple: true + path: ${{ runner.temp }}/publish + + - name: Zip build output step + run: 7z a -t7z ${{ runner.temp }}/Console.7z ${{ runner.temp }}/publish/* + + - name: Upload build artifacts step + id: artifact-upload-step + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: executable-build + path: ${{ runner.temp }}/Console.7z diff --git a/.github/workflows/build-nugetlibrary-task.yml b/.github/workflows/build-nugetlibrary-task.yml index a0b9413f..8c1b983c 100644 --- a/.github/workflows/build-nugetlibrary-task.yml +++ b/.github/workflows/build-nugetlibrary-task.yml @@ -1,69 +1,69 @@ -name: Build NuGet library task - -on: - workflow_call: - inputs: - # Input to control whether to push the NuGet library to NuGet.org - push: - required: false - type: boolean - default: false - outputs: - # Output of the uploaded artifact id - artifact-id: - value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }} - -jobs: - - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - - build-nugetlibrary: - name: Build NuGet library project job - runs-on: ubuntu-latest - outputs: - artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }} - needs: [get-version] - - steps: - - - name: Setup .NET SDK step - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.x - - - name: Checkout code step - uses: actions/checkout@v6 - - - name: Build NuGet library project step - run: | - set -euo pipefail - dotnet build ./NuGetLibrary/NuGetLibrary.csproj \ - -property:OutputPath=${{ runner.temp }}/publish/ \ - -property:PackageOutputPath=${{ runner.temp }}/publish/ \ - --configuration ${{ github.ref_name == 'main' && 'Release' || 'Debug' }} \ - -property:Version=${{ needs.get-version.outputs.AssemblyVersion }} \ - -property:FileVersion=${{ needs.get-version.outputs.AssemblyFileVersion }} \ - -property:AssemblyVersion=${{ needs.get-version.outputs.AssemblyVersion }} \ - -property:InformationalVersion=${{ needs.get-version.outputs.AssemblyInformationalVersion }} \ - -property:PackageVersion=${{ needs.get-version.outputs.SemVer2 }} - - - name: Publish to NuGet.org step - if: ${{ inputs.push }} - run: | - set -euo pipefail - dotnet nuget push ${{ runner.temp }}/publish/*.nupkg \ - --source https://api.nuget.org/v3/index.json \ - --api-key ${{ secrets.NUGET_API_KEY }} \ - --skip-duplicate - - - name: Zip output step - run: 7z a -t7z ${{ runner.temp }}/NuGetLibrary.7z ${{ runner.temp }}/publish/* - - - name: Upload build artifacts step - id: artifact-upload-step - uses: actions/upload-artifact@v6 - with: - name: nugetlibrary-build - path: ${{ runner.temp }}/NuGetLibrary.7z +name: Build NuGet library task + +on: + workflow_call: + inputs: + # Input to control whether to push the NuGet library to NuGet.org + push: + required: false + type: boolean + default: false + outputs: + # Output of the uploaded artifact id + artifact-id: + value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }} + +jobs: + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + + build-nugetlibrary: + name: Build NuGet library project job + runs-on: ubuntu-latest + outputs: + artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }} + needs: [get-version] + + steps: + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Build NuGet library project step + run: | + set -euo pipefail + dotnet build ./NuGetLibrary/NuGetLibrary.csproj \ + -property:OutputPath=${{ runner.temp }}/publish/ \ + -property:PackageOutputPath=${{ runner.temp }}/publish/ \ + --configuration ${{ github.ref_name == 'main' && 'Release' || 'Debug' }} \ + -property:Version=${{ needs.get-version.outputs.AssemblyVersion }} \ + -property:FileVersion=${{ needs.get-version.outputs.AssemblyFileVersion }} \ + -property:AssemblyVersion=${{ needs.get-version.outputs.AssemblyVersion }} \ + -property:InformationalVersion=${{ needs.get-version.outputs.AssemblyInformationalVersion }} \ + -property:PackageVersion=${{ needs.get-version.outputs.SemVer2 }} + + - name: Publish to NuGet.org step + if: ${{ inputs.push }} + run: | + set -euo pipefail + dotnet nuget push ${{ runner.temp }}/publish/*.nupkg \ + --source https://api.nuget.org/v3/index.json \ + --api-key ${{ secrets.NUGET_API_KEY }} \ + --skip-duplicate + + - name: Zip output step + run: 7z a -t7z ${{ runner.temp }}/NuGetLibrary.7z ${{ runner.temp }}/publish/* + + - name: Upload build artifacts step + id: artifact-upload-step + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: nugetlibrary-build + path: ${{ runner.temp }}/NuGetLibrary.7z diff --git a/.github/workflows/build-pypilibrary-task.yml b/.github/workflows/build-pypilibrary-task.yml index 60e926e4..4ecd0ab4 100644 --- a/.github/workflows/build-pypilibrary-task.yml +++ b/.github/workflows/build-pypilibrary-task.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Checkout code step - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup uv step uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 @@ -143,7 +143,7 @@ jobs: - name: Upload build artifacts step id: artifact-upload-step - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: pypilibrary-build path: PyPiLibrary/dist/* diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index bf5d90c8..12b289b9 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -1,98 +1,98 @@ -name: Build project release task - -on: - workflow_call: - inputs: - # Input to control whether to create a GitHub release - github: - required: false - type: boolean - default: false - # Input to control whether to push the library to NuGet.org - nuget: - required: false - type: boolean - default: false - # Input to control whether to push the docker image to Docker Hub - dockerhub: - required: false - type: boolean - default: false - -jobs: - - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - secrets: inherit - - build-nugetlibrary: - name: Build NuGet library job - uses: ./.github/workflows/build-nugetlibrary-task.yml - secrets: inherit - with: - # Conditional push to NuGet.org - push: ${{ inputs.nuget }} - - # PyPI publishing happens in `publish-release.yml`, not here, so that - # `id-token: write` only needs to be granted at the entry-point job. - # This reusable workflow just builds and uploads the artifact; the - # publish-release workflow downloads it by name in a sibling job. - build-pypilibrary: - name: Build PyPI library job - uses: ./.github/workflows/build-pypilibrary-task.yml - secrets: inherit - - build-executable: - name: Build executable job - uses: ./.github/workflows/build-executable-task.yml - secrets: inherit - - build-docker: - name: Build Docker job - uses: ./.github/workflows/build-docker-task.yml - secrets: inherit - with: - # Conditional push to Docker Hub - push: ${{ inputs.dockerhub }} - - github-release: - name: Publish GitHub release job - if: ${{ inputs.github }} - runs-on: ubuntu-latest - needs: [get-version, build-nugetlibrary, build-pypilibrary, build-executable, build-docker] - - steps: - - - name: Checkout code step - uses: actions/checkout@v6 - - - name: Download NuGet library build artifacts step - uses: actions/download-artifact@v7 - with: - artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }} - path: ./Publish - - - name: Download executable build artifacts step - uses: actions/download-artifact@v7 - with: - artifact-ids: ${{ needs.build-executable.outputs.artifact-id }} - path: ./Publish - - # `target_commitish` MUST be set explicitly: softprops doesn't pass a - # default through, and GitHub's REST API then defaults the new tag to - # the repository's default branch (main). On `push: develop` runs the - # tag would land on main's tip instead of the develop commit that - # built the artifact, leaving "Browse files" and `git checkout ` - # pointing at unrelated code. - - name: Create GitHub release step - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 - with: - generate_release_notes: true - tag_name: ${{ needs.get-version.outputs.SemVer2 }} - target_commitish: ${{ github.sha }} - prerelease: ${{ github.ref_name != 'main' }} - files: | - LICENSE - README.md - ./Publish/* +name: Build project release task + +on: + workflow_call: + inputs: + # Input to control whether to create a GitHub release + github: + required: false + type: boolean + default: false + # Input to control whether to push the library to NuGet.org + nuget: + required: false + type: boolean + default: false + # Input to control whether to push the docker image to Docker Hub + dockerhub: + required: false + type: boolean + default: false + +jobs: + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + + build-nugetlibrary: + name: Build NuGet library job + uses: ./.github/workflows/build-nugetlibrary-task.yml + secrets: inherit + with: + # Conditional push to NuGet.org + push: ${{ inputs.nuget }} + + # PyPI publishing happens in `publish-release.yml`, not here, so that + # `id-token: write` only needs to be granted at the entry-point job. + # This reusable workflow just builds and uploads the artifact; the + # publish-release workflow downloads it by name in a sibling job. + build-pypilibrary: + name: Build PyPI library job + uses: ./.github/workflows/build-pypilibrary-task.yml + secrets: inherit + + build-executable: + name: Build executable job + uses: ./.github/workflows/build-executable-task.yml + secrets: inherit + + build-docker: + name: Build Docker job + uses: ./.github/workflows/build-docker-task.yml + secrets: inherit + with: + # Conditional push to Docker Hub + push: ${{ inputs.dockerhub }} + + github-release: + name: Publish GitHub release job + if: ${{ inputs.github }} + runs-on: ubuntu-latest + needs: [get-version, build-nugetlibrary, build-pypilibrary, build-executable, build-docker] + + steps: + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Download NuGet library build artifacts step + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }} + path: ./Publish + + - name: Download executable build artifacts step + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + artifact-ids: ${{ needs.build-executable.outputs.artifact-id }} + path: ./Publish + + # `target_commitish` MUST be set explicitly: softprops doesn't pass a + # default through, and GitHub's REST API then defaults the new tag to + # the repository's default branch (main). On `push: develop` runs the + # tag would land on main's tip instead of the develop commit that + # built the artifact, leaving "Browse files" and `git checkout ` + # pointing at unrelated code. + - name: Create GitHub release step + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 + with: + generate_release_notes: true + tag_name: ${{ needs.get-version.outputs.SemVer2 }} + target_commitish: ${{ github.sha }} + prerelease: ${{ github.ref_name != 'main' }} + files: | + LICENSE + README.md + ./Publish/* diff --git a/.github/workflows/get-version-task.yml b/.github/workflows/get-version-task.yml index 2677cc9f..88f35e3b 100644 --- a/.github/workflows/get-version-task.yml +++ b/.github/workflows/get-version-task.yml @@ -1,58 +1,58 @@ -name: Get version information task - -on: - workflow_call: - outputs: - # Version information outputs - SemVer2: - value: ${{ jobs.get-version.outputs.SemVer2 }} - AssemblyVersion: - value: ${{ jobs.get-version.outputs.AssemblyVersion }} - AssemblyFileVersion: - value: ${{ jobs.get-version.outputs.AssemblyFileVersion }} - AssemblyInformationalVersion: - value: ${{ jobs.get-version.outputs.AssemblyInformationalVersion }} - -jobs: - - get-version: - name: Get version information job - runs-on: ubuntu-latest - outputs: - SemVer2: ${{ steps.nbgv.outputs.SemVer2 }} - AssemblyVersion: ${{ steps.nbgv.outputs.AssemblyVersion }} - AssemblyFileVersion: ${{ steps.nbgv.outputs.AssemblyFileVersion }} - AssemblyInformationalVersion: ${{ steps.nbgv.outputs.AssemblyInformationalVersion }} - - 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: - 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 +name: Get version information task + +on: + workflow_call: + outputs: + # Version information outputs + SemVer2: + value: ${{ jobs.get-version.outputs.SemVer2 }} + AssemblyVersion: + value: ${{ jobs.get-version.outputs.AssemblyVersion }} + AssemblyFileVersion: + value: ${{ jobs.get-version.outputs.AssemblyFileVersion }} + AssemblyInformationalVersion: + value: ${{ jobs.get-version.outputs.AssemblyInformationalVersion }} + +jobs: + + get-version: + name: Get version information job + runs-on: ubuntu-latest + outputs: + SemVer2: ${{ steps.nbgv.outputs.SemVer2 }} + AssemblyVersion: ${{ steps.nbgv.outputs.AssemblyVersion }} + AssemblyFileVersion: ${{ steps.nbgv.outputs.AssemblyFileVersion }} + AssemblyInformationalVersion: ${{ steps.nbgv.outputs.AssemblyInformationalVersion }} + + steps: + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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 diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 1e91d6d1..4b04b30c 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -77,7 +77,7 @@ jobs: - name: Generate GitHub App token step id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 with: app-id: ${{ secrets.CODEGEN_APP_ID }} private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} @@ -140,7 +140,7 @@ jobs: - name: Generate GitHub App token step id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 with: app-id: ${{ secrets.CODEGEN_APP_ID }} private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} @@ -193,7 +193,7 @@ jobs: # origin, not by event actor), and the restricted GITHUB_TOKEN # is read-only. Same App token pattern as the other merge jobs. id: app-token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 with: app-id: ${{ secrets.CODEGEN_APP_ID }} private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} diff --git a/.github/workflows/publish-periodic-docker-release.yml b/.github/workflows/publish-periodic-docker-release.yml index a264c592..9865ae8e 100644 --- a/.github/workflows/publish-periodic-docker-release.yml +++ b/.github/workflows/publish-periodic-docker-release.yml @@ -1,29 +1,29 @@ -name: Publish weekly Docker image to Docker Hub action - -on: - workflow_dispatch: - schedule: - # Run weekly on Mondays at 02:00 UTC - - cron: '0 2 * * MON' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - build-docker: - name: Build Docker image job - uses: ./.github/workflows/build-docker-task.yml - secrets: inherit - with: - # Push to registry - push: true - - date-badge: - name: Create BYOB date badge job - needs: [build-docker] - uses: ./.github/workflows/build-datebadge-task.yml - secrets: inherit - permissions: - contents: write +name: Publish weekly Docker image to Docker Hub action + +on: + workflow_dispatch: + schedule: + # Run weekly on Mondays at 02:00 UTC + - cron: '0 2 * * MON' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + build-docker: + name: Build Docker image job + uses: ./.github/workflows/build-docker-task.yml + secrets: inherit + with: + # Push to registry + push: true + + date-badge: + name: Create BYOB date badge job + needs: [build-docker] + uses: ./.github/workflows/build-datebadge-task.yml + secrets: inherit + permissions: + contents: write diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 63842a37..449f1cac 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,84 +1,84 @@ -name: Publish project release action - -on: - push: - branches: [ main, develop ] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - create-release: - name: Publish project release job - uses: ./.github/workflows/build-release-task.yml - secrets: inherit - permissions: - contents: write - with: - # Push to GitHub and NuGet and Docker Hub - github: true - nuget: true - dockerhub: true - - publish-pypi: - name: Publish PyPI library job - # Runs on pushes to both `main` and `develop`. `build-pypilibrary-task.yml` - # picks the PEP 440 version per branch (`M.N.P.B` release on main, - # `M.N.P.B.dev0` on develop — BuildNumber stays in the release segment - # so develop's release segment grows past main's per commit). Default - # `pip install ` filters `.dev0` and picks the main release; - # `pip install --pre ` includes dev releases and picks develop's - # higher release segment. Matches how NuGet/Docker tag develop builds - # as prerelease via NBGV `SemVer2`. - # The `pypi` GitHub environment's Deployment branch rule - # (Settings → Environments → pypi) restricts uploads to `main` + - # `develop` as defense in depth — see PyPiLibrary/README.md. - needs: [create-release] - runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/project/ptr727-projecttemplate-library/ - # When a `permissions:` block is present, every scope not listed - # collapses to `none`. The job needs three things explicitly: - # - `id-token: write` for Trusted Publishing's OIDC exchange - # (pypa/gh-action-pypi-publish swaps the token for a short-lived - # PyPI upload token; no PYPI_API_TOKEN secret involved). - # - `contents: read` so `actions/checkout`-style operations and any - # repo metadata reads continue to work. - # - `actions: read` so `actions/download-artifact` can list and - # fetch the artifact uploaded by the build workflow earlier in - # the same run. - permissions: - id-token: write - contents: read - actions: read - - steps: - - - name: Download PyPI library build artifacts step - uses: actions/download-artifact@v7 - with: - name: pypilibrary-build - path: ./dist - - - name: Publish to PyPI step - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - with: - packages-dir: ./dist - # Skip rather than fail when the version already exists on PyPI. - # The template ships with `__version__ = "0.0.0"` as a placeholder - # — the release-on-every-push model would otherwise re-upload the - # same version and fail the workflow until the adopter wires a - # real version scheme (see PyPiLibrary/README.md). - skip-existing: true - - date-badge: - name: Create BYOB date badge job - needs: [create-release] - uses: ./.github/workflows/build-datebadge-task.yml - secrets: inherit - permissions: - contents: write +name: Publish project release action + +on: + push: + branches: [ main, develop ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + create-release: + name: Publish project release job + uses: ./.github/workflows/build-release-task.yml + secrets: inherit + permissions: + contents: write + with: + # Push to GitHub and NuGet and Docker Hub + github: true + nuget: true + dockerhub: true + + publish-pypi: + name: Publish PyPI library job + # Runs on pushes to both `main` and `develop`. `build-pypilibrary-task.yml` + # picks the PEP 440 version per branch (`M.N.P.B` release on main, + # `M.N.P.B.dev0` on develop — BuildNumber stays in the release segment + # so develop's release segment grows past main's per commit). Default + # `pip install ` filters `.dev0` and picks the main release; + # `pip install --pre ` includes dev releases and picks develop's + # higher release segment. Matches how NuGet/Docker tag develop builds + # as prerelease via NBGV `SemVer2`. + # The `pypi` GitHub environment's Deployment branch rule + # (Settings → Environments → pypi) restricts uploads to `main` + + # `develop` as defense in depth — see PyPiLibrary/README.md. + needs: [create-release] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/ptr727-projecttemplate-library/ + # When a `permissions:` block is present, every scope not listed + # collapses to `none`. The job needs three things explicitly: + # - `id-token: write` for Trusted Publishing's OIDC exchange + # (pypa/gh-action-pypi-publish swaps the token for a short-lived + # PyPI upload token; no PYPI_API_TOKEN secret involved). + # - `contents: read` so `actions/checkout`-style operations and any + # repo metadata reads continue to work. + # - `actions: read` so `actions/download-artifact` can list and + # fetch the artifact uploaded by the build workflow earlier in + # the same run. + permissions: + id-token: write + contents: read + actions: read + + steps: + + - name: Download PyPI library build artifacts step + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: pypilibrary-build + path: ./dist + + - name: Publish to PyPI step + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + with: + packages-dir: ./dist + # Skip rather than fail when the version already exists on PyPI. + # The template ships with `__version__ = "0.0.0"` as a placeholder + # — the release-on-every-push model would otherwise re-upload the + # same version and fail the workflow until the adopter wires a + # real version scheme (see PyPiLibrary/README.md). + skip-existing: true + + date-badge: + name: Create BYOB date badge job + needs: [create-release] + uses: ./.github/workflows/build-datebadge-task.yml + secrets: inherit + permissions: + contents: write diff --git a/.github/workflows/run-codegen-pull-request-task.yml b/.github/workflows/run-codegen-pull-request-task.yml index 4e284c34..bf82e3c2 100644 --- a/.github/workflows/run-codegen-pull-request-task.yml +++ b/.github/workflows/run-codegen-pull-request-task.yml @@ -48,18 +48,18 @@ jobs: # 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 + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 with: app-id: ${{ secrets.CODEGEN_APP_ID }} private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} - name: Setup .NET SDK step - uses: actions/setup-dotnet@v5 + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 with: dotnet-version: 10.x - name: Checkout code step - uses: actions/checkout@v6 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: ref: ${{ matrix.target.ref }} token: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index dd87da69..4d261c5b 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -1,37 +1,37 @@ -name: Test pull request action - -on: - pull_request: - branches: [ main, develop, codegen ] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - test-release: - name: Test release job - uses: ./.github/workflows/test-release-task.yml - secrets: inherit - - # TODO: Workaround for GitHub Actions not supporting status checks on conditional jobs - # https://github.com/orgs/community/discussions/12395#discussioncomment-12970019 - check-workflow-status: - name: Check pull request workflow status - runs-on: ubuntu-latest - needs: - [ test-release ] - if: always() - steps: - - name: Check workflow results step - run: | - set -euo pipefail - exit_on_result() { - if [[ "$2" == "failure" || "$2" == "cancelled" ]]; then - echo "Job '$1' failed or was cancelled." - exit 1 - fi - } - exit_on_result "test-release" "${{ needs.test-release.result }}" +name: Test pull request action + +on: + pull_request: + branches: [ main, develop, codegen ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + test-release: + name: Test release job + uses: ./.github/workflows/test-release-task.yml + secrets: inherit + + # TODO: Workaround for GitHub Actions not supporting status checks on conditional jobs + # https://github.com/orgs/community/discussions/12395#discussioncomment-12970019 + check-workflow-status: + name: Check pull request workflow status + runs-on: ubuntu-latest + needs: + [ test-release ] + if: always() + steps: + - name: Check workflow results step + run: | + set -euo pipefail + exit_on_result() { + if [[ "$2" == "failure" || "$2" == "cancelled" ]]; then + echo "Job '$1' failed or was cancelled." + exit 1 + fi + } + exit_on_result "test-release" "${{ needs.test-release.result }}" diff --git a/.github/workflows/test-release-task.yml b/.github/workflows/test-release-task.yml index d58bbb58..e3fdc78e 100644 --- a/.github/workflows/test-release-task.yml +++ b/.github/workflows/test-release-task.yml @@ -1,44 +1,44 @@ -name: Test release task - -on: - workflow_call: - workflow_dispatch: - -jobs: - - unit-test: - name: Run unit tests job - runs-on: ubuntu-latest - - steps: - - - name: Setup .NET SDK step - uses: actions/setup-dotnet@v5 - with: - dotnet-version: 10.x - - - name: Checkout code step - uses: actions/checkout@v6 - - - name: Restore .NET local tools step - run: dotnet tool restore - - - name: Check formatting with CSharpier step - run: dotnet csharpier check . - - - name: Verify .NET style with dotnet format step - run: dotnet format style --verify-no-changes --severity=info --verbosity=detailed - - - name: Run unit tests step - run: dotnet test - - build-release: - name: Build release without publishing job - needs: [unit-test] - uses: ./.github/workflows/build-release-task.yml - secrets: inherit - with: - # Do not publish - github: false - nuget: false - dockerhub: false +name: Test release task + +on: + workflow_call: + workflow_dispatch: + +jobs: + + unit-test: + name: Run unit tests job + runs-on: ubuntu-latest + + steps: + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Restore .NET local tools step + run: dotnet tool restore + + - name: Check formatting with CSharpier step + run: dotnet csharpier check . + + - name: Verify .NET style with dotnet format step + run: dotnet format style --verify-no-changes --severity=info --verbosity=detailed + + - name: Run unit tests step + run: dotnet test + + build-release: + name: Build release without publishing job + needs: [unit-test] + uses: ./.github/workflows/build-release-task.yml + secrets: inherit + with: + # Do not publish + github: false + nuget: false + dockerhub: false diff --git a/AGENTS.md b/AGENTS.md index ef94c7d0..fa7cc87e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -109,9 +109,9 @@ Anti-pattern: don't keep flipping the code on the same style point. Flip the rul ## Workflow YAML Conventions -These conventions describe the target state. New and modified workflows must respect them; existing workflows are migrated opportunistically when they're being touched for other reasons. Don't open a PR purely to apply these rules across the repo — the churn isn't worth it. +These conventions describe the target state. New and modified workflows must respect them; the rest of the repo is expected to be brought up to the same standard. Sweep PRs that apply a rule everywhere are welcome when a rule changes. -- **Action pinning**: pin third-party actions to a commit SHA with a trailing `# vX.Y.Z` comment so Renovate / Dependabot can still bump it but a tag swap can't change the executed code. First-party `actions/*` are encouraged but not required to follow the same convention. +- **Action pinning**: pin **every** action — first-party (`actions/*`) and third-party — to a commit SHA with a trailing `# vX.Y.Z` comment, so Renovate / Dependabot can still bump it but a tag swap can't change the executed code. Documented exception: [`dotnet/nbgv`](./.github/workflows/get-version-task.yml) is consumed via `@master` because the upstream tag stream lags `master` substantially and Dependabot's tag-tracking would propose a downgrade — the rationale is documented inline in that workflow. - **Filename**: reusable workflows (those with `on: workflow_call`) end in `-task.yml`. Entry-point workflows (`on: push` / `pull_request` / `schedule` / `workflow_dispatch`) do NOT use the `-task` suffix; they end with what they do — `-pull-request.yml`, `-release.yml`, etc. The suffix carries semantic meaning: a `-task.yml` file is meant to be `uses:`-d, never triggered directly. - **Workflow `name:`** (the top-level `name:` field): reusable workflow names end in **"task"** (e.g. `Build PyPI library task`); entry-point workflow names end in **"action"** (e.g. `Publish project release action`, `Test pull request action`). The displayed action name in the GitHub Actions UI tells you at a glance whether you're looking at an orchestrator or a callee. - **Job and step `name:` suffixes**: every job's `name:` ends in **"job"**; every step's `name:` ends in **"step"**. **Exception**: a job whose `name:` is also referenced as a required-status-check `context:` in a branch ruleset (currently `Check pull request workflow status` in `test-pull-request.yml`) keeps the ruleset-bound name verbatim — renaming would silently break required-status-check enforcement. Do not "fix" that name; if a future job becomes ruleset-bound, mark it the same way. From 94e6b8aeb4e2e7bf5b2892e15f1265f66935a457 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 11 May 2026 18:23:07 -0700 Subject: [PATCH 4/7] Restore CRLF line endings on workflow files The previous SHA-pin sweep used `sed -i` which rewrote every workflow file with LF endings. The repo's `.gitattributes` has `* -text` (no line-ending normalization), so the original files were stored as CRLF and the LF rewrite showed up as an additional ~600 line-ending churn alongside the actual SHA pins. Convert the files back to CRLF. Once this PR squash-merges to develop, the cumulative diff is only the SHA pins (the LF detour cancels itself out). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/build-datebadge-task.yml | 54 +-- .github/workflows/build-docker-task.yml | 134 +++--- .github/workflows/build-executable-task.yml | 156 +++---- .github/workflows/build-nugetlibrary-task.yml | 138 +++--- .github/workflows/build-pypilibrary-task.yml | 298 ++++++------- .github/workflows/build-release-task.yml | 196 ++++----- .github/workflows/get-version-task.yml | 116 ++--- .github/workflows/merge-bot-pull-request.yml | 410 +++++++++--------- .../publish-periodic-docker-release.yml | 58 +-- .github/workflows/publish-release.yml | 168 +++---- .../run-codegen-pull-request-task.yml | 186 ++++---- .../run-periodic-codegen-pull-request.yml | 44 +- .github/workflows/test-pull-request.yml | 74 ++-- .github/workflows/test-release-task.yml | 88 ++-- 14 files changed, 1060 insertions(+), 1060 deletions(-) diff --git a/.github/workflows/build-datebadge-task.yml b/.github/workflows/build-datebadge-task.yml index fc3355a6..6f12a28a 100644 --- a/.github/workflows/build-datebadge-task.yml +++ b/.github/workflows/build-datebadge-task.yml @@ -1,27 +1,27 @@ -name: Build BYOB date badge task - -on: - workflow_call: - -jobs: - - date-badge: - name: Build BYOB date badge job - runs-on: ubuntu-latest - - steps: - - - name: Get current date step - id: date - run: echo "date=$(date)" >> $GITHUB_OUTPUT - - - name: Build BYOB date badge step - if: ${{ github.ref_name == 'main' }} - uses: RubbaBoy/BYOB@v1 - with: - name: lastbuild - label: "Last Build" - icon: "github" - status: ${{ steps.date.outputs.date }} - color: "blue" - github_token: ${{ secrets.GITHUB_TOKEN }} +name: Build BYOB date badge task + +on: + workflow_call: + +jobs: + + date-badge: + name: Build BYOB date badge job + runs-on: ubuntu-latest + + steps: + + - name: Get current date step + id: date + run: echo "date=$(date)" >> $GITHUB_OUTPUT + + - name: Build BYOB date badge step + if: ${{ github.ref_name == 'main' }} + uses: RubbaBoy/BYOB@v1 + with: + name: lastbuild + label: "Last Build" + icon: "github" + status: ${{ steps.date.outputs.date }} + color: "blue" + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index a379c291..ea54c290 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -1,67 +1,67 @@ -name: Build Docker image task - -on: - workflow_call: - inputs: - # Input to control whether to push the Docker image to Docker Hub - push: - required: false - type: boolean - default: false - -jobs: - - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - secrets: inherit - - build-docker: - name: Build Docker image job - runs-on: ubuntu-latest - needs: [get-version] - - steps: - - - name: Checkout step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Setup QEMU step - uses: docker/setup-qemu-action@v3 - with: - platforms: linux/amd64,linux/arm64 - - - name: Setup Buildx step - uses: docker/setup-buildx-action@v3 - with: - platforms: linux/amd64,linux/arm64 - - # Always login to Docker Hub, not just on push, to benefit from - # higher rate limits with a Docker subscription for pulls and cache - - name: Login to Docker Hub step - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Docker build and push step - uses: docker/build-push-action@v6 - with: - context: . - push: ${{ inputs.push }} - file: ./Docker/Dockerfile - tags: | - docker.io/ptr727/projecttemplate:${{ github.ref_name == 'main' && 'latest' || 'develop' }} - docker.io/ptr727/projecttemplate:${{ needs.get-version.outputs.SemVer2 }} - platforms: linux/amd64,linux/arm64 - cache-from: type=registry,ref=docker.io/ptr727/projecttemplate:buildcache - cache-to: type=registry,ref=docker.io/ptr727/projecttemplate:buildcache,mode=max - build-args: | - LABEL_VERSION=${{ needs.get-version.outputs.SemVer2 }} - BUILD_CONFIGURATION=${{ github.ref_name == 'main' && 'Release' || 'Debug' }} - BUILD_VERSION=${{ needs.get-version.outputs.AssemblyVersion }} - BUILD_FILE_VERSION=${{ needs.get-version.outputs.AssemblyFileVersion }} - BUILD_ASSEMBLY_VERSION=${{ needs.get-version.outputs.AssemblyVersion }} - BUILD_INFORMATION_VERSION=${{ needs.get-version.outputs.AssemblyInformationalVersion }} - BUILD_PACKAGE_VERSION=${{ needs.get-version.outputs.SemVer2 }} - +name: Build Docker image task + +on: + workflow_call: + inputs: + # Input to control whether to push the Docker image to Docker Hub + push: + required: false + type: boolean + default: false + +jobs: + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + + build-docker: + name: Build Docker image job + runs-on: ubuntu-latest + needs: [get-version] + + steps: + + - name: Checkout step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup QEMU step + uses: docker/setup-qemu-action@v3 + with: + platforms: linux/amd64,linux/arm64 + + - name: Setup Buildx step + uses: docker/setup-buildx-action@v3 + with: + platforms: linux/amd64,linux/arm64 + + # Always login to Docker Hub, not just on push, to benefit from + # higher rate limits with a Docker subscription for pulls and cache + - name: Login to Docker Hub step + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_HUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Docker build and push step + uses: docker/build-push-action@v6 + with: + context: . + push: ${{ inputs.push }} + file: ./Docker/Dockerfile + tags: | + docker.io/ptr727/projecttemplate:${{ github.ref_name == 'main' && 'latest' || 'develop' }} + docker.io/ptr727/projecttemplate:${{ needs.get-version.outputs.SemVer2 }} + platforms: linux/amd64,linux/arm64 + cache-from: type=registry,ref=docker.io/ptr727/projecttemplate:buildcache + cache-to: type=registry,ref=docker.io/ptr727/projecttemplate:buildcache,mode=max + build-args: | + LABEL_VERSION=${{ needs.get-version.outputs.SemVer2 }} + BUILD_CONFIGURATION=${{ github.ref_name == 'main' && 'Release' || 'Debug' }} + BUILD_VERSION=${{ needs.get-version.outputs.AssemblyVersion }} + BUILD_FILE_VERSION=${{ needs.get-version.outputs.AssemblyFileVersion }} + BUILD_ASSEMBLY_VERSION=${{ needs.get-version.outputs.AssemblyVersion }} + BUILD_INFORMATION_VERSION=${{ needs.get-version.outputs.AssemblyInformationalVersion }} + BUILD_PACKAGE_VERSION=${{ needs.get-version.outputs.SemVer2 }} + diff --git a/.github/workflows/build-executable-task.yml b/.github/workflows/build-executable-task.yml index 27e0268e..c9724930 100644 --- a/.github/workflows/build-executable-task.yml +++ b/.github/workflows/build-executable-task.yml @@ -1,78 +1,78 @@ -name: Build executable task - -on: - workflow_call: - outputs: - # Output of the uploaded artifact id - artifact-id: - value: ${{ jobs.upload-build-artifacts.outputs.artifact-id }} - -jobs: - - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - secrets: inherit - - build-executable-matrix: - name: Build executable project matrix job - runs-on: ubuntu-latest - needs: [get-version] - strategy: - matrix: - runtime: [ win-x64, linux-x64, linux-musl-x64, linux-arm, linux-arm64, osx-x64, osx-arm64 ] - - steps: - - - name: Setup .NET SDK step - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 - with: - dotnet-version: 10.x - - - name: Checkout code step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Build executable project step - run: | - dotnet publish ./Console/Console.csproj \ - --runtime ${{ matrix.runtime }} \ - -property:PublishDir=${{ runner.temp }}/publish/${{ matrix.runtime }}/ \ - --configuration ${{ github.ref_name == 'main' && 'Release' || 'Debug' }} \ - -property:PublishAot=false \ - -property:Version=${{ needs.get-version.outputs.AssemblyVersion }} \ - -property:FileVersion=${{ needs.get-version.outputs.AssemblyFileVersion }} \ - -property:AssemblyVersion=${{ needs.get-version.outputs.AssemblyVersion }} \ - -property:InformationalVersion=${{ needs.get-version.outputs.AssemblyInformationalVersion }} \ - -property:PackageVersion=${{ needs.get-version.outputs.SemVer2 }} - - - name: Upload matrix build artifacts step - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: publish-${{ matrix.runtime }} - path: ${{ runner.temp }}/publish - - upload-build-artifacts: - name: Upload matrix build artifacts job - outputs: - artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }} - runs-on: ubuntu-latest - needs: [ build-executable-matrix ] - - steps: - - - name: Download matrix build artifacts step - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - with: - pattern: publish-* - merge-multiple: true - path: ${{ runner.temp }}/publish - - - name: Zip build output step - run: 7z a -t7z ${{ runner.temp }}/Console.7z ${{ runner.temp }}/publish/* - - - name: Upload build artifacts step - id: artifact-upload-step - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: executable-build - path: ${{ runner.temp }}/Console.7z +name: Build executable task + +on: + workflow_call: + outputs: + # Output of the uploaded artifact id + artifact-id: + value: ${{ jobs.upload-build-artifacts.outputs.artifact-id }} + +jobs: + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + + build-executable-matrix: + name: Build executable project matrix job + runs-on: ubuntu-latest + needs: [get-version] + strategy: + matrix: + runtime: [ win-x64, linux-x64, linux-musl-x64, linux-arm, linux-arm64, osx-x64, osx-arm64 ] + + steps: + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Build executable project step + run: | + dotnet publish ./Console/Console.csproj \ + --runtime ${{ matrix.runtime }} \ + -property:PublishDir=${{ runner.temp }}/publish/${{ matrix.runtime }}/ \ + --configuration ${{ github.ref_name == 'main' && 'Release' || 'Debug' }} \ + -property:PublishAot=false \ + -property:Version=${{ needs.get-version.outputs.AssemblyVersion }} \ + -property:FileVersion=${{ needs.get-version.outputs.AssemblyFileVersion }} \ + -property:AssemblyVersion=${{ needs.get-version.outputs.AssemblyVersion }} \ + -property:InformationalVersion=${{ needs.get-version.outputs.AssemblyInformationalVersion }} \ + -property:PackageVersion=${{ needs.get-version.outputs.SemVer2 }} + + - name: Upload matrix build artifacts step + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: publish-${{ matrix.runtime }} + path: ${{ runner.temp }}/publish + + upload-build-artifacts: + name: Upload matrix build artifacts job + outputs: + artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }} + runs-on: ubuntu-latest + needs: [ build-executable-matrix ] + + steps: + + - name: Download matrix build artifacts step + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + pattern: publish-* + merge-multiple: true + path: ${{ runner.temp }}/publish + + - name: Zip build output step + run: 7z a -t7z ${{ runner.temp }}/Console.7z ${{ runner.temp }}/publish/* + + - name: Upload build artifacts step + id: artifact-upload-step + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: executable-build + path: ${{ runner.temp }}/Console.7z diff --git a/.github/workflows/build-nugetlibrary-task.yml b/.github/workflows/build-nugetlibrary-task.yml index 8c1b983c..cfd39777 100644 --- a/.github/workflows/build-nugetlibrary-task.yml +++ b/.github/workflows/build-nugetlibrary-task.yml @@ -1,69 +1,69 @@ -name: Build NuGet library task - -on: - workflow_call: - inputs: - # Input to control whether to push the NuGet library to NuGet.org - push: - required: false - type: boolean - default: false - outputs: - # Output of the uploaded artifact id - artifact-id: - value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }} - -jobs: - - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - - build-nugetlibrary: - name: Build NuGet library project job - runs-on: ubuntu-latest - outputs: - artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }} - needs: [get-version] - - steps: - - - name: Setup .NET SDK step - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 - with: - dotnet-version: 10.x - - - name: Checkout code step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Build NuGet library project step - run: | - set -euo pipefail - dotnet build ./NuGetLibrary/NuGetLibrary.csproj \ - -property:OutputPath=${{ runner.temp }}/publish/ \ - -property:PackageOutputPath=${{ runner.temp }}/publish/ \ - --configuration ${{ github.ref_name == 'main' && 'Release' || 'Debug' }} \ - -property:Version=${{ needs.get-version.outputs.AssemblyVersion }} \ - -property:FileVersion=${{ needs.get-version.outputs.AssemblyFileVersion }} \ - -property:AssemblyVersion=${{ needs.get-version.outputs.AssemblyVersion }} \ - -property:InformationalVersion=${{ needs.get-version.outputs.AssemblyInformationalVersion }} \ - -property:PackageVersion=${{ needs.get-version.outputs.SemVer2 }} - - - name: Publish to NuGet.org step - if: ${{ inputs.push }} - run: | - set -euo pipefail - dotnet nuget push ${{ runner.temp }}/publish/*.nupkg \ - --source https://api.nuget.org/v3/index.json \ - --api-key ${{ secrets.NUGET_API_KEY }} \ - --skip-duplicate - - - name: Zip output step - run: 7z a -t7z ${{ runner.temp }}/NuGetLibrary.7z ${{ runner.temp }}/publish/* - - - name: Upload build artifacts step - id: artifact-upload-step - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: nugetlibrary-build - path: ${{ runner.temp }}/NuGetLibrary.7z +name: Build NuGet library task + +on: + workflow_call: + inputs: + # Input to control whether to push the NuGet library to NuGet.org + push: + required: false + type: boolean + default: false + outputs: + # Output of the uploaded artifact id + artifact-id: + value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }} + +jobs: + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + + build-nugetlibrary: + name: Build NuGet library project job + runs-on: ubuntu-latest + outputs: + artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }} + needs: [get-version] + + steps: + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Build NuGet library project step + run: | + set -euo pipefail + dotnet build ./NuGetLibrary/NuGetLibrary.csproj \ + -property:OutputPath=${{ runner.temp }}/publish/ \ + -property:PackageOutputPath=${{ runner.temp }}/publish/ \ + --configuration ${{ github.ref_name == 'main' && 'Release' || 'Debug' }} \ + -property:Version=${{ needs.get-version.outputs.AssemblyVersion }} \ + -property:FileVersion=${{ needs.get-version.outputs.AssemblyFileVersion }} \ + -property:AssemblyVersion=${{ needs.get-version.outputs.AssemblyVersion }} \ + -property:InformationalVersion=${{ needs.get-version.outputs.AssemblyInformationalVersion }} \ + -property:PackageVersion=${{ needs.get-version.outputs.SemVer2 }} + + - name: Publish to NuGet.org step + if: ${{ inputs.push }} + run: | + set -euo pipefail + dotnet nuget push ${{ runner.temp }}/publish/*.nupkg \ + --source https://api.nuget.org/v3/index.json \ + --api-key ${{ secrets.NUGET_API_KEY }} \ + --skip-duplicate + + - name: Zip output step + run: 7z a -t7z ${{ runner.temp }}/NuGetLibrary.7z ${{ runner.temp }}/publish/* + + - name: Upload build artifacts step + id: artifact-upload-step + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: nugetlibrary-build + path: ${{ runner.temp }}/NuGetLibrary.7z diff --git a/.github/workflows/build-pypilibrary-task.yml b/.github/workflows/build-pypilibrary-task.yml index 4ecd0ab4..7b92e201 100644 --- a/.github/workflows/build-pypilibrary-task.yml +++ b/.github/workflows/build-pypilibrary-task.yml @@ -1,149 +1,149 @@ -name: Build PyPI library task - -# This reusable workflow only builds the PyPI library and uploads the -# wheel + sdist as a workflow-run artifact. It does NOT publish to PyPI. -# Publishing happens directly in `publish-release.yml` so that the -# `id-token: write` permission required by Trusted Publishing is granted -# at the entry-point job, not propagated through a reusable-workflow -# chain (which would require every caller — including `test-release-task.yml` -# during PR validation — to also grant id-token write, even when no -# publishing happens). - -on: - workflow_call: - outputs: - artifact-name: - value: ${{ jobs.build-pypilibrary.outputs.artifact-name }} - artifact-id: - value: ${{ jobs.build-pypilibrary.outputs.artifact-id }} - -jobs: - - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - - build-pypilibrary: - name: Build PyPI library project job - runs-on: ubuntu-latest - needs: [get-version] - defaults: - run: - working-directory: ./PyPiLibrary - outputs: - artifact-name: pypilibrary-build - artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }} - - steps: - - - name: Checkout code step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Setup uv step - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - with: - # Pin uv to the same version as `.devcontainer/python/post-create.sh` - # (UV_VERSION) so CI and local devcontainer behavior cannot drift - # — same uv resolves the same lockfile the same way. Bump in lock- - # step with the devcontainer pin. - version: "0.11.8" - enable-cache: true - cache-dependency-glob: "PyPiLibrary/uv.lock" - - - name: Sync dependencies step - run: uv sync --all-groups --frozen - - - name: Lint with ruff step - run: uv run ruff check - - - name: Verify formatting with ruff step - run: uv run ruff format --check - - - name: Type check with pyright step - run: uv run pyright - - - name: Run pytest step - run: uv run pytest - - # Compute the PEP 440 version string for this build: - # refs/heads/main -> AssemblyFileVersion as-is - # (`Major.Minor.Patch.BuildNumber`). PEP 440 - # treats this 4-segment numeric form as a - # release. - # refs/heads/develop -> `${AssemblyFileVersion}.dev0` - # (`Major.Minor.Patch.BuildNumber.dev0`). - # The BuildNumber stays in the release - # segment so develop's release segment - # (which grows past main's after every - # new commit) compares higher than main's - # under PEP 440 ordering — so `pip install - # --pre ` picks the develop dev - # build, while default `pip install ` - # filters the dev suffix and picks the - # main release. The `.dev0` literal is a - # constant because BuildNumber alone - # already differentiates each develop - # push (NBGV BuildNumber increments per - # commit), so we don't need a second - # counter in the dev segment. - # - # Edge case: 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) — `--pre` will still resolve to - # the main release until a new develop - # commit lands. This is accepted as a - # small, self-healing gap. - # other refs (PR validation via test-release-task, feature - # branches) -> AssemblyFileVersion as-is. These - # refs never publish; we just need a PEP 440 valid - # string for `uv build`. - - name: Compute PyPI version step - id: pypiver - run: | - set -euo pipefail - if [[ "$GITHUB_REF" == "refs/heads/develop" ]]; then - version="${AFV}.dev0" - else - version="$AFV" - fi - echo "PyPI version for $GITHUB_REF: $version" - echo "version=$version" >> "$GITHUB_OUTPUT" - env: - AFV: ${{ needs.get-version.outputs.AssemblyFileVersion }} - - # Replace the `__version__` line in `_version.py` (which ships - # hardcoded "0.0.0" so local `uv build` works without CI) with the - # branch-aware PEP 440 version computed above. `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. On main, the PyPI version string equals the .NET - # assemblies' `FileVersion` stamp (= NBGV `AssemblyFileVersion`, - # `M.N.P.B`); on develop, the PyPI version is `M.N.P.B.dev0` — - # numerically the same `FileVersion` with a trailing `.dev0` - # prerelease marker. .NET keeps the bare `FileVersion`, and - # NuGet/Docker use NBGV `SemVer2`, so strings are not byte- - # identical across artifacts on either channel. All four still - # derive from the same NBGV computation per commit (main pushes - # publish release versions; develop pushes publish PEP 440 dev - # releases / NBGV prereleases). - - 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: ${{ steps.pypiver.outputs.version }} - - - name: Build sdist and wheel step - run: uv build - - - name: Upload build artifacts step - id: artifact-upload-step - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - with: - name: pypilibrary-build - path: PyPiLibrary/dist/* +name: Build PyPI library task + +# This reusable workflow only builds the PyPI library and uploads the +# wheel + sdist as a workflow-run artifact. It does NOT publish to PyPI. +# Publishing happens directly in `publish-release.yml` so that the +# `id-token: write` permission required by Trusted Publishing is granted +# at the entry-point job, not propagated through a reusable-workflow +# chain (which would require every caller — including `test-release-task.yml` +# during PR validation — to also grant id-token write, even when no +# publishing happens). + +on: + workflow_call: + outputs: + artifact-name: + value: ${{ jobs.build-pypilibrary.outputs.artifact-name }} + artifact-id: + value: ${{ jobs.build-pypilibrary.outputs.artifact-id }} + +jobs: + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + + build-pypilibrary: + name: Build PyPI library project job + runs-on: ubuntu-latest + needs: [get-version] + defaults: + run: + working-directory: ./PyPiLibrary + outputs: + artifact-name: pypilibrary-build + artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }} + + steps: + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup uv step + uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 + with: + # Pin uv to the same version as `.devcontainer/python/post-create.sh` + # (UV_VERSION) so CI and local devcontainer behavior cannot drift + # — same uv resolves the same lockfile the same way. Bump in lock- + # step with the devcontainer pin. + version: "0.11.8" + enable-cache: true + cache-dependency-glob: "PyPiLibrary/uv.lock" + + - name: Sync dependencies step + run: uv sync --all-groups --frozen + + - name: Lint with ruff step + run: uv run ruff check + + - name: Verify formatting with ruff step + run: uv run ruff format --check + + - name: Type check with pyright step + run: uv run pyright + + - name: Run pytest step + run: uv run pytest + + # Compute the PEP 440 version string for this build: + # refs/heads/main -> AssemblyFileVersion as-is + # (`Major.Minor.Patch.BuildNumber`). PEP 440 + # treats this 4-segment numeric form as a + # release. + # refs/heads/develop -> `${AssemblyFileVersion}.dev0` + # (`Major.Minor.Patch.BuildNumber.dev0`). + # The BuildNumber stays in the release + # segment so develop's release segment + # (which grows past main's after every + # new commit) compares higher than main's + # under PEP 440 ordering — so `pip install + # --pre ` picks the develop dev + # build, while default `pip install ` + # filters the dev suffix and picks the + # main release. The `.dev0` literal is a + # constant because BuildNumber alone + # already differentiates each develop + # push (NBGV BuildNumber increments per + # commit), so we don't need a second + # counter in the dev segment. + # + # Edge case: 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) — `--pre` will still resolve to + # the main release until a new develop + # commit lands. This is accepted as a + # small, self-healing gap. + # other refs (PR validation via test-release-task, feature + # branches) -> AssemblyFileVersion as-is. These + # refs never publish; we just need a PEP 440 valid + # string for `uv build`. + - name: Compute PyPI version step + id: pypiver + run: | + set -euo pipefail + if [[ "$GITHUB_REF" == "refs/heads/develop" ]]; then + version="${AFV}.dev0" + else + version="$AFV" + fi + echo "PyPI version for $GITHUB_REF: $version" + echo "version=$version" >> "$GITHUB_OUTPUT" + env: + AFV: ${{ needs.get-version.outputs.AssemblyFileVersion }} + + # Replace the `__version__` line in `_version.py` (which ships + # hardcoded "0.0.0" so local `uv build` works without CI) with the + # branch-aware PEP 440 version computed above. `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. On main, the PyPI version string equals the .NET + # assemblies' `FileVersion` stamp (= NBGV `AssemblyFileVersion`, + # `M.N.P.B`); on develop, the PyPI version is `M.N.P.B.dev0` — + # numerically the same `FileVersion` with a trailing `.dev0` + # prerelease marker. .NET keeps the bare `FileVersion`, and + # NuGet/Docker use NBGV `SemVer2`, so strings are not byte- + # identical across artifacts on either channel. All four still + # derive from the same NBGV computation per commit (main pushes + # publish release versions; develop pushes publish PEP 440 dev + # releases / NBGV prereleases). + - 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: ${{ steps.pypiver.outputs.version }} + + - name: Build sdist and wheel step + run: uv build + + - name: Upload build artifacts step + id: artifact-upload-step + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: pypilibrary-build + path: PyPiLibrary/dist/* diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index 12b289b9..cfe9b9ae 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -1,98 +1,98 @@ -name: Build project release task - -on: - workflow_call: - inputs: - # Input to control whether to create a GitHub release - github: - required: false - type: boolean - default: false - # Input to control whether to push the library to NuGet.org - nuget: - required: false - type: boolean - default: false - # Input to control whether to push the docker image to Docker Hub - dockerhub: - required: false - type: boolean - default: false - -jobs: - - get-version: - name: Get version information job - uses: ./.github/workflows/get-version-task.yml - secrets: inherit - - build-nugetlibrary: - name: Build NuGet library job - uses: ./.github/workflows/build-nugetlibrary-task.yml - secrets: inherit - with: - # Conditional push to NuGet.org - push: ${{ inputs.nuget }} - - # PyPI publishing happens in `publish-release.yml`, not here, so that - # `id-token: write` only needs to be granted at the entry-point job. - # This reusable workflow just builds and uploads the artifact; the - # publish-release workflow downloads it by name in a sibling job. - build-pypilibrary: - name: Build PyPI library job - uses: ./.github/workflows/build-pypilibrary-task.yml - secrets: inherit - - build-executable: - name: Build executable job - uses: ./.github/workflows/build-executable-task.yml - secrets: inherit - - build-docker: - name: Build Docker job - uses: ./.github/workflows/build-docker-task.yml - secrets: inherit - with: - # Conditional push to Docker Hub - push: ${{ inputs.dockerhub }} - - github-release: - name: Publish GitHub release job - if: ${{ inputs.github }} - runs-on: ubuntu-latest - needs: [get-version, build-nugetlibrary, build-pypilibrary, build-executable, build-docker] - - steps: - - - name: Checkout code step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Download NuGet library build artifacts step - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - with: - artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }} - path: ./Publish - - - name: Download executable build artifacts step - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - with: - artifact-ids: ${{ needs.build-executable.outputs.artifact-id }} - path: ./Publish - - # `target_commitish` MUST be set explicitly: softprops doesn't pass a - # default through, and GitHub's REST API then defaults the new tag to - # the repository's default branch (main). On `push: develop` runs the - # tag would land on main's tip instead of the develop commit that - # built the artifact, leaving "Browse files" and `git checkout ` - # pointing at unrelated code. - - name: Create GitHub release step - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 - with: - generate_release_notes: true - tag_name: ${{ needs.get-version.outputs.SemVer2 }} - target_commitish: ${{ github.sha }} - prerelease: ${{ github.ref_name != 'main' }} - files: | - LICENSE - README.md - ./Publish/* +name: Build project release task + +on: + workflow_call: + inputs: + # Input to control whether to create a GitHub release + github: + required: false + type: boolean + default: false + # Input to control whether to push the library to NuGet.org + nuget: + required: false + type: boolean + default: false + # Input to control whether to push the docker image to Docker Hub + dockerhub: + required: false + type: boolean + default: false + +jobs: + + get-version: + name: Get version information job + uses: ./.github/workflows/get-version-task.yml + secrets: inherit + + build-nugetlibrary: + name: Build NuGet library job + uses: ./.github/workflows/build-nugetlibrary-task.yml + secrets: inherit + with: + # Conditional push to NuGet.org + push: ${{ inputs.nuget }} + + # PyPI publishing happens in `publish-release.yml`, not here, so that + # `id-token: write` only needs to be granted at the entry-point job. + # This reusable workflow just builds and uploads the artifact; the + # publish-release workflow downloads it by name in a sibling job. + build-pypilibrary: + name: Build PyPI library job + uses: ./.github/workflows/build-pypilibrary-task.yml + secrets: inherit + + build-executable: + name: Build executable job + uses: ./.github/workflows/build-executable-task.yml + secrets: inherit + + build-docker: + name: Build Docker job + uses: ./.github/workflows/build-docker-task.yml + secrets: inherit + with: + # Conditional push to Docker Hub + push: ${{ inputs.dockerhub }} + + github-release: + name: Publish GitHub release job + if: ${{ inputs.github }} + runs-on: ubuntu-latest + needs: [get-version, build-nugetlibrary, build-pypilibrary, build-executable, build-docker] + + steps: + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Download NuGet library build artifacts step + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }} + path: ./Publish + + - name: Download executable build artifacts step + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + artifact-ids: ${{ needs.build-executable.outputs.artifact-id }} + path: ./Publish + + # `target_commitish` MUST be set explicitly: softprops doesn't pass a + # default through, and GitHub's REST API then defaults the new tag to + # the repository's default branch (main). On `push: develop` runs the + # tag would land on main's tip instead of the develop commit that + # built the artifact, leaving "Browse files" and `git checkout ` + # pointing at unrelated code. + - name: Create GitHub release step + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 + with: + generate_release_notes: true + tag_name: ${{ needs.get-version.outputs.SemVer2 }} + target_commitish: ${{ github.sha }} + prerelease: ${{ github.ref_name != 'main' }} + files: | + LICENSE + README.md + ./Publish/* diff --git a/.github/workflows/get-version-task.yml b/.github/workflows/get-version-task.yml index 88f35e3b..24e1a7ca 100644 --- a/.github/workflows/get-version-task.yml +++ b/.github/workflows/get-version-task.yml @@ -1,58 +1,58 @@ -name: Get version information task - -on: - workflow_call: - outputs: - # Version information outputs - SemVer2: - value: ${{ jobs.get-version.outputs.SemVer2 }} - AssemblyVersion: - value: ${{ jobs.get-version.outputs.AssemblyVersion }} - AssemblyFileVersion: - value: ${{ jobs.get-version.outputs.AssemblyFileVersion }} - AssemblyInformationalVersion: - value: ${{ jobs.get-version.outputs.AssemblyInformationalVersion }} - -jobs: - - get-version: - name: Get version information job - runs-on: ubuntu-latest - outputs: - SemVer2: ${{ steps.nbgv.outputs.SemVer2 }} - AssemblyVersion: ${{ steps.nbgv.outputs.AssemblyVersion }} - AssemblyFileVersion: ${{ steps.nbgv.outputs.AssemblyFileVersion }} - AssemblyInformationalVersion: ${{ steps.nbgv.outputs.AssemblyInformationalVersion }} - - steps: - - - name: Setup .NET SDK step - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 - with: - dotnet-version: 10.x - - - name: Checkout code step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - 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 +name: Get version information task + +on: + workflow_call: + outputs: + # Version information outputs + SemVer2: + value: ${{ jobs.get-version.outputs.SemVer2 }} + AssemblyVersion: + value: ${{ jobs.get-version.outputs.AssemblyVersion }} + AssemblyFileVersion: + value: ${{ jobs.get-version.outputs.AssemblyFileVersion }} + AssemblyInformationalVersion: + value: ${{ jobs.get-version.outputs.AssemblyInformationalVersion }} + +jobs: + + get-version: + name: Get version information job + runs-on: ubuntu-latest + outputs: + SemVer2: ${{ steps.nbgv.outputs.SemVer2 }} + AssemblyVersion: ${{ steps.nbgv.outputs.AssemblyVersion }} + AssemblyFileVersion: ${{ steps.nbgv.outputs.AssemblyFileVersion }} + AssemblyInformationalVersion: ${{ steps.nbgv.outputs.AssemblyInformationalVersion }} + + steps: + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + 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 diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 4b04b30c..2c274a13 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -1,205 +1,205 @@ -name: Merge bot pull request action - -# Three-job model: -# 1. `merge-dependabot` / `merge-codegen` run on `opened` and `reopened` -# events only. They enable auto-merge via `gh pr merge --auto` once -# per PR. Restricting to open/reopen (skipping `synchronize`) is what -# makes step 3 below stick — if these jobs re-ran on every -# `synchronize`, they'd undo a maintainer-triggered disable. -# 2. The merge method (`--squash` vs `--merge`) is dispatched by a -# `case` statement on `pull_request.base.ref` so the form matches -# each branch's ruleset (develop = squash-only, main = merge-only, -# see AGENTS.md "Branching Model"). Both Dependabot and codegen -# open parallel PRs against both branches; Dependabot security -# updates always target `main` and flow through the same code path. -# 3. `disable-auto-merge-on-maintainer-push` runs on `synchronize` -# events against bot-authored PRs when the event actor is NOT the -# same bot — i.e. a maintainer pushed commits to a bot PR. It -# calls `gh pr merge --disable-auto` so the maintainer's commits -# don't auto-merge along with the bot's content. The maintainer -# re-enables auto-merge manually (UI or `gh pr merge --auto`) -# when ready. -# -# Token strategy: -# Every job uses an App token (`actions/create-github-app-token`). -# The resulting push is committed by the App, which fires downstream -# workflows on develop and main. `GITHUB_TOKEN`-authored pushes 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. The disable job needs an App token too: -# even though the event actor is a maintainer, the workflow context -# on a Dependabot PR runs with Dependabot's restricted secrets -# regardless of actor, so plain `GITHUB_TOKEN` would be read-only. - -on: - pull_request: - types: [opened, reopened, synchronize] - -# `cancel-in-progress: false` is load-bearing. The three-job model -# (enable on opened/reopened, disable on maintainer-triggered -# synchronize) relies on those events running to completion in arrival -# order. With cancel-in-progress: true, a fast follow-up synchronize -# (e.g. a Dependabot rebase right after PR open) would cancel the -# in-flight `opened` run before it reached `gh pr merge --auto`, and -# the new synchronize run skips the enable jobs (opened/reopened -# filter), leaving auto-merge never enabled. Queueing instead of -# cancelling makes the final state deterministic: opened enables, -# then any subsequent synchronize disables (if maintainer) or no-ops -# (if bot). Action-aware grouping has its own race (opened finishing -# after a maintainer synchronize would re-enable auto-merge), so we -# keep a single group and just disable cancellation. -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: false - -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. Only runs on `opened` / `reopened` events so the auto-merge - # enable happens once per PR; the `disable-auto-merge-on-maintainer-push` - # job below is what disables auto-merge when a maintainer pushes to a - # Dependabot branch. Skipping `synchronize` here is what keeps that - # disable sticky. - if: >- - (github.event.action == 'opened' || github.event.action == 'reopened') && - 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@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 - 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 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: >- - (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 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`. - # Only runs on `opened` / `reopened` events so the auto-merge enable - # happens once per PR; the `disable-auto-merge-on-maintainer-push` - # job below is what disables auto-merge when a maintainer pushes to a - # codegen branch. Skipping `synchronize` here is what keeps that - # disable sticky. - if: >- - (github.event.action == 'opened' || github.event.action == 'reopened') && - github.event.pull_request.user.login == 'ptr727-codegen[bot]' && - 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 - - steps: - - - name: Generate GitHub App token step - id: app-token - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 - with: - app-id: ${{ secrets.CODEGEN_APP_ID }} - private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} - - - name: Merge pull request step - 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 }} - - disable-auto-merge-on-maintainer-push: - name: Disable auto-merge on maintainer push job - runs-on: ubuntu-latest - # Fires on `synchronize` events against bot-authored PRs (Dependabot - # or codegen) when the event actor is NOT the same bot — i.e. a - # maintainer pushed commits to the bot's branch. Disables auto-merge - # so the maintainer's commits don't auto-merge along with the bot's - # content. The maintainer re-enables auto-merge manually when ready - # (UI button, or `gh pr merge --auto `). - # - # `gh pr merge --disable-auto` is idempotent — calling it on a PR - # that already has auto-merge disabled is a no-op. - if: >- - github.event.action == 'synchronize' && - github.event.pull_request.head.repo.full_name == github.repository && - ( - github.event.pull_request.user.login == 'dependabot[bot]' || - github.event.pull_request.user.login == 'ptr727-codegen[bot]' - ) && - github.actor != github.event.pull_request.user.login - permissions: - pull-requests: write - - steps: - - - name: Generate GitHub App token step - # App token rather than GITHUB_TOKEN: on a Dependabot PR the - # workflow context runs with Dependabot's restricted secrets - # regardless of who triggered the event (GitHub gates by PR - # origin, not by event actor), and the restricted GITHUB_TOKEN - # is read-only. Same App token pattern as the other merge jobs. - id: app-token - uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 - with: - app-id: ${{ secrets.CODEGEN_APP_ID }} - private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} - - - name: Disable auto-merge step - run: gh pr merge --disable-auto "$PR_URL" - env: - PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ steps.app-token.outputs.token }} +name: Merge bot pull request action + +# Three-job model: +# 1. `merge-dependabot` / `merge-codegen` run on `opened` and `reopened` +# events only. They enable auto-merge via `gh pr merge --auto` once +# per PR. Restricting to open/reopen (skipping `synchronize`) is what +# makes step 3 below stick — if these jobs re-ran on every +# `synchronize`, they'd undo a maintainer-triggered disable. +# 2. The merge method (`--squash` vs `--merge`) is dispatched by a +# `case` statement on `pull_request.base.ref` so the form matches +# each branch's ruleset (develop = squash-only, main = merge-only, +# see AGENTS.md "Branching Model"). Both Dependabot and codegen +# open parallel PRs against both branches; Dependabot security +# updates always target `main` and flow through the same code path. +# 3. `disable-auto-merge-on-maintainer-push` runs on `synchronize` +# events against bot-authored PRs when the event actor is NOT the +# same bot — i.e. a maintainer pushed commits to a bot PR. It +# calls `gh pr merge --disable-auto` so the maintainer's commits +# don't auto-merge along with the bot's content. The maintainer +# re-enables auto-merge manually (UI or `gh pr merge --auto`) +# when ready. +# +# Token strategy: +# Every job uses an App token (`actions/create-github-app-token`). +# The resulting push is committed by the App, which fires downstream +# workflows on develop and main. `GITHUB_TOKEN`-authored pushes 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. The disable job needs an App token too: +# even though the event actor is a maintainer, the workflow context +# on a Dependabot PR runs with Dependabot's restricted secrets +# regardless of actor, so plain `GITHUB_TOKEN` would be read-only. + +on: + pull_request: + types: [opened, reopened, synchronize] + +# `cancel-in-progress: false` is load-bearing. The three-job model +# (enable on opened/reopened, disable on maintainer-triggered +# synchronize) relies on those events running to completion in arrival +# order. With cancel-in-progress: true, a fast follow-up synchronize +# (e.g. a Dependabot rebase right after PR open) would cancel the +# in-flight `opened` run before it reached `gh pr merge --auto`, and +# the new synchronize run skips the enable jobs (opened/reopened +# filter), leaving auto-merge never enabled. Queueing instead of +# cancelling makes the final state deterministic: opened enables, +# then any subsequent synchronize disables (if maintainer) or no-ops +# (if bot). Action-aware grouping has its own race (opened finishing +# after a maintainer synchronize would re-enable auto-merge), so we +# keep a single group and just disable cancellation. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +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. Only runs on `opened` / `reopened` events so the auto-merge + # enable happens once per PR; the `disable-auto-merge-on-maintainer-push` + # job below is what disables auto-merge when a maintainer pushes to a + # Dependabot branch. Skipping `synchronize` here is what keeps that + # disable sticky. + if: >- + (github.event.action == 'opened' || github.event.action == 'reopened') && + 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@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + 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 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: >- + (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 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`. + # Only runs on `opened` / `reopened` events so the auto-merge enable + # happens once per PR; the `disable-auto-merge-on-maintainer-push` + # job below is what disables auto-merge when a maintainer pushes to a + # codegen branch. Skipping `synchronize` here is what keeps that + # disable sticky. + if: >- + (github.event.action == 'opened' || github.event.action == 'reopened') && + github.event.pull_request.user.login == 'ptr727-codegen[bot]' && + 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 + + steps: + + - name: Generate GitHub App token step + id: app-token + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + with: + app-id: ${{ secrets.CODEGEN_APP_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Merge pull request step + 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 }} + + disable-auto-merge-on-maintainer-push: + name: Disable auto-merge on maintainer push job + runs-on: ubuntu-latest + # Fires on `synchronize` events against bot-authored PRs (Dependabot + # or codegen) when the event actor is NOT the same bot — i.e. a + # maintainer pushed commits to the bot's branch. Disables auto-merge + # so the maintainer's commits don't auto-merge along with the bot's + # content. The maintainer re-enables auto-merge manually when ready + # (UI button, or `gh pr merge --auto `). + # + # `gh pr merge --disable-auto` is idempotent — calling it on a PR + # that already has auto-merge disabled is a no-op. + if: >- + github.event.action == 'synchronize' && + github.event.pull_request.head.repo.full_name == github.repository && + ( + github.event.pull_request.user.login == 'dependabot[bot]' || + github.event.pull_request.user.login == 'ptr727-codegen[bot]' + ) && + github.actor != github.event.pull_request.user.login + permissions: + pull-requests: write + + steps: + + - name: Generate GitHub App token step + # App token rather than GITHUB_TOKEN: on a Dependabot PR the + # workflow context runs with Dependabot's restricted secrets + # regardless of who triggered the event (GitHub gates by PR + # origin, not by event actor), and the restricted GITHUB_TOKEN + # is read-only. Same App token pattern as the other merge jobs. + id: app-token + uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + with: + app-id: ${{ secrets.CODEGEN_APP_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Disable auto-merge step + run: gh pr merge --disable-auto "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/publish-periodic-docker-release.yml b/.github/workflows/publish-periodic-docker-release.yml index 9865ae8e..a264c592 100644 --- a/.github/workflows/publish-periodic-docker-release.yml +++ b/.github/workflows/publish-periodic-docker-release.yml @@ -1,29 +1,29 @@ -name: Publish weekly Docker image to Docker Hub action - -on: - workflow_dispatch: - schedule: - # Run weekly on Mondays at 02:00 UTC - - cron: '0 2 * * MON' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - build-docker: - name: Build Docker image job - uses: ./.github/workflows/build-docker-task.yml - secrets: inherit - with: - # Push to registry - push: true - - date-badge: - name: Create BYOB date badge job - needs: [build-docker] - uses: ./.github/workflows/build-datebadge-task.yml - secrets: inherit - permissions: - contents: write +name: Publish weekly Docker image to Docker Hub action + +on: + workflow_dispatch: + schedule: + # Run weekly on Mondays at 02:00 UTC + - cron: '0 2 * * MON' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + build-docker: + name: Build Docker image job + uses: ./.github/workflows/build-docker-task.yml + secrets: inherit + with: + # Push to registry + push: true + + date-badge: + name: Create BYOB date badge job + needs: [build-docker] + uses: ./.github/workflows/build-datebadge-task.yml + secrets: inherit + permissions: + contents: write diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 449f1cac..1821d717 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,84 +1,84 @@ -name: Publish project release action - -on: - push: - branches: [ main, develop ] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - create-release: - name: Publish project release job - uses: ./.github/workflows/build-release-task.yml - secrets: inherit - permissions: - contents: write - with: - # Push to GitHub and NuGet and Docker Hub - github: true - nuget: true - dockerhub: true - - publish-pypi: - name: Publish PyPI library job - # Runs on pushes to both `main` and `develop`. `build-pypilibrary-task.yml` - # picks the PEP 440 version per branch (`M.N.P.B` release on main, - # `M.N.P.B.dev0` on develop — BuildNumber stays in the release segment - # so develop's release segment grows past main's per commit). Default - # `pip install ` filters `.dev0` and picks the main release; - # `pip install --pre ` includes dev releases and picks develop's - # higher release segment. Matches how NuGet/Docker tag develop builds - # as prerelease via NBGV `SemVer2`. - # The `pypi` GitHub environment's Deployment branch rule - # (Settings → Environments → pypi) restricts uploads to `main` + - # `develop` as defense in depth — see PyPiLibrary/README.md. - needs: [create-release] - runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/project/ptr727-projecttemplate-library/ - # When a `permissions:` block is present, every scope not listed - # collapses to `none`. The job needs three things explicitly: - # - `id-token: write` for Trusted Publishing's OIDC exchange - # (pypa/gh-action-pypi-publish swaps the token for a short-lived - # PyPI upload token; no PYPI_API_TOKEN secret involved). - # - `contents: read` so `actions/checkout`-style operations and any - # repo metadata reads continue to work. - # - `actions: read` so `actions/download-artifact` can list and - # fetch the artifact uploaded by the build workflow earlier in - # the same run. - permissions: - id-token: write - contents: read - actions: read - - steps: - - - name: Download PyPI library build artifacts step - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 - with: - name: pypilibrary-build - path: ./dist - - - name: Publish to PyPI step - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - with: - packages-dir: ./dist - # Skip rather than fail when the version already exists on PyPI. - # The template ships with `__version__ = "0.0.0"` as a placeholder - # — the release-on-every-push model would otherwise re-upload the - # same version and fail the workflow until the adopter wires a - # real version scheme (see PyPiLibrary/README.md). - skip-existing: true - - date-badge: - name: Create BYOB date badge job - needs: [create-release] - uses: ./.github/workflows/build-datebadge-task.yml - secrets: inherit - permissions: - contents: write +name: Publish project release action + +on: + push: + branches: [ main, develop ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + create-release: + name: Publish project release job + uses: ./.github/workflows/build-release-task.yml + secrets: inherit + permissions: + contents: write + with: + # Push to GitHub and NuGet and Docker Hub + github: true + nuget: true + dockerhub: true + + publish-pypi: + name: Publish PyPI library job + # Runs on pushes to both `main` and `develop`. `build-pypilibrary-task.yml` + # picks the PEP 440 version per branch (`M.N.P.B` release on main, + # `M.N.P.B.dev0` on develop — BuildNumber stays in the release segment + # so develop's release segment grows past main's per commit). Default + # `pip install ` filters `.dev0` and picks the main release; + # `pip install --pre ` includes dev releases and picks develop's + # higher release segment. Matches how NuGet/Docker tag develop builds + # as prerelease via NBGV `SemVer2`. + # The `pypi` GitHub environment's Deployment branch rule + # (Settings → Environments → pypi) restricts uploads to `main` + + # `develop` as defense in depth — see PyPiLibrary/README.md. + needs: [create-release] + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/project/ptr727-projecttemplate-library/ + # When a `permissions:` block is present, every scope not listed + # collapses to `none`. The job needs three things explicitly: + # - `id-token: write` for Trusted Publishing's OIDC exchange + # (pypa/gh-action-pypi-publish swaps the token for a short-lived + # PyPI upload token; no PYPI_API_TOKEN secret involved). + # - `contents: read` so `actions/checkout`-style operations and any + # repo metadata reads continue to work. + # - `actions: read` so `actions/download-artifact` can list and + # fetch the artifact uploaded by the build workflow earlier in + # the same run. + permissions: + id-token: write + contents: read + actions: read + + steps: + + - name: Download PyPI library build artifacts step + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: pypilibrary-build + path: ./dist + + - name: Publish to PyPI step + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 + with: + packages-dir: ./dist + # Skip rather than fail when the version already exists on PyPI. + # The template ships with `__version__ = "0.0.0"` as a placeholder + # — the release-on-every-push model would otherwise re-upload the + # same version and fail the workflow until the adopter wires a + # real version scheme (see PyPiLibrary/README.md). + skip-existing: true + + date-badge: + name: Create BYOB date badge job + needs: [create-release] + uses: ./.github/workflows/build-datebadge-task.yml + secrets: inherit + permissions: + contents: write diff --git a/.github/workflows/run-codegen-pull-request-task.yml b/.github/workflows/run-codegen-pull-request-task.yml index bf82e3c2..daf3d675 100644 --- a/.github/workflows/run-codegen-pull-request-task.yml +++ b/.github/workflows/run-codegen-pull-request-task.yml @@ -1,93 +1,93 @@ -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: - # 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 ${{ 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: - - - 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@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 - with: - app-id: ${{ secrets.CODEGEN_APP_ID }} - private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} - - - name: Setup .NET SDK step - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 - with: - dotnet-version: 10.x - - - name: Checkout code step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - ref: ${{ matrix.target.ref }} - 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: ${{ matrix.target.ref }} - branch: ${{ matrix.target.branch }} - title: 'Update codegen files' - body: 'This PR updates the codegen files.' - commit-message: 'Update codegen files' - delete-branch: true - sign-commits: true +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: + # 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 ${{ 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: + + - 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@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0 + with: + app-id: ${{ secrets.CODEGEN_APP_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ matrix.target.ref }} + 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: ${{ matrix.target.ref }} + branch: ${{ matrix.target.branch }} + 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-pull-request.yml b/.github/workflows/run-periodic-codegen-pull-request.yml index 0623b651..c64eaa4b 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. - - 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/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 4d261c5b..dd87da69 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -1,37 +1,37 @@ -name: Test pull request action - -on: - pull_request: - branches: [ main, develop, codegen ] - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - - test-release: - name: Test release job - uses: ./.github/workflows/test-release-task.yml - secrets: inherit - - # TODO: Workaround for GitHub Actions not supporting status checks on conditional jobs - # https://github.com/orgs/community/discussions/12395#discussioncomment-12970019 - check-workflow-status: - name: Check pull request workflow status - runs-on: ubuntu-latest - needs: - [ test-release ] - if: always() - steps: - - name: Check workflow results step - run: | - set -euo pipefail - exit_on_result() { - if [[ "$2" == "failure" || "$2" == "cancelled" ]]; then - echo "Job '$1' failed or was cancelled." - exit 1 - fi - } - exit_on_result "test-release" "${{ needs.test-release.result }}" +name: Test pull request action + +on: + pull_request: + branches: [ main, develop, codegen ] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + + test-release: + name: Test release job + uses: ./.github/workflows/test-release-task.yml + secrets: inherit + + # TODO: Workaround for GitHub Actions not supporting status checks on conditional jobs + # https://github.com/orgs/community/discussions/12395#discussioncomment-12970019 + check-workflow-status: + name: Check pull request workflow status + runs-on: ubuntu-latest + needs: + [ test-release ] + if: always() + steps: + - name: Check workflow results step + run: | + set -euo pipefail + exit_on_result() { + if [[ "$2" == "failure" || "$2" == "cancelled" ]]; then + echo "Job '$1' failed or was cancelled." + exit 1 + fi + } + exit_on_result "test-release" "${{ needs.test-release.result }}" diff --git a/.github/workflows/test-release-task.yml b/.github/workflows/test-release-task.yml index e3fdc78e..bc36044d 100644 --- a/.github/workflows/test-release-task.yml +++ b/.github/workflows/test-release-task.yml @@ -1,44 +1,44 @@ -name: Test release task - -on: - workflow_call: - workflow_dispatch: - -jobs: - - unit-test: - name: Run unit tests job - runs-on: ubuntu-latest - - steps: - - - name: Setup .NET SDK step - uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 - with: - dotnet-version: 10.x - - - name: Checkout code step - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Restore .NET local tools step - run: dotnet tool restore - - - name: Check formatting with CSharpier step - run: dotnet csharpier check . - - - name: Verify .NET style with dotnet format step - run: dotnet format style --verify-no-changes --severity=info --verbosity=detailed - - - name: Run unit tests step - run: dotnet test - - build-release: - name: Build release without publishing job - needs: [unit-test] - uses: ./.github/workflows/build-release-task.yml - secrets: inherit - with: - # Do not publish - github: false - nuget: false - dockerhub: false +name: Test release task + +on: + workflow_call: + workflow_dispatch: + +jobs: + + unit-test: + name: Run unit tests job + runs-on: ubuntu-latest + + steps: + + - name: Setup .NET SDK step + uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0 + with: + dotnet-version: 10.x + + - name: Checkout code step + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Restore .NET local tools step + run: dotnet tool restore + + - name: Check formatting with CSharpier step + run: dotnet csharpier check . + + - name: Verify .NET style with dotnet format step + run: dotnet format style --verify-no-changes --severity=info --verbosity=detailed + + - name: Run unit tests step + run: dotnet test + + build-release: + name: Build release without publishing job + needs: [unit-test] + uses: ./.github/workflows/build-release-task.yml + secrets: inherit + with: + # Do not publish + github: false + nuget: false + dockerhub: false From 50e5852dc71471907476dd364c53d98fd9850f36 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 11 May 2026 18:30:33 -0700 Subject: [PATCH 5/7] Pin remaining docker/* and RubbaBoy/BYOB actions; note merge-bot concurrency exception in AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third Copilot pass on PR #79 caught two valid follow-on issues from the SHA-pin sweep in 5daf12f: 1. The first sweep only touched `actions/*` references but missed the other floating tags. Pinning those now: - RubbaBoy/BYOB@v1 -> a4919104bc0ec7cfd7f113e42c405cc45246f2a4 # v1 (the floating @v1 tag at this upstream doesn't match a specific v1.x release SHA; pinning to current floating-tag SHA so behaviour is unchanged and Dependabot can later suggest a versioned bump) - docker/setup-qemu-action@v3 -> c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 - docker/setup-buildx-action@v3 -> 8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 - docker/login-action@v3 -> c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 - docker/build-push-action@v6 -> 10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 `dotnet/nbgv@master` remains the documented exception. 2. The AGENTS.md "Concurrency" convention says top-level workflows should use `cancel-in-progress: true`, but the safeguard PR set it to `false` in merge-bot-pull-request.yml — load-bearing for the three-job model. Add a documented exception to the convention so it's not a silent contradiction. Used the Edit tool per-line this time instead of `sed -i` to avoid the CRLF -> LF rewrite that happened in 5daf12f (and was reversed in 94e6b8a). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/build-datebadge-task.yml | 2 +- .github/workflows/build-docker-task.yml | 8 ++++---- AGENTS.md | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-datebadge-task.yml b/.github/workflows/build-datebadge-task.yml index 6f12a28a..471ec3cc 100644 --- a/.github/workflows/build-datebadge-task.yml +++ b/.github/workflows/build-datebadge-task.yml @@ -17,7 +17,7 @@ jobs: - name: Build BYOB date badge step if: ${{ github.ref_name == 'main' }} - uses: RubbaBoy/BYOB@v1 + uses: RubbaBoy/BYOB@a4919104bc0ec7cfd7f113e42c405cc45246f2a4 # v1 with: name: lastbuild label: "Last Build" diff --git a/.github/workflows/build-docker-task.yml b/.github/workflows/build-docker-task.yml index ea54c290..e103c23f 100644 --- a/.github/workflows/build-docker-task.yml +++ b/.github/workflows/build-docker-task.yml @@ -27,25 +27,25 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Setup QEMU step - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 with: platforms: linux/amd64,linux/arm64 - name: Setup Buildx step - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 with: platforms: linux/amd64,linux/arm64 # Always login to Docker Hub, not just on push, to benefit from # higher rate limits with a Docker subscription for pulls and cache - name: Login to Docker Hub step - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Docker build and push step - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 with: context: . push: ${{ inputs.push }} diff --git a/AGENTS.md b/AGENTS.md index fa7cc87e..f5c10d87 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -115,7 +115,7 @@ These conventions describe the target state. New and modified workflows must res - **Filename**: reusable workflows (those with `on: workflow_call`) end in `-task.yml`. Entry-point workflows (`on: push` / `pull_request` / `schedule` / `workflow_dispatch`) do NOT use the `-task` suffix; they end with what they do — `-pull-request.yml`, `-release.yml`, etc. The suffix carries semantic meaning: a `-task.yml` file is meant to be `uses:`-d, never triggered directly. - **Workflow `name:`** (the top-level `name:` field): reusable workflow names end in **"task"** (e.g. `Build PyPI library task`); entry-point workflow names end in **"action"** (e.g. `Publish project release action`, `Test pull request action`). The displayed action name in the GitHub Actions UI tells you at a glance whether you're looking at an orchestrator or a callee. - **Job and step `name:` suffixes**: every job's `name:` ends in **"job"**; every step's `name:` ends in **"step"**. **Exception**: a job whose `name:` is also referenced as a required-status-check `context:` in a branch ruleset (currently `Check pull request workflow status` in `test-pull-request.yml`) keeps the ruleset-bound name verbatim — renaming would silently break required-status-check enforcement. Do not "fix" that name; if a future job becomes ruleset-bound, mark it the same way. -- **Concurrency**: top-level workflows declare `concurrency: { group: '${{ github.workflow }}-${{ github.ref }}', cancel-in-progress: true }` so a fresh push supersedes an in-flight run on the same ref. +- **Concurrency**: top-level workflows declare `concurrency: { group: '${{ github.workflow }}-${{ github.ref }}', cancel-in-progress: true }` so a fresh push supersedes an in-flight run on the same ref. **Documented exception**: [`merge-bot-pull-request.yml`](./.github/workflows/merge-bot-pull-request.yml) uses `cancel-in-progress: false` because its three-job model (enable-auto-merge on opened, disable-auto-merge on maintainer-pushed synchronize, with method dispatched by base) requires each event to run to completion in arrival order. Cancellation would leave auto-merge in an inconsistent state. The rationale is recorded inline in that workflow's header comment. - **Shells**: multi-line `run:` blocks with bash start with `set -euo pipefail` — fail fast, fail on undefined vars, fail on a failed pipe segment. - **Conditionals**: multi-line `if:` uses folded scalar `if: >-` so YAML preserves whitespace correctly. Literal block (`if: |`) is wrong because it embeds newlines inside the boolean expression. - **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks — one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans; `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms — `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`. From 8d9b1ed937eccbd1377ff5be5835bb16bfb85210 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 11 May 2026 18:37:21 -0700 Subject: [PATCH 6/7] Update stale codegen concurrency comment; allow # vX when no specific tag matches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-on Copilot findings on PR #79: 1. run-periodic-codegen-pull-request.yml header comment claimed the workflow "checks out and targets main/codegen" — stale since PR #78 converted the reusable workflow to a matrix over both `main` and `develop` (branch names codegen-main / codegen-develop). Rewrote the concurrency comment and renamed the group from `codegen-main` to plain `codegen` to match the actual behavior (one group for the whole workflow; a new scheduled or manual run supersedes the matrix-legs of an in-flight one). 2. The `RubbaBoy/BYOB@a491910... # v1` pin uses a major-only version comment, but the AGENTS.md action-pinning rule prescribes `# vX.Y.Z`. The reason for the major-only comment: the upstream's `@v1` floating tag at RubbaBoy/BYOB points at a SHA that doesn't correspond to any specific v1.x tag (v1.3.0 is the latest specific release and has a different SHA). Pinning to v1.3.0 would be a behavior change away from what the floating tag currently delivers. Updating AGENTS.md to allow `# vX` (major-only) in this narrow case — the SHA pin still gives full protection, only the version comment loses specificity. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/run-periodic-codegen-pull-request.yml | 8 ++++++-- AGENTS.md | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/run-periodic-codegen-pull-request.yml b/.github/workflows/run-periodic-codegen-pull-request.yml index c64eaa4b..90f68705 100644 --- a/.github/workflows/run-periodic-codegen-pull-request.yml +++ b/.github/workflows/run-periodic-codegen-pull-request.yml @@ -7,8 +7,12 @@ on: - cron: '0 2 * * MON' concurrency: - # Workflow always checks out and targets main/codegen - group: codegen-main + # Workflow checks out and opens PRs against both `main` and `develop` + # via a matrix in `run-codegen-pull-request-task.yml` (branch names + # `codegen-main` and `codegen-develop`). One concurrency group for the + # whole workflow — a new scheduled or manual run supersedes an + # in-flight one across both matrix legs. + group: codegen cancel-in-progress: true jobs: diff --git a/AGENTS.md b/AGENTS.md index f5c10d87..89ae840f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,7 +111,7 @@ Anti-pattern: don't keep flipping the code on the same style point. Flip the rul These conventions describe the target state. New and modified workflows must respect them; the rest of the repo is expected to be brought up to the same standard. Sweep PRs that apply a rule everywhere are welcome when a rule changes. -- **Action pinning**: pin **every** action — first-party (`actions/*`) and third-party — to a commit SHA with a trailing `# vX.Y.Z` comment, so Renovate / Dependabot can still bump it but a tag swap can't change the executed code. Documented exception: [`dotnet/nbgv`](./.github/workflows/get-version-task.yml) is consumed via `@master` because the upstream tag stream lags `master` substantially and Dependabot's tag-tracking would propose a downgrade — the rationale is documented inline in that workflow. +- **Action pinning**: pin **every** action — first-party (`actions/*`) and third-party — to a commit SHA with a trailing `# vX.Y.Z` comment, so Renovate / Dependabot can still bump it but a tag swap can't change the executed code. Use `# vX` (major-only) only when the upstream's floating major tag doesn't correspond to a specific patch/minor release SHA — pinning to the floating-tag SHA still gives the SHA guarantee, the version comment just records the major line. Documented exception (no SHA pin at all): [`dotnet/nbgv`](./.github/workflows/get-version-task.yml) is consumed via `@master` because the upstream tag stream lags `master` substantially and Dependabot's tag-tracking would propose a downgrade — the rationale is documented inline in that workflow. - **Filename**: reusable workflows (those with `on: workflow_call`) end in `-task.yml`. Entry-point workflows (`on: push` / `pull_request` / `schedule` / `workflow_dispatch`) do NOT use the `-task` suffix; they end with what they do — `-pull-request.yml`, `-release.yml`, etc. The suffix carries semantic meaning: a `-task.yml` file is meant to be `uses:`-d, never triggered directly. - **Workflow `name:`** (the top-level `name:` field): reusable workflow names end in **"task"** (e.g. `Build PyPI library task`); entry-point workflow names end in **"action"** (e.g. `Publish project release action`, `Test pull request action`). The displayed action name in the GitHub Actions UI tells you at a glance whether you're looking at an orchestrator or a callee. - **Job and step `name:` suffixes**: every job's `name:` ends in **"job"**; every step's `name:` ends in **"step"**. **Exception**: a job whose `name:` is also referenced as a required-status-check `context:` in a branch ruleset (currently `Check pull request workflow status` in `test-pull-request.yml`) keeps the ruleset-bound name verbatim — renaming would silently break required-status-check enforcement. Do not "fix" that name; if a future job becomes ruleset-bound, mark it the same way. From b86b24a82199715ace50b85170bd9c1b20b8d2d3 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Mon, 11 May 2026 18:43:57 -0700 Subject: [PATCH 7/7] Use AGENTS.md concurrency convention in periodic codegen workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review on PR #79 caught that my prior rename ('codegen-main' to 'codegen') made the concurrency group a constant — which deviates from AGENTS.md's documented convention of `${{ github.workflow }}-${{ github.ref }}`. The constant was a pre-existing pattern that my change inherited rather than fixed. Switching to the AGENTS.md convention. In practice the behavior is identical for this workflow: scheduled cron runs always have `github.ref == refs/heads/`, so the group is effectively constant across all scheduled runs anyway. The reusable workflow it calls then matrixes over both `main` and `develop` internally, so one group per workflow+ref still serializes all matrix legs. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../workflows/run-periodic-codegen-pull-request.yml | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/.github/workflows/run-periodic-codegen-pull-request.yml b/.github/workflows/run-periodic-codegen-pull-request.yml index 90f68705..8508744a 100644 --- a/.github/workflows/run-periodic-codegen-pull-request.yml +++ b/.github/workflows/run-periodic-codegen-pull-request.yml @@ -7,12 +7,13 @@ on: - cron: '0 2 * * MON' concurrency: - # Workflow checks out and opens PRs against both `main` and `develop` - # via a matrix in `run-codegen-pull-request-task.yml` (branch names - # `codegen-main` and `codegen-develop`). One concurrency group for the - # whole workflow — a new scheduled or manual run supersedes an - # in-flight one across both matrix legs. - group: codegen + # Standard AGENTS.md "Concurrency" convention. Scheduled runs always + # have `github.ref == refs/heads/` (cron's default), + # and the reusable workflow `run-codegen-pull-request-task.yml` then + # matrixes over both `main` and `develop` internally — so a single + # group per workflow+ref serializes all matrix legs of all scheduled + # / dispatched runs in practice. + group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: