diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index f61f6468..4632c89a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -330,6 +330,30 @@ The project includes comprehensive `.editorconfig` settings that enforce: - **Never run destructive git commands** (`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`) without explicit developer instruction. - **Staging is the limit.** Prepare changes and stage files; the developer handles all commits and pushes. +## Pull Request Title and Commit Message Conventions + +### 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*. + +### 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 developer explicitly asks. +- Don't put release-bump magnitude in the title — no "minor", "patch", "release v0.2.0", etc. Nerdbank.GitVersioning 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 +``` + ## Workflow 1. **Before coding**: Run `dotnet tool restore` to ensure tools are installed diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index 20130c2c..79ac2db7 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -58,23 +58,30 @@ jobs: - name: Checkout code step uses: actions/checkout@v6 - - name: Download library build artifacts job + - name: Download library build artifacts step uses: actions/download-artifact@v7 with: artifact-ids: ${{ needs.build-library.outputs.artifact-id }} path: ./Publish - - name: Download executable build artifacts job + - name: Download executable build artifacts step uses: actions/download-artifact@v7 with: artifact-ids: ${{ needs.build-executable.outputs.artifact-id }} path: ./Publish - - name: Create GitHub release job - uses: softprops/action-gh-release@v2 + # `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 diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml index 4e27cfd6..c35b1576 100644 --- a/.github/workflows/merge-bot-pull-request.yml +++ b/.github/workflows/merge-bot-pull-request.yml @@ -1,5 +1,19 @@ name: Merge bot pull request action +# Token strategy: +# GitHub's recursion guard blocks pushes authored by `GITHUB_TOKEN` from +# triggering further workflow runs. When `gh pr merge --auto --squash` runs +# under `secrets.GITHUB_TOKEN`, the resulting squash-merge push therefore +# does NOT fire `publish-release.yml`. +# +# All three jobs below merge bot PRs targeting `main` (per the per-job `if:` +# conditions). Releases on `main` are dispatched manually via +# `workflow_dispatch`, so the missing trigger is acceptable for all three +# paths. If a future bot PR targets `develop` (where releases auto-fire on +# push), this merge action would need to switch to an App token so the +# resulting push is authored by an App identity not blocked by the +# recursion guard. + on: pull_request: types: [opened, reopened, synchronize] diff --git a/AGENTS.md b/AGENTS.md index ac3a0281..8a49fcea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,6 +13,62 @@ For comprehensive coding standards and detailed conventions, refer to [`.github/ - **Never run destructive git commands** (`git reset --hard`, `git checkout .`, `git restore .`, `git clean -f`) without explicit developer instruction. - **Staging is the limit.** Prepare and stage file changes; the developer runs `git commit` in their own environment where signing keys are available. +## Pull Request Title and Commit Message Conventions + +### 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*. + +### 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 developer explicitly asks. +- Don't put release-bump magnitude in the title — no "minor", "patch", "release v0.2.0", etc. Nerdbank.GitVersioning 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 +``` + +## Documentation Style Conventions + +### Markdown + +- Use reference-style links for any URL referenced more than once or appearing in lists; alphabetize the reference definitions block. +- Inline single-use relative links (e.g. `[CODESTYLE.md](./CODESTYLE.md)`) are fine. +- One logical paragraph per line; no hard-wrap line-length limit. +- Headings follow the title-case-with-short-bind-words rule from the PR-title section. + +### Quantitative Claims + +- Any quantitative claim in `README.md` (counts, sizes, version floors, supported platforms) must be verified against current code. If a doc number is derived from a code constant, mark the dependency in a source-code comment so the next editor knows to update both. + +## 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. + +- **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. +- **Naming**: every step's `name:` ends in `step`; every job's `name:` ends in `job`. Reusable workflow filenames end in `-task.yml`. +- **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. +- **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. +- **Reusable workflows**: job-level `permissions:` are validated *before* the `if:` evaluates, so even a skipped job needs valid permissions declared. +- **Tag pinning on releases**: when using `softprops/action-gh-release` (or any tag-creating action), pass `target_commitish: ${{ github.sha }}` explicitly. Without it, GitHub's REST API defaults the new tag to the repository's default branch instead of the commit that built the artifact. + +## Branching Model + +- `develop` is the integration branch. Feature branches → `develop` is **squash-only**; the develop branch is kept linear. +- `develop` → `main` is **merge-commit only** (no squash, no rebase). Merge commits preserve develop's commit list as a real second-parent reference on main; this is what allows the "release on every push" model to attribute releases to the develop commits that produced them. Branch protection enforces this: the develop ruleset allows only `squash`, the main ruleset allows only `merge`. +- All commits on both branches must be cryptographically signed (SSH or GPG). Squash and merge commits created via the GitHub UI are signed by GitHub's web-flow key. + ## Key Requirements for All Projects Derived from This Template ### Build & Quality Standards