ptr727.ProjectTemplate.Library
1.0.90
dotnet add package ptr727.ProjectTemplate.Library --version 1.0.90
NuGet\Install-Package ptr727.ProjectTemplate.Library -Version 1.0.90
<PackageReference Include="ptr727.ProjectTemplate.Library" Version="1.0.90" />
<PackageVersion Include="ptr727.ProjectTemplate.Library" Version="1.0.90" />
<PackageReference Include="ptr727.ProjectTemplate.Library" />
paket add ptr727.ProjectTemplate.Library --version 1.0.90
#r "nuget: ptr727.ProjectTemplate.Library, 1.0.90"
#:package ptr727.ProjectTemplate.Library@1.0.90
#addin nuget:?package=ptr727.ProjectTemplate.Library&version=1.0.90
#tool nuget:?package=ptr727.ProjectTemplate.Library&version=1.0.90
ProjectTemplate
C# .NET project template.
Build and Distribution
- Source Code: GitHub - Source code, issues, discussions, and CI/CD pipelines.
- Versioned Releases: GitHub Releases - Version tagged source code and build artifacts.
- Docker Images: Docker Hub - Container images with all tools pre-installed.
- NuGet Packages: NuGet Packages - .NET libraries published to NuGet.org.
- PyPI Packages: PyPI Packages - Python library published to PyPI.org.
Build Status
Releases
Release Notes
Version: 1.0:
Summary:
- Something.
- And something else.
⚠️ Breaking Changes:
- Something.
- And something else.
See Release History for complete release notes and older versions.
Getting Started
Get started with ProjectTemplate in three easy steps:
⚠️ Important: Some important warning.
ℹ️ Note: Some interesting note.
Install ProjectTemplate:
- Do something.
Configure ProjectTemplate:
- Then something else.
Run ProjectTemplate:
Console --loglevel=Debug
See Installation for detailed setup instructions.
Table of Contents
- Build and Distribution
- Getting Started
- Table of Contents
- Use Cases
- Installation
- Configuration
- Usage
- Questions or Issues
- Development Environment Setup
- 3rd Party Tools
- License
- Template Project Setup
Use Cases
ℹ️ TL;DR: This widget is special because it does something special.
- It does something special.
- And it does something else special.
Installation
Choose an installation method based on your platform and requirements:
- Method 1 (Recommended): Easiest and most up-to-date option.
- ✅ Some good reason.
- ⚠️ Some not so good reason.
- ❌ Strong reason to avoid.
- Best for: Linux, NAS devices, servers, cross-platform deployments.
- Method 2: Custom configuration options.
- ✅ Some good reason.
- ⚠️ Some not so good reason.
- ❌ Strong reason to avoid.
- Best for: Specialized devices.
Configuration
⚠️ Important: The spinner setting must be configured before first use.
Required configuration:
- Set
footo something. - Set
barto something else.
Optional configuration:
- Set
advancedtospecial. - Some other custom option.
Usage
Console [global options] <command> [command options]
Command Quick Reference
| Command | Description | Notes |
|---|---|---|
| default | Default action when no command is specified | First time setup |
test |
Do something else useful | Some note |
--help |
Show help output | Use <command> --help for command specific help |
--version |
Show version output |
Use the --help option to get a list of all commands and global options.
To get help for a specific command run Console <command> --help.
Global Options
Global options apply to all commands:
| Option | Description | Default |
|---|---|---|
--logfile |
Debug log file | Optional |
--loglevel |
Debug log level | Default is Information |
--logfile-clear |
Clear log file at startup | Default is false |
General help:
>.\Console\bin\Debug\net10.0\Console --help
Description:
C# .NET console project
Usage:
Console [command] [options]
Options:
-l, --loglevel <Debug|Error|Fatal|Information|Verbose|Warning> Set the log level (default: Information). [default: Information]
-f, --logfile <logfile> Write logs to the specified file (optional).
-c, --logfile-clear Clear the log file before writing (default: false).
-?, -h, --help Show help and usage information
--version Show version information
Commands:
test Test command
Test Command
Test command options:
| Option | Description | Default |
|---|---|---|
--test |
Test options | Optional |
Test command help:
>.\Console\bin\Debug\net10.0\Console test --help
Description:
Test command
Usage:
Console test [options]
Options:
-t, --test <test> Test command option (optional).
-?, -h, --help Show help and usage information
-l, --loglevel <Debug|Error|Fatal|Information|Verbose|Warning> Set the log level (default: Information). [default: Information]
-f, --logfile <logfile> Write logs to the specified file (optional).
-c, --logfile-clear Clear the log file before writing (default: false).
Questions or Issues
For General Questions:
- Use the Discussions forum for general questions.
For Bug Reports:
- Ask in the Discussions forum if you are not sure if it is a bug.
- Check the existing Issues tracker for known problems.
- If the issue is unique and a bug, file it in Issues, and include all pertinent steps to reproduce the issue.
Development Environment Setup
The recommended setup is one of the per-language Dev Containers under .devcontainer/:
.devcontainer/dotnet/- .NET 10 SDK + GitHub CLI. Pair withDotNet.code-workspace..devcontainer/python/- Python 3.14 +uv+ GitHub CLI. Pair withPython.code-workspace.
Each container bind-mounts your SSH public key, allowed-signers file, and gh config from the host so commits sign correctly. gh is pre-authenticated when the host token is file-backed; macOS Keychain and Linux libsecret-backed tokens require an in-container gh auth login - see the credential-store nuance section.
Windows note: Python work is intentionally not supported on the Windows host. The Python extension caches the Linux-layout
PyPiLibrary/.venv/bin/pythonagainst a venv whose actual Windows path isPyPiLibrary\.venv\Scripts\python.exe, breaking Ruff. Use the python devcontainer.
Recommended (devcontainer):
- Complete host setup once per machine (git identity, SSH key, allowed_signers,
gh auth login, SSH commit signing). - Clone the repo, open the matching workspace (
DotNet.code-workspaceorPython.code-workspace) in VS Code with the Dev Containers extension, and run Reopen in Container - pick the language flavor. - The
postCreateCommandrunsdotnet tool restore(.NET container) or installsuvand runsuv sync(Python container). No git hooks are installed by default - see "Optional: enable git hooks locally" below.
Alternative (host install):
Install Developer Tools:
Install .NET SDK:
# Windows winget install Microsoft.DotNet.SDK.10 # Linux apt install dotnet-sdk-10.0Install Visual Studio Code:
# Windows winget install Microsoft.VisualStudioCodeInstall Visual Studio:
# Windows winget install Microsoft.VisualStudio.Community
Clone and Configure Project:
Clone the repository and initialize tools:
# Clone from CLI (or clone from VSCode) git clone -b main https://github.com/ptr727/[Project].git ./[Project] # Initialize dotnet tools cd ./[Project] dotnet tool restoreOpen
DotNet.code-workspace(orPython.code-workspace) in Visual Studio Code.Open
[Project].slnxin Visual Studio.
Optional: enable git hooks locally:
Hooks are not shipped with the template - CI is the lint backstop. Opt in per language if you want pre-commit checks locally.
For .NET work - install Husky.Net:
dotnet new tool-manifest # if no tool manifest exists yet dotnet tool install Husky dotnet husky install dotnet husky add pre-commit -c "dotnet csharpier check . && dotnet format style --verify-no-changes --severity=info"For Python work - install pre-commit:
uv tool install pre-commit pre-commit installSample
.pre-commit-config.yaml(the hooks shell intoPyPiLibrary/because the uv project - and therefore ruff/pyright and their configs - lives there, not at the repo root):repos: - repo: local hooks: - id: ruff-check name: ruff check entry: uv run --directory PyPiLibrary ruff check language: system files: ^PyPiLibrary/.*\.py$ pass_filenames: false - id: ruff-format name: ruff format entry: uv run --directory PyPiLibrary ruff format --check language: system files: ^PyPiLibrary/.*\.py$ pass_filenames: false - id: pyright name: pyright entry: uv run --directory PyPiLibrary pyright language: system files: ^PyPiLibrary/.*\.py$ pass_filenames: false
CI runs these same checks on every PR, so hooks are purely a local convenience.
3rd Party Tools
3rd Party tools used in this project:
- API Ninjas
- AwesomeAssertions
- Bring Your Own Badge
- Create Pull Request
- CSharpier
- GH Release
- Git Auto Commit
- GitHub Actions
- GitHub Dependabot
- Nerdbank.GitVersioning
- Serilog
- xUnit.Net
License
Licensed under the MIT License
Template Project Setup
Template - TODO List
- Configure git for SSH signing before making any commits (the signed-commits ruleset rejects unsigned history, and retrofitting it forces a full re-sign - see GitHub Setup), plus SSH forwarding in dev containers - see docs/host-setup.md, docs/ssh-signing.md, and docs/devcontainer.md.
- Decide whether your project needs the .NET (
NuGetLibrary/) side, the Python (PyPiLibrary/) side, or both. Delete the unused folder and remove its references fromProjectTemplate.slnx,.github/dependabot.yml, and the corresponding.github/workflows/build-*-task.yml. - Start on Linux to avoid file permission issues when moving from Windows.
- Configure the Developer Environment.
- Open the project directory (not the workspace) in Visual Studio Code, and rename (Ctrl-Shift-H) all instances of
ProjectTemplateto[NewProject]in code. - Rename
DotNet.code-workspaceto[NewProject].code-workspaceandPython.code-workspaceto[NewProject]-Python.code-workspace, or delete the workspace for the language you don't need. RenameProjectTemplate.slnxto[NewProject].slnx. - Open the workspace file for the language you kept (
[NewProject].code-workspaceand/or[NewProject]-Python.code-workspace) in Visual Studio Code. - Delete any projects and associated actions that will not be used, update dependencies in actions to remove deleted actions.
- Rename projects to match the naming, update
.slnxand.csprojfiles, and update actions to match the naming. - Update the
namespacein.csand.csprojfiles to match the naming. - Update all ref-links in
README.mdto point to the naming. - Keep the template's mandatory shared files and sections - do not re-invent them per repo. Carry verbatim the
AGENTS.md"PR Review Etiquette" section,.github/copilot-instructions.md(the Copilot review runbook),.markdownlint-cli2.jsonc,.editorconfig, and.gitattributes, adapting only<owner>/<repo>placeholders. See AGENTS.md "Files and Sections Derived Repos Must Carry Verbatim", and re-sync them from the template periodically - filing an upstream issue when you spot a template gap. - Publish to GitHub from VSCode to create a new empty GitHub repository.
- Commit and push the
first-branch. - Edit and iterate only in
first-branchuntil ready to start with git history. - Setup
mainas the first permanent branch when ready. - Configure GitHub for the new repository - including deleting any classic branch protection and creating the two
develop/mainrulesets by exporting/importing the template's (see Rules / Rulesets). - Follow the Branching Workflow.
- Delete the
Project Template Setupsection fromREADME.md.
Template - Developer Environment Setup
Template - Git Setup
⚠️ Prerequisites:
- Configure git for SSH signing - see SSH commit signing.
- Configure host prerequisites (SSH key,
allowed_signers,ghauth) - see host setup. - Configure SSH forwarding for dev containers - see devcontainer setup.
Setup new project from template:
# Clone the template project git clone -b main https://github.com/ptr727/ProjectTemplate.git ./[NewProject] # Reset git to start a new repo rm -r ./[NewProject]/.git cd ./[NewProject] git init -b first-branch # Init dotnet tools dotnet tool restore # Update dotnet tools dotnet tool update --all dotnet outdated --upgrade:promptSetup new project from scratch:
⚠️ Linux: Start configuration on Linux to avoid file permission issues.
# Init git mkdir ./[NewProject] cd ./[NewProject] git init -b first-branch # Init dotnet tools dotnet new tool-manifest dotnet tool install csharpier dotnet tool install dotnet-outdated-toolUse
first-branchfor all the initial project setup and testing.When ready, only when ready, create
mainbranch fromfirst-branchwith no history:Warning - sign from the very first commit. The
Initial import (squashed)commit below - and every commit after it - must be cryptographically signed. Configure SSH signing (the Prerequisites above) before running these commands, and verify it is live:git config --get commit.gpgsignistrueand a signer is loaded (ssh-add -Lfor SSH). Do not enable theRequire signed commitsruleset (below) until the branch's history is fully signed. Enabling it on a branch that already contains unsigned commits forces you to rewrite the entire history to re-sign it - which changes every commit SHA and makes whoever does the rewrite the committer and signer of every commit (you cannot sign another contributor's commits for them; a rebase preserves theauthorfield but not the original signatures). Order: signing first → clean signed history → then the ruleset.# Create main branch with no history git checkout --orphan main git commit --allow-empty -m "temp" # Squash merge changes git merge --squash first-branch git commit -m "Initial import (squashed)" # Drop the temporary commit git reset --hard HEAD~1 # Delete first-branch git branch -D first-branch
Template - GitHub Setup
GitHub secrets setup:
Create a NuGet API Key.
- Save the Key as
NUGET_API_KEYin:- GitHub project Settings / Secrets / Actions.
- GitHub project Settings / Secrets / Dependabot.
- GitHub Local Actions Settings / Secrets.
- Save the Key as
Create a Docker Hub Personal Access Token.
- Save the PAT as
DOCKER_HUB_ACCESS_TOKENandDOCKER_HUB_USERNAMEin:- GitHub project Settings / Secrets / Actions.
- GitHub project Settings / Secrets / Dependabot.
- Save the PAT as
Create a GitHub App for the codegen and merge-bot workflows.
- App name:
ptr727-codegen. - Bot user:
ptr727-codegen[bot]. - Permissions required (repository scope):
- Contents: Read & write - push commits to the
codegenbranch and merge bot PRs. - Pull requests: Read & write - open, update, and merge pull requests.
- Metadata: Read-only (auto-required).
- Contents: Read & write - push commits to the
- Note the client id, and download and secure the private key
.pemfile. - Grant the app access to the repository, or all repositories.
- GitHub Settings / Developer settings / GitHub Apps / ptr727-codegen / Configure
- The app must be both created and installed - creating it alone is not sufficient (
actions/create-github-app-tokenfails withNot Foundif the app isn't installed on the repository).
- Save the Client ID as
CODEGEN_APP_CLIENT_IDand the private key contents asCODEGEN_APP_PRIVATE_KEYin both of:- GitHub project Settings / Secrets / Actions - for the codegen workflow and the codegen merge job.
- GitHub project Settings / Secrets / Dependabot - required because Dependabot-triggered
pull_requestworkflow runs use a separate, restricted secret context that doesn't see Actions secrets. Without the App secrets in the Dependabot store, themerge-dependabotjob inmerge-bot-pull-request.ymlcan't mint an App token and the PR will never auto-merge.
- If the codegen workflows require additional secrets (e.g. third-party API keys), register them in the Actions store; if a Dependabot-triggered workflow ever needs them, register them in the Dependabot store too.
- The App token is used by both the codegen workflow (
run-codegen-pull-request-task.yml) and every job inmerge-bot-pull-request.yml. App-authored pushes/PRs trigger downstreampull_requestandpushworkflow events directly - unlikeGITHUB_TOKEN-authored events, which are blocked by GitHub's recursion guard. This matters for two reasons: bot-opened PRs trigger thetest-pull-request.ymlsmoke build (so they can't auto-merge unvalidated), and - whenPUBLISH_ON_MERGEis enabled - the merge commit triggerspublish-release.yml. App-authored events also let the codegen workflow's auto-merge fire directly. - The codegen auto-merge condition in
merge-bot-pull-request.yml(merge-codegenjob) requires:- Event is
openedorreopened- auto-merge is enabled once per PR at open time; subsequentsynchronizeevents do not re-enable. This is what lets thedisable-auto-merge-on-maintainer-pushsafeguard (below) stick. github.event.pull_request.user.login == 'ptr727-codegen[bot]'- PR was opened by the App.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 acodegen-developbranch intomainor vice versa.
- Event is
- The
disable-auto-merge-on-maintainer-pushjob inmerge-bot-pull-request.ymlruns onsynchronizeevents against bot-authored PRs (Dependabot or codegen) when the event actor is NOT the same bot - i.e. a maintainer pushed commits. It callsgh pr merge --disable-autoso the maintainer's commits don't auto-merge along with the bot's content. Re-enable auto-merge manually (gh pr merge --auto <PR>or the GitHub UI) when ready.
Codegen targets
mainANDdevelopin parallel (matrix inrun-codegen-pull-request-task.yml), so generated content lands on both branches independently without any back-merging. See AGENTS.md "Branching Model" for why this dual-target pattern beats develop-only-with-flow-through.- App name:
Codegen workflow schedule:
run-periodic-codegen-pull-request.ymlruns daily at 04:00 UTC (staggered two hours after the weekly publish), plus on-demand viaworkflow_dispatch. It uses the App token (CODEGEN_APP_CLIENT_ID+CODEGEN_APP_PRIVATE_KEY) to commit, open the PR asptr727-codegen[bot], and let the merge-bot auto-merge once CI passes. No PAT, no close/reopen dance. Daily is cheap in the default two-phase model - codegen merges only smoke-test; the weekly publish batches the actual release.
GitHub project settings:
- General:
- Default branch:
main - Pull requests - both merge methods enabled at the repo level so each branch ruleset can pick the right one (develop =
Squash, main =Merge):Allow merge commits✓ (required for develop → main releases)Allow squash merging✓ (required for feature → develop merges)Allow rebase merging- disabled (no flow uses it; the develop ruleset forbids it anyway)Always suggest updating pull request branchesAllow auto-merge
- Default branch:
- Rules / Rulesets - separate rulesets per branch. Develop and main intentionally diverge on two rules - allowed merge methods and
Require linear history.Require branches to be up to date before mergingis off on both for related-but-distinct reasons (below); everything else is shared.- Configure these by exporting the template's rulesets and re-importing them - do not hand-build the rules. The result must be exactly two rulesets named
developandmain(the names are load-bearing:AGENTS.mdand these docs reference them). Reconstructing each rule by hand is the step that has gone wrong on past ports.Step 0 - remove ALL legacy protection first. Delete every classic branch-protection rule (Settings → Branches) and every pre-existing or stray ruleset (Settings → Rules → Rulesets) - not just some - so enforcement isn't doubled or contradicted. This template uses rulesets only, configured exclusively by the JSON export/import in Steps 1-2 below; never hand-build the rules in the UI. Partial cleanup (leaving a stray ruleset or a classic rule behind) is what has gone wrong on past ports. Equivalent API:
# Delete classic branch protection if present (404 = none, which is fine) for b in main develop; do gh api -X DELETE "repos/<owner>/<repo>/branches/$b/protection" 2>/dev/null || true; done # List existing rulesets; delete any that are not the two created below gh api "repos/<owner>/<repo>/rulesets" --jq '.[] | "\(.id)\t\(.name)"' # gh api -X DELETE "repos/<owner>/<repo>/rulesets/<id>"Step 1 - export the template's two rulesets, keeping only the re-importable fields (the GET response also carries
id, timestamps,_links,source, etc. that a create call rejects):for name in develop main; do id=$(gh api repos/ptr727/ProjectTemplate/rulesets --jq ".[] | select(.name==\"$name\") | .id") gh api "repos/ptr727/ProjectTemplate/rulesets/$id" \ --jq '{name, target, enforcement, bypass_actors, conditions, rules}' > "$name-ruleset.json" doneStep 2 - import into the new repo:
for name in develop main; do gh api -X POST "repos/<owner>/<repo>/rulesets" --input "$name-ruleset.json" doneCaveats:
bypass_actorsuses the Admin repository role (actor_id: 5), a global GitHub id that ports across repos as-is. The required status-check context (Check pull request workflow status) is matched by name and only turns green aftertest-pull-request.ymlhas run at least once.gh rulesetis read-only (list/view) - creation must go throughgh api -X POSTas above. If a field is rejected, edit the JSON and re-run the import.Renaming or updating an existing ruleset needs a FULL-payload PUT, not a partial one.
gh api -X PUT "repos/<owner>/<repo>/rulesets/<id>" -f name=develop(name only) fails with422 Unexpected parameter 'allowed_dismissal_actors': GitHub re-validates the storedpull_requestrule on a partial update, and that rule carries fields the GET response does not return. To rename (e.g. legacyDevelop/Main→develop/main) or otherwise edit a ruleset, GET it, change the field, and PUT the whole{name, target, enforcement, bypass_actors, conditions, rules}back (the same writable-field subset used for export above). Back up the GET first and verify afterward that the rule types,required_signatures,non_fast_forward, and the required status-check context are all still present. Renaming is safe for enforcement - the required status-check binds by check name, not ruleset name, so a rename won't break CI - but the template still expects the exactdevelop/mainnames thatAGENTS.mdand these docs reference (which is precisely what a legacyDevelop/Mainrepo is renaming to); the rename removes inconsistency, it isn't a license for arbitrary names.Migrating a brownfield repo with unsigned history. The shared
Require signed commitsrule (below) rejects any commit made before signing was enabled, so on a pre-existing repo the firstdevelop -> mainrelease is blocked the moment it tries to introduce that legacy history. The fix is to re-sign the legacy commits, but that rewrite is a non-fast-forward and theBlock force pushesrule rejects it - and the ruleset's admin bypass does not covergit push --force(GitHub honors ruleset bypass for UI/API operations, not git force-push). So even the owner cannot complete the re-sign without temporarily relaxing the ruleset. This is a one-time, maintainer-performed manual migration - it deliberately uses the force-push that AGENTS.md "Git and Commit Rules" forbids agents from running, so an AI agent must never execute this procedure; surface it to the maintainer instead. Procedure:Re-sign the divergent history, preserving merge topology. Prefer a rebase, which re-signs each commit with your current key (
commit.gpgsign/-S):git rebase --rebase-merges --exec 'git commit --amend --no-edit -S' <merge-base>git filter-branchalso works but is deprecated upstream (it prints a warning; suppress withFILTER_BRANCH_SQUELCH_WARNING=1, or usegit filter-repoif installed) - keep it only as a fallback:git filter-branch -f --commit-filter 'git commit-tree -S "$@"' -- <merge-base>..HEADTemporarily set the
develop(andmainif it diverged) ruleset Enforcement to Disabled (Settings → Rules → Rulesets), since the admin bypass won't permit the force-push.(Maintainer only) Force-push the re-signed branch. This is the single manual force-push the template sanctions; agents must never run it (see AGENTS.md "Git and Commit Rules").
Re-enable Enforcement.
Alternatively, enable
Require signed commitsonly on a repo whose full history is already signed - greenfield repos created from this template (where signing is live before the first commit, per AGENTS.md "Git and Commit Rules") never hit this.The per-branch settings below are the reference for what each ruleset contains and why (and the manual fallback if you configure via the UI):
- "Develop":
- Target branches:
develop. - Allowed merge methods:
Squash Require linear history(develop is kept linear; main carries merge commits by design, so this setting belongs to develop only)Require status checks to pass→Require branches to be up to date before mergingintentionally OFF. Leaving it on stalls bot auto-merge when two bot PRs against develop land within the same window - the first merge flips the second tomergeStateStatus: BEHIND, and GitHub's auto-merge will not fire while strict is on. The merge-bot in.github/workflows/merge-bot-pull-request.ymlonly enables auto-merge onopened/reopenedand never auto-updates bot branches; Dependabot's rebase isn't real-time. With strict off, squash mechanics still rebase the diff onto develop's tip on merge,Require linear historystill enforces linearity, textual conflicts still blockmergeable: CONFLICTING, and the requiredCheck pull request workflow statusstill gates merges. See AGENTS.md "Branching Model" for the full reasoning.- Plus shared settings (below).
- Target branches:
- "Main":
- Target branches:
main. - Allowed merge methods:
Merge Require status checks to pass→Require branches to be up to date before mergingintentionally OFF. This rule is incompatible with the forward-only develop model. GitHub's "up to date" check is graph-based: it asks whether main's tip commit is reachable from develop. After any develop → main release, main's new tip is a brand-new merge commit that develop's history doesn't contain. Forward-only develop never adds it (no back-merge of main into develop, no rebase of develop onto main), so the check fails permanently on every subsequent release. Leaving the rule on would force every release through an admin bypass. See AGENTS.md "Branching Model" for the full reasoning.- Plus shared settings (below).
- Target branches:
- Shared settings (apply to both rulesets):
Restrict deletionsRequire signed commitsRequire a pull request before mergingDismiss stale pull request approvals when new commits are pushedRequire conversation resolution before merging
Require status checks to pass- Status checks that are required:
Check pull request workflow status
- Status checks that are required:
Block force pushesAutomatically request Copilot code reviewReview new pushesReview draft pull requests
- Configure these by exporting the template's rulesets and re-importing them - do not hand-build the rules. The result must be exactly two rulesets named
- Actions / General:
Allow GitHub Actions to create and approve pull requests
Template - Branching Workflow
See AGENTS.md "Branching Model" for the authoritative definition. Summary:
- Persistent
mainanddevelopbranches, each with its own ruleset (above). Both must always be building error free. - Feature branches off
develop. Only commit on feature branches, never directly todevelopormain. - Feature →
develop: squash-merge (develop ruleset enforces this; develop is kept linear). develop→main: merge-commit (preserves develop's commit list as a real second-parent reference on main; main ruleset enforces this).developis forward-only. Nomain -> developback-merges. The develop squash-only ruleset physically blocks merge commits.- A
develop -> mainrelease requires a develop version bump right after. Once the merge lands and main's publish completes, raise theversionminor inversion.jsonondevelopvia an isolatedbump-version-X.YPR, so develop's prereleases stay numerically above main's last stable. Adevelop -> mainpromotion that carries only maintenance (not a release) holds main's version instead (git checkout main -- version.jsonon the promotion branch). See AGENTS.md "Release Model". - Bots open parallel PRs against both branches.
.github/dependabot.ymlduplicates each ecosystem entry per branch, and.github/workflows/run-codegen-pull-request-task.ymlruns as a matrix (branch namescodegen-mainandcodegen-develop). Each branch absorbs its own bot PRs independently - neither falls behind, no back-merges needed. - Review-then-merge loop. Every PR is reviewed by GitHub Copilot. The agent pushes, re-requests a review on the new head (via the
requestReviewsGraphQL mutation), addresses and resolves each finding, repeats until green, and then waits for the maintainer's explicit permission to merge - it does not self-merge. See AGENTS.md "PR Review Etiquette" and the Copilot Review Runbook for the mechanics.
Template - Release Distribution Model: Two-Phase by Default
This template ships with a two-phase model that decouples merging from publishing:
- Pull requests smoke-test only.
.github/workflows/test-pull-request.ymlalways runs unit tests, then path-gates a reduced build of only the targets a PR touches (dorny/paths-filter): Docker aslinux/amd64only (no QEMU/arm64), the executable as a representative runtime subset, and nothing is pushed. A docs-only PR runs unit tests alone; a Dependabot github-actions bump is unit-tests-only. This is fast feedback, not a release. - Merges to
main/developdo not publish. A push only smoke-tested the PR; merging it republishes nothing. - The weekly schedule + manual dispatch are the sole publishers.
.github/workflows/publish-release.ymlruns every Monday 02:00 UTC and on-demand viaworkflow_dispatch, and on either trigger does the full build/publish of bothmain(Release /latest/ non-prerelease) anddevelop(Debug /develop/ prerelease) - GitHub release, NuGet/PyPI uploads, multi-arch Docker tags, platform executables, and a refreshed Docker base image. Trigger a release on demand from the Actions UI when you want one between weekly runs.
This batches cheap bot churn (Dependabot/codegen merge daily, validated by smoke builds) into one periodic publish instead of one release per merge, and keeps PR feedback fast by deferring the slow arm64/full-matrix builds to the publisher. A no-op weekly run (no new commit, so an unchanged SemVer2) re-pushes nothing to GitHub Releases / NuGet / PyPI - only Docker re-pushes, to pick up upstream base-image refreshes.
Reusing the Release Pipeline in a Derived Project (Any Language)
The pipeline is built in two layers so you only customize one of them:
- Orchestration (sync verbatim - don't rewrite): the publish plan and branch matrix in
publish-release.yml, the version step (get-version-task.yml), the date badge (build-datebadge-task.yml), and - the key part - thegithub-releasejob that tags the built commit, creates the GitHub Release, and attaches assets. It collects assets by the patternrelease-asset-<branch>-*and never names a build job, so it works unchanged no matter what you ship. - Build (you own these): the
build-<target>-task.ymlleaf tasks. Each one builds an output and either pushes it to a registry, uploads arelease-asset-<branch>-<name>artifact for the GitHub Release, or both.
The one rule: to put a file on the GitHub Release, upload it as an artifact named release-asset-<branch>-<name>. That's the seam - implement it in a leaf task and the rest of the release just works.
Customize by where your outputs go, not by language:
| What you ship | What to do | Goes to |
|---|---|---|
| A zip / packaged files / a binary on the GitHub Release (e.g. a data or asset library) | One leaf task: validate → zip → upload release-asset-<branch>-library |
GitHub Release asset |
| A NuGet package | Keep/adapt build-nugetlibrary-task (it dotnet nuget pushes and uploads a release-asset-*) |
NuGet.org + GitHub Release asset |
| A PyPI package | Keep/adapt build-pypilibrary-task (build + artifact) and the publish-pypi job in publish-release.yml (OIDC upload) |
PyPI only |
| A Docker image | Keep/adapt build-docker-task (pushes multi-arch tags) |
Docker Hub only |
| A compiled app/CLI | Keep/adapt build-executable-task - note it is specifically dotnet publish; replace it wholesale for another toolchain |
GitHub Release asset |
| Just validate + tag a release (no build output) | Put your checks in test-pull-request.yml; attach a release-asset-* only if you have a file |
GitHub Release (tag, optionally an asset) |
For each output you don't ship, delete its build-<target>-task.yml, its job + needs entry in build-release-task.yml, its test-pull-request.yml path filter, and (PyPI) the publish-pypi job. So build-release-task.yml is edited to reflect your set of leaf jobs, but its github-release logic stays as-is - that's the part you reuse rather than fork. get-version-task.yml installs the .NET SDK to run NBGV (which produces the version/tag) even in a non-.NET repo. See AGENTS.md "Release Model" for the full seam contract and the no-op-republish guarantee.
Opt in to publish-on-merge. Set the repository variable PUBLISH_ON_MERGE to true (Settings → Secrets and variables → Actions → Variables) to switch to the continuous-release model: every push/merge to main publishes main and every push to develop publishes develop, immediately. The weekly and manual publishers also run. Leave the variable unset (or false) for the two-phase default. It's a repository variable, not a workflow edit, so pulling template updates never conflicts with your choice.
Which to pick: two-phase suits projects whose consumers are pushed updates (HACS for Home Assistant, package managers that auto-update, Linux distros that vendor from main) where every release is a forced update and frequent bot-driven releases are noise. PUBLISH_ON_MERGE=true suits projects whose consumers pull at their own cadence (Docker pulls, NuGet/PyPI installs, manual downloads) and want every merged change available immediately. For an example of a push-distribution project, see homeassistant-purpleair (ships through HACS).
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.90 | 45 | 6/15/2026 |
| 1.0.82 | 42 | 6/15/2026 |
| 1.0.75 | 95 | 6/8/2026 |
| 1.0.70 | 88 | 6/8/2026 |
| 1.0.64 | 100 | 6/4/2026 |
| 1.0.62 | 98 | 6/1/2026 |
| 1.0.60 | 94 | 5/25/2026 |
| 1.0.59-gad39ee1061 | 89 | 6/1/2026 |
| 1.0.58 | 91 | 5/25/2026 |
| 1.0.58-g427b88a37b | 95 | 5/25/2026 |
| 1.0.57 | 103 | 5/18/2026 |
| 1.0.57-g9c1f39e442 | 93 | 5/25/2026 |
| 1.0.56-gd013abf7ef | 89 | 5/24/2026 |
| 1.0.55 | 95 | 5/13/2026 |
| 1.0.55-gbd3876e3dc | 96 | 5/18/2026 |
| 1.0.54 | 102 | 5/12/2026 |
| 1.0.54-ga03ad12524 | 92 | 5/13/2026 |
| 1.0.53-gd450626890 | 90 | 5/12/2026 |
| 1.0.52 | 97 | 5/12/2026 |
| 1.0.52-gbb66475387 | 91 | 5/12/2026 |