Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ end_of_line = lf
[*.{cmd,bat,ps1}]
end_of_line = crlf

# --- .NET-only below: C# and ReSharper style. Everything above is the line-ending
# governance every derived repo carries; a non-.NET repo may drop from here down. ---

# C# files
[*.cs]
end_of_line = crlf
Expand Down
36 changes: 7 additions & 29 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

Repository conventions for GitHub Copilot (and any other AI agent reading this file).

The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first for this repo's conventions, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract that this file's runbook implements.

This file is intentionally narrow: commit/PR-title conventions (so VS Code's AI commit-message and PR-title generators get them without an extra fetch), plus a GitHub Copilot Review Runbook that documents the provider-specific mechanics behind the review-loop contract defined in AGENTS.md.
The **canonical guide is [AGENTS.md](../AGENTS.md)** at the repo root - read it first, including the [PR Review Etiquette](../AGENTS.md#pr-review-etiquette) review-loop contract this file's runbook implements. This file is intentionally narrow: commit/PR-title conventions (summarized inline so VS Code's commit-message and PR-title generators have them) plus the GitHub Copilot Review Runbook.

For language-specific style rules, see:

Expand All @@ -15,31 +13,11 @@ Do not duplicate language-specific rules here.

## Commit Messages and Pull Request Titles

Feature -> develop PRs squash-merge - the PR title becomes the single commit on develop. Develop -> main PRs merge-commit - main's history shows one merge commit per release with develop's tip as the second parent. Titles are descriptive and have no versioning effect - versioning is handled by [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) reading [version.json](../version.json) and git history, not by parsing commit messages.

Branch protection enforces the merge method on both bases (develop allows only squash, main allows only merge). When running `gh pr merge` against either base, pick the matching flag (`--squash` for develop, `--merge` for main); a mismatch fails with "Merge method ... is not allowed on this repository". The merge-bot workflow (`.github/workflows/merge-bot-pull-request.yml`) does this dispatch automatically for Dependabot and codegen PRs via a `case` on `base.ref` - keep that pattern when adding new auto-merge jobs.

### Format

- Imperative subject summarizing the change, <= 72 characters, no trailing period. ("Add 24-hour PM2.5 average sensor", not "Added X" or "Adds X".)
- Optional body, blank-line separated, explaining *why* the change is being made when that's non-obvious. The diff shows *what*.
Summarized for VS Code's generators; the full rules, rationale, and examples are in [AGENTS.md "Pull Request Title and Commit Message Conventions"](../AGENTS.md#pull-request-title-and-commit-message-conventions).

### Rules

- Don't write `update stuff`, `wip`, or other vague titles. (Dependabot's default `Bump X from Y to Z` titles are fine - keep them.)
- Don't add `Co-Authored-By:` lines unless the user explicitly asks.
- Don't put release-bump magnitude in the title - no "minor", "patch", "release v0.2.0", etc. NBGV computes the next release version from `version.json` + git history. Dependency versions in dependency-bump titles are fine and expected.
- Use US English spelling and match the existing heading style of the file you're editing: title case with lowercase short bind words (a, an, the, and, but, or, of, in, on, at, to, by, for, from); hyphenated compounds capitalize both parts unless the second is a short preposition (*Built-in*, *EPA-Corrected*, *24-Hour*).

### Examples

```text
Add structured logging extensions to library
Pin softprops/action-gh-release to commit SHA
Drop net8.0 multi-targeting from console project
Bump xunit.v3 from 3.2.2 to 3.3.0
Clarify devcontainer setup steps in README
```
- Imperative subject, <= 72 characters, no trailing period; optional blank-line-separated body for the non-obvious *why*.
- US English, title case with lowercase short bind words; no vague titles, no `Co-Authored-By:` unless asked, no release-bump magnitude (NBGV handles versioning). Dependabot's `Bump X from Y to Z` titles are fine.
- develop PRs squash-merge (`gh pr merge --squash`), main PRs merge-commit (`--merge`); a mismatched flag is rejected by branch protection.

## GitHub Copilot Review Runbook

Expand All @@ -49,9 +27,9 @@ Use this section for provider-specific mechanics. The expected review loop *cont

### Triggering and Polling

Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This now works reliably (it previously did not - a maintainer had to click "re-request review" in the UI; the agent can now drive the loop end-to-end without that hand-off).
Auto-review on push is configured (via the branch ruleset's `copilot_code_review` rule with `review_on_push: true`) but fires inconsistently in practice - treat it as best-effort, not guaranteed. After every push, **re-request a review programmatically** via the GraphQL `requestReviews` mutation, passing the Copilot reviewer's bot node id in `botIds`. This drives the loop end-to-end without a UI hand-off.

> **The reviewer login differs by API - this is intentional, not a typo.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them.
> **The reviewer login differs by API.** In **GraphQL** (`gh api graphql` and `gh pr view --json reviews`, which is GraphQL-backed) the `Bot.login` is `copilot-pull-request-reviewer` - **no `[bot]` suffix**. In the **REST** API (`gh api repos/.../issues|pulls/...`) the same account's `user.login` is `copilot-pull-request-reviewer[bot]` - **with** the suffix. Each query below uses the correct form for its API; match the API, not a single spelling, when adapting them.

```sh
# 1. PR node id + the Copilot reviewer's bot node id (read from any existing
Expand Down
38 changes: 10 additions & 28 deletions .github/workflows/build-docker-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,13 @@ on:
required: false
type: string
default: ''
# Logical branch driving config and tags (`main` => Release/`latest`,
# anything else => Debug/`develop`). Required (no `github.ref_name`
# fallback): the publisher builds develop from a run whose
# `github.ref_name` is `main`, so a silent fallback would mistag it.
# The orchestrator always passes it explicitly.
# Logical branch driving config and tags (main => Release/latest, otherwise Debug/develop). Required (no
# github.ref_name fallback): the publisher builds develop from a main-ref run, so a fallback would mistag it.
branch:
required: true
type: string
# Smoke mode: build `linux/amd64` only (no QEMU/arm64), never push, and
# skip the shared registry `cache-to` so PR builds don't pollute the
# release buildcache. Used for fast PR feedback.
# Smoke mode: build linux/amd64 only (no QEMU/arm64), never push, and skip the shared registry cache-to so PR
# builds don't pollute the release buildcache. Used for fast PR feedback.
smoke:
required: false
type: boolean
Expand All @@ -50,8 +46,7 @@ jobs:
with:
ref: ${{ inputs.ref }}

# QEMU only exists to emulate arm64. Smoke builds are amd64-only, so
# skip it entirely to save the emulation setup cost.
# QEMU only emulates arm64; smoke builds are amd64-only, so skip it to save setup cost.
- name: Setup QEMU step
if: ${{ !inputs.smoke }}
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
Expand All @@ -63,14 +58,8 @@ jobs:
with:
platforms: ${{ inputs.smoke && 'linux/amd64' || '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 reads on
# every build (including smoke). This is a CONSCIOUS choice over gating
# login on `inputs.push`: the trade-off is that fork PRs without access
# to the Docker Hub secrets cannot run the Docker smoke build.
# Acceptable here because the repo is private and PRs are same-repo; a
# public derived project that accepts fork PRs may prefer to gate this
# step on `inputs.push`.
# Always login (even for smoke) to get the higher Docker subscription rate limits for pulls and cache reads. The
# trade-off: fork PRs without the Docker Hub secrets cannot run the Docker smoke build; acceptable for same-repo PRs.
- name: Login to Docker Hub step
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
Expand All @@ -87,16 +76,9 @@ jobs:
docker.io/ptr727/projecttemplate:${{ inputs.branch == 'main' && 'latest' || 'develop' }}
docker.io/ptr727/projecttemplate:${{ needs.get-version.outputs.SemVer2 }}
platforms: ${{ inputs.smoke && 'linux/amd64' || 'linux/amd64,linux/arm64' }}
# Branch-scoped registry cache. READ both branches' caches - the
# layers are nearly identical (only BUILD_CONFIGURATION differs), so
# a main build can seed from develop's cache and vice versa - but
# WRITE only this branch's own tag, and only when actually pushing.
# Gating the export on `inputs.push` (not just `!smoke`) means a
# non-publishing build never writes the shared registry cache or
# needs Docker Hub write creds - smoke builds (always push=false) are
# covered too. Branch-scoping is what lets the publisher's weekly
# matrix build main and develop concurrently in one run without the
# two legs overwriting a single shared cache (destroying hit rates).
# Branch-scoped registry cache: read both branches' caches (layers are near-identical) but write only this branch's
# tag, and only when pushing. Branch-scoping lets the weekly matrix build main and develop concurrently without the
# two legs overwriting one shared cache.
cache-from: |
type=registry,ref=docker.io/ptr727/projecttemplate:buildcache-main
type=registry,ref=docker.io/ptr727/projecttemplate:buildcache-develop
Expand Down
42 changes: 15 additions & 27 deletions .github/workflows/build-executable-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@ on:
required: false
type: string
default: ''
# Logical branch driving build configuration (`main` => Release, else
# Debug). Required (no `github.ref_name` fallback, which would mislabel
# the develop leg of the publisher's matrix); the orchestrator passes it.
# Logical branch driving build configuration (`main` => Release, else Debug). Required (no fallback) so the
# develop leg of the publisher's matrix isn't mislabeled.
branch:
required: true
type: string
# Smoke mode: build a representative runtime subset (linux-x64 +
# win-x64) instead of the full 7-runtime matrix, and skip the zip /
# artifact aggregation. Used for fast PR feedback.
# Smoke mode: build a runtime subset (linux-x64 + win-x64) instead of the full matrix and skip the zip /
# artifact aggregation, for fast PR feedback.
smoke:
required: false
type: boolean
Expand All @@ -41,14 +39,8 @@ jobs:

steps:

# NOTE: NuGet restore caching is intentionally NOT enabled on the .NET
# jobs (this matrix, build-nugetlibrary-task, and the unit-test job). The
# restore is low-overhead for this template's small dependency set, and
# `setup-dotnet`'s built-in cache requires a `packages.lock.json` that
# Central Package Management (Directory.Packages.props) does not produce
# by default. The Docker layer cache and uv's cache (which carry the
# expensive work) are enabled; revisit .NET restore caching only if the
# dependency graph grows enough to make it worthwhile.
# NuGet restore caching is not enabled: restore is cheap for this small dependency set, and `setup-dotnet`'s
# cache requires a `packages.lock.json` that Central Package Management does not produce by default.
- name: Setup .NET SDK step
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
Expand All @@ -72,19 +64,17 @@ jobs:
-property:InformationalVersion=${{ needs.get-version.outputs.AssemblyInformationalVersion }} \
-property:PackageVersion=${{ needs.get-version.outputs.SemVer2 }}

# Artifact names are suffixed with the branch so the publisher can build
# `main` and `develop` in the same workflow run (a branch matrix) without
# two legs colliding on an identical artifact name.
# Branch-suffixed so the publisher can build `main` and `develop` in one run without colliding on the name.
- name: Upload matrix build artifacts step
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: publish-${{ inputs.branch }}-${{ matrix.runtime }}
path: ${{ runner.temp }}/publish
# Consumed within this run by the aggregation job; minimize artifact storage.
retention-days: 1

# Smoke builds only need the per-runtime compile to succeed (fast PR
# feedback) - the zipped, downloadable artifact is a release concern, so
# skip the aggregation entirely on smoke. The GitHub release job never runs
# on smoke, so no `release-asset-*` artifact is needed then.
# Smoke builds only need the per-runtime compile to succeed, so skip the zip aggregation; the release job never
# runs on smoke, so no `release-asset-*` artifact is needed.
upload-build-artifacts:
name: Upload matrix build artifacts job
if: ${{ !inputs.smoke }}
Expand All @@ -103,14 +93,12 @@ jobs:
- name: Zip build output step
run: 7z a -t7z ${{ runner.temp }}/Console.7z ${{ runner.temp }}/publish/*

# GitHub-release asset, uploaded under the `release-asset-<branch>-*`
# convention that the `github-release` job in build-release-task.yml
# collects by pattern (it never names this job) - so a derived project
# swaps its release contents by replacing this leaf task, not the
# orchestrator. Branch-suffixed so the publisher's branch matrix can
# build both branches in one run without colliding on the artifact name.
# GitHub-release asset, uploaded under the `release-asset-<branch>-*` pattern that the `github-release` job
# collects. Branch-suffixed so the publisher can build both branches in one run without colliding on the name.
- name: Upload build artifacts step
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: release-asset-${{ inputs.branch }}-executable
path: ${{ runner.temp }}/Console.7z
# Consumed within this run by the github-release job; minimize artifact storage.
retention-days: 1
17 changes: 7 additions & 10 deletions .github/workflows/build-nugetlibrary-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Build NuGet library task
on:
workflow_call:
inputs:
# Input to control whether to push the NuGet library to NuGet.org
# Whether to push the NuGet library to NuGet.org.
push:
required: false
type: boolean
Expand All @@ -13,9 +13,8 @@ on:
required: false
type: string
default: ''
# Logical branch driving build configuration (`main` => Release, else
# Debug). Required (no `github.ref_name` fallback, which would mislabel
# the develop leg of the publisher's matrix); the orchestrator passes it.
# Logical branch driving build configuration (`main` => Release, else Debug). Required (no fallback) so the
# develop leg of the publisher's matrix isn't mislabeled.
branch:
required: true
type: string
Expand Down Expand Up @@ -70,14 +69,12 @@ jobs:
- name: Zip output step
run: 7z a -t7z ${{ runner.temp }}/NuGetLibrary.7z ${{ runner.temp }}/publish/*

# GitHub-release asset, uploaded under the `release-asset-<branch>-*`
# convention that the `github-release` job in build-release-task.yml
# collects by pattern (it never names this job) - so a derived project
# swaps its release contents by replacing this leaf task, not the
# orchestrator. Branch-suffixed so the publisher's branch matrix can
# build both branches in one run without colliding on the artifact name.
# GitHub-release asset, uploaded under the `release-asset-<branch>-*` pattern that the `github-release` job
# collects. Branch-suffixed so the publisher can build both branches in one run without colliding on the name.
- name: Upload build artifacts step
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: release-asset-${{ inputs.branch }}-nugetlibrary
path: ${{ runner.temp }}/NuGetLibrary.7z
# Consumed within this run by the github-release job; minimize artifact storage.
retention-days: 1
Loading
Loading