Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
37b10b4
Rename Library project to NuGetLibrary
ptr727 May 3, 2026
5243352
Add devcontainer + per-OS host and SSH signing docs
ptr727 May 3, 2026
5155075
Add PyPiLibrary Python sibling project
ptr727 May 3, 2026
410f174
Address Copilot review on PR #63
ptr727 May 3, 2026
aa0a1e7
Drop USERPROFILE concat from devcontainer mount sources
ptr727 May 3, 2026
0334729
Address Copilot review on PR #64
ptr727 May 3, 2026
2e9dbb4
Restore HOME/USERPROFILE fallback for devcontainer mount sources
ptr727 May 3, 2026
4c939f6
Move PyPi publish job out of reusable workflow chain
ptr727 May 3, 2026
43d2bbf
Merge remote-tracking branch 'origin/develop' into pypilibrary
ptr727 May 3, 2026
6b95dd4
Address Copilot review on PR #63
ptr727 May 3, 2026
6dbe5ba
Address Copilot review on PR #64
ptr727 May 3, 2026
e2626a2
Drop hard-coded ruff and Python interpreter paths from workspace
ptr727 May 3, 2026
07a101a
Enforce uv version pin even when uv is already installed
ptr727 May 3, 2026
692f27e
Make publish-pypi permissions explicit for download-artifact
ptr727 May 3, 2026
b0cfcb1
Merge remote-tracking branch 'origin/develop' into pypilibrary
ptr727 May 3, 2026
ba24ee0
Address Copilot review on PR #63 (post-merge round)
ptr727 May 3, 2026
dd7073d
Drop ms-pyright.pyright; mark mypy/pylint/black/etc as unwanted
ptr727 May 3, 2026
a1092d2
Drop ms-pyright.pyright from devcontainer extension list
ptr727 May 3, 2026
6067aa3
Pin uv in CI; skip-existing on PyPI publish; document version scheme
ptr727 May 3, 2026
a1b4d4b
Restructure agent docs for polyglot template; add review runbook
ptr727 May 3, 2026
6ee7ccb
Merge remote-tracking branch 'origin/develop' into pypilibrary
ptr727 May 3, 2026
b77a936
Address Copilot review on PR #64
ptr727 May 3, 2026
176bb4d
Use official PyPI capitalization in prose; consistent plural
ptr727 May 3, 2026
89c7f0a
Tighten workflow naming conventions; fix one step name deviation
ptr727 May 3, 2026
22db3bc
Fix dependabot.yml path in deletion checklists
ptr727 May 3, 2026
e76aab0
Bump default Python target to 3.14; fix pyright per-path strict config
ptr727 May 3, 2026
ab57f01
Add PEP 561 py.typed; alphabetize README defs; canonical PyPI URL
ptr727 May 3, 2026
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
65 changes: 65 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"name": "ProjectTemplate",
"image": "mcr.microsoft.com/devcontainers/dotnet:1-10.0",

"features": {
"ghcr.io/devcontainers/features/common-utils:2": {},
"ghcr.io/devcontainers/features/github-cli:1": {}
},

"mounts": [
{
"source": "${localEnv:HOME}${localEnv:USERPROFILE}/.ssh/id_ed25519.pub",
"target": "/home/vscode/.ssh/id_ed25519.pub",
"type": "bind",
"readonly": true
},
{
"source": "${localEnv:HOME}${localEnv:USERPROFILE}/.config/git/allowed_signers",
"target": "/home/vscode/.config/git/allowed_signers",
"type": "bind",
"readonly": true
},
{
"source": "${localEnv:HOME}${localEnv:USERPROFILE}/.config/gh",
Comment thread
ptr727 marked this conversation as resolved.
Comment thread
ptr727 marked this conversation as resolved.
"target": "/home/vscode/.config/gh",
"type": "bind",
"readonly": false
}
],

"remoteUser": "vscode",
// workspaceFolder defaults to /workspaces/${localWorkspaceFolderBasename},
// which makes the devcontainer config portable: when this template is
// forked into a repo with a different folder name, the mount path tracks
// the host folder name automatically.

// The bind-mount on macOS hosts surfaces /home/vscode/.ssh as root-owned;
// chown it back so writes from inside the container (known_hosts updates
// by gh / git) land cleanly. Idempotent on Linux/WSL2.
"onCreateCommand": "sudo install -d -m 700 -o vscode -g vscode /home/vscode/.ssh",

// Install uv for the Python sibling project, restore .NET local tools,
// and install the husky git hooks. uv is installed under $HOME/.local/bin
// and added to PATH by uv's install script.
"postCreateCommand": ".devcontainer/post-create.sh",

"customizations": {
"vscode": {
"extensions": [
"csharpier.csharpier-vscode",
"davidanson.vscode-markdownlint",
"editorconfig.editorconfig",
"github.vscode-github-actions",
"gruntfuggly.todo-tree",
"ms-azuretools.vscode-docker",
"ms-dotnettools.csdevkit",
"streetsidesoftware.code-spell-checker",
"yzhang.markdown-all-in-one",
"ms-python.python",
"charliermarsh.ruff",
"ms-pyright.pyright"
]
}
}
}
26 changes: 26 additions & 0 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -euo pipefail

# Install uv (Astral) for the Python sibling project. Idempotent — re-running
# overwrites in place. The installer drops the binary in $HOME/.local/bin and
# updates user shell init to add it to PATH for new shells; we add it to the
# current PATH explicitly so the rest of this script can invoke `uv` without a
# hard-coded path.
if ! command -v uv >/dev/null 2>&1; then
curl -LsSf https://astral.sh/uv/install.sh | sh
export PATH="$HOME/.local/bin:$PATH"
fi

# Restore the .NET local-tool manifest (CSharpier, Husky.Net, dotnet-outdated).
dotnet tool restore

# Install Husky.Net git hooks so commits run pre-commit checks. Failures here
# (e.g. missing .git directory, broken tool restore) should surface — the
# devcontainer setup is not "successful" if hook installation fails silently.
dotnet husky install
Comment thread
ptr727 marked this conversation as resolved.

# Pre-warm uv environment for PyPiLibrary if it exists. Guarded so this script
# is safe before PyPiLibrary lands in the repo.
if [[ -f PyPiLibrary/pyproject.toml ]]; then
(cd PyPiLibrary && uv sync)
fi
12 changes: 6 additions & 6 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

**ProjectTemplate** is a C# .NET template project that demonstrates best practices for C# .NET development. The project includes:

- **Library**: Core library with AOT compatibility (`Library.csproj`)
- **NuGetLibrary**: Core .NET NuGet library with AOT compatibility (`NuGetLibrary.csproj`, published as `ptr727.ProjectTemplate.Library`)
- **Console**: Command-line application using System.CommandLine (`Console.csproj`)
- **Tests**: Unit tests using xUnit and AwesomeAssertions (`Tests.csproj`)
- **Benchmarks**: Performance benchmarks using BenchmarkDotNet (`Benchmarks.csproj`)
Expand Down Expand Up @@ -43,7 +43,7 @@ Available VS Code tasks (use via `run_task` tool):

1. **File-Scoped Namespaces**: Always use file-scoped namespaces
```csharp
namespace ptr727.ProjectTemplate.Library;
namespace ptr727.ProjectTemplate.NuGetLibrary;
```

2. **Nullable Reference Types**: Enabled (`<Nullable>enable</Nullable>`)
Expand Down Expand Up @@ -92,7 +92,7 @@ Available VS Code tasks (use via `run_task` tool):
```

4. **Namespace**: Follow format `ptr727.ProjectTemplate.<ProjectName>`
- Library: `ptr727.ProjectTemplate.Library`
- NuGetLibrary: `ptr727.ProjectTemplate.NuGetLibrary`
- Console: `ptr727.ProjectTemplate.Console`
- Tests: `ptr727.ProjectTemplate.Tests`

Expand All @@ -110,7 +110,7 @@ Available VS Code tasks (use via `run_task` tool):
```csharp
using System.CommandLine;
using System.Runtime.CompilerServices;
using ptr727.ProjectTemplate.Library;
using ptr727.ProjectTemplate.NuGetLibrary;

namespace ptr727.ProjectTemplate.Console;
```
Expand Down Expand Up @@ -211,7 +211,7 @@ Available VS Code tasks (use via `run_task` tool):

1. **Target Framework**: .NET 10.0 (`<TargetFramework>net10.0</TargetFramework>`)

2. **AOT Compatibility**: Library is AOT compatible
2. **AOT Compatibility**: NuGetLibrary is AOT compatible
- `<IsAotCompatible>true</IsAotCompatible>`
- `<VerifyReferenceAotCompatibility>true</VerifyReferenceAotCompatibility>`

Expand Down Expand Up @@ -292,7 +292,7 @@ Available VS Code tasks (use via `run_task` tool):
- `CodeGen/` - Code generation utilities (internal tooling)
- `Console/` - Console/CLI application using System.CommandLine
- `Docker/` - Docker build scripts and Dockerfile
- `Library/` - Core reusable library
- `NuGetLibrary/` - Core reusable .NET NuGet library (published as `ptr727.ProjectTemplate.Library`)
- `Tests/` - Unit tests using xUnit and AwesomeAssertions

## Best Practices
Expand Down
56 changes: 33 additions & 23 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:

# main
- package-ecosystem: "nuget"
target-branch: "main"
directory: "/"
schedule:
interval: "daily"
groups:
nuget-deps:
patterns:
- "*"
- package-ecosystem: "github-actions"
target-branch: "main"
directory: "/"
schedule:
interval: "daily"
groups:
actions-deps:
patterns:
- "*"
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:

- package-ecosystem: "nuget"
target-branch: "main"
directory: "/"
schedule:
interval: "daily"
groups:
nuget-deps:
patterns:
- "*"

- package-ecosystem: "github-actions"
target-branch: "main"
directory: "/"
schedule:
interval: "daily"
groups:
actions-deps:
patterns:
- "*"

- package-ecosystem: "uv"
target-branch: "main"
directory: "/PyPiLibrary"
schedule:
interval: "daily"
groups:
pypi-deps:
patterns:
- "*"
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
name: Build library task
name: Build NuGet library task

on:
workflow_call:
inputs:
# Input to control whether to push the library to NuGet.org
# 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-library.outputs.artifact-id }}
value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }}

jobs:

Expand All @@ -20,8 +20,8 @@ jobs:
uses: ./.github/workflows/get-version-task.yml
secrets: inherit

build-library:
name: Build library project job
build-nugetlibrary:
name: Build NuGet library project job
runs-on: ubuntu-latest
outputs:
artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }}
Expand All @@ -37,9 +37,9 @@ jobs:
- name: Checkout code step
uses: actions/checkout@v6

- name: Build library project step
- name: Build NuGet library project step
run: |
dotnet build ./Library/Library.csproj \
dotnet build ./NuGetLibrary/NuGetLibrary.csproj \
-property:OutputPath=${{ runner.temp }}/publish/ \
-property:PackageOutputPath=${{ runner.temp }}/publish/ \
--configuration ${{ github.ref_name == 'main' && 'Release' || 'Debug' }} \
Expand All @@ -58,11 +58,11 @@ jobs:
--skip-duplicate

- name: Zip output step
run: 7z a -t7z ${{ runner.temp }}/Library.7z ${{ runner.temp }}/publish/*
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: library-build
path: ${{ runner.temp }}/Library.7z
name: nugetlibrary-build
path: ${{ runner.temp }}/NuGetLibrary.7z
66 changes: 66 additions & 0 deletions .github/workflows/build-pypilibrary-task.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
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:

build-pypilibrary:
name: Build PyPi library project job
runs-on: ubuntu-latest
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@v6

- name: Setup uv step
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
Comment thread
ptr727 marked this conversation as resolved.
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

- name: Build sdist and wheel step
run: uv build

- name: Upload build artifacts step
id: artifact-upload-step
uses: actions/upload-artifact@v6
with:
name: pypilibrary-build
path: PyPiLibrary/dist/*
21 changes: 15 additions & 6 deletions .github/workflows/build-release-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,23 @@ jobs:
uses: ./.github/workflows/get-version-task.yml
secrets: inherit

build-library:
name: Build library job
uses: ./.github/workflows/build-library-task.yml
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

Comment thread
ptr727 marked this conversation as resolved.
Outdated
Comment thread
ptr727 marked this conversation as resolved.
Outdated
build-executable:
name: Build executable job
uses: ./.github/workflows/build-executable-task.yml
Expand All @@ -51,17 +60,17 @@ jobs:
name: Publish GitHub release job
if: ${{ inputs.github }}
runs-on: ubuntu-latest
needs: [get-version, build-library, build-executable, build-docker]
needs: [get-version, build-nugetlibrary, build-pypilibrary, build-executable, build-docker]

steps:

- name: Checkout code step
uses: actions/checkout@v6

- name: Download library build artifacts step
- name: Download NuGet library build artifacts step
uses: actions/download-artifact@v7
with:
artifact-ids: ${{ needs.build-library.outputs.artifact-id }}
artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }}
path: ./Publish

- name: Download executable build artifacts step
Expand Down
Loading
Loading