diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 082a7b06..b1d48d8c 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,380 +1,139 @@ -# GitHub Copilot Instructions for ProjectTemplate - -## Project Overview - -**ProjectTemplate** is a C# .NET template project that demonstrates best practices for C# .NET development. The project includes: - -- **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`) -- **Docker**: Docker build configurations for Linux containers - -## Build Requirements - -### Zero Warnings Policy - -**CRITICAL**: All builds must complete without warnings. The project enforces this through: - -1. **VS Code Task**: The `.Net Format` task must run successfully with `--verify-no-changes` flag - - Command: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed` - - This task must pass before any code is committed - - Task dependencies: `CSharpier Format` → `.Net Build` → `.Net Format` - -2. **Analysis Level**: Projects use `latest-all` - - All .NET analyzers enabled: `true` - - Analyzer severity: `suggestion` (but must be addressed) - -3. **Husky.Net Pre-commit Hooks**: Automated checks run before commits - -### Build Tasks - -Available VS Code tasks (use via `run_task` tool): -- `.Net Build`: Build with diagnostic verbosity -- `.Net Format`: Verify formatting and style (must pass) -- `CSharpier Format`: Auto-format code with CSharpier -- `.Net Tool Update`: Update dotnet tools -- `.Net Outdated Upgrade`: Upgrade outdated NuGet dependencies (interactive prompt) -- `Husky.Net Run`: Run pre-commit hooks manually - -## Coding Standards and Conventions - -### C# Language Features - -1. **File-Scoped Namespaces**: Always use file-scoped namespaces - ```csharp - namespace ptr727.ProjectTemplate.NuGetLibrary; - ``` - -2. **Nullable Reference Types**: Enabled (`enable`) - - Always use nullable annotations appropriately - - Use `required` modifier for mandatory properties - -3. **Modern C# Features**: Prefer modern language constructs - - Primary constructors when appropriate - - Top-level statements for console apps - - Pattern matching over traditional checks - - Collection expressions when types loosely match - - Extension methods using `extension()` syntax (C# 13) - - Implicit object creation when type is apparent - - Range and index operators - -4. **Expression-Bodied Members**: Use for all applicable members - - Methods, properties, accessors, operators, lambdas, local functions - -5. **var Keyword**: Do NOT use `var` - always use explicit types - ```csharp - // Correct - int count = 42; - string name = "test"; - - // Incorrect - var count = 42; - var name = "test"; - ``` - -### Naming Conventions - -1. **Private Fields**: Use underscore prefix with camelCase - ```csharp - private readonly HttpClient _httpClient; - private int _counter; - ``` - -2. **Static Fields**: Use `s_` prefix with camelCase - ```csharp - private static int s_instanceCount; - ``` - -3. **Constants**: Use PascalCase - ```csharp - private const int MaxRetries = 3; - ``` - -4. **Namespace**: Follow format `ptr727.ProjectTemplate.` - - NuGetLibrary: `ptr727.ProjectTemplate.NuGetLibrary` - - Console: `ptr727.ProjectTemplate.Console` - - Tests: `ptr727.ProjectTemplate.Tests` - -### Code Structure - -1. **Global Usings**: Use `GlobalUsings.cs` for common namespaces - ```csharp - global using System; - global using System.Net.Http; - global using System.Threading.Tasks; - global using Serilog; - ``` - -2. **Usings Placement**: Outside namespace, sorted with System directives first - ```csharp - using System.CommandLine; - using System.Runtime.CompilerServices; - using ptr727.ProjectTemplate.NuGetLibrary; - - namespace ptr727.ProjectTemplate.Console; - ``` - -3. **Braces**: New line before all braces (Allman style) - ```csharp - public void Method() - { - if (condition) - { - // code - } - } - ``` - -4. **Indentation**: - - C# files: 4 spaces - - XML/csproj files: 2 spaces - - YAML files: 2 spaces - - JSON files: 4 spaces - -5. **Line Endings**: - - C#, XML, YAML, JSON, Windows scripts: CRLF - - Linux scripts (.sh): LF - -### Comments and Documentation - -1. **XML Documentation**: Generate documentation files - - `true` - - Missing XML comments for public APIs are suppressed (NoWarn 1591) - -2. **Code Analysis Suppressions**: Use attributes with justifications - ```csharp - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Design", - "CA1034:Nested types should not be visible", - Justification = "https://github.com/dotnet/sdk/issues/51681" - )] - ``` - -3. **Spelling**: All code must pass the Code Spell Checker extension - - Configure exceptions in workspace settings if needed - - British and American spelling both accepted - -4. **Markdown Quality**: Markdown files must pass Markdownlint - - Proper heading hierarchy, spacing, and formatting - - -### Error Handling and Logging - -1. **Serilog Logging**: Use structured logging with Serilog - ```csharp - logger.Error(exception, "{Function}", function); - ``` - -2. **CallerMemberName**: Use for automatic function name tracking - ```csharp - public bool LogAndPropagate( - Exception exception, - [CallerMemberName] string function = "unknown" - ) - ``` - -3. **Extension Methods**: Use for logger extensions - ```csharp - extension(ILogger logger) - { - public bool LogAndPropagate(Exception exception, ...) { } - } - ``` - -### Testing Conventions - -1. **Test Framework**: xUnit with AwesomeAssertions - ```csharp - [Fact] - public void MethodName_Scenario_ExpectedBehavior() - { - // Arrange - int expected = 42; - - // Act - int actual = GetValue(); - - // Assert - actual.Should().Be(expected); - } - ``` - -2. **Test Organization**: Arrange-Act-Assert pattern -3. **Test Naming**: Use descriptive names with underscores separating parts -4. **Theory Tests**: Use `[Theory]` with `[InlineData]` for parameterized tests -5. **Avoid Regions**: Don't use regions in test files -6. **Logical Grouping**: Organize tests in separate files by feature or class - - -### Project Configuration - -1. **Target Framework**: .NET 10.0 (`net10.0`) - -2. **AOT Compatibility**: NuGetLibrary is AOT compatible - - `true` - - `true` - -3. **Assembly Information**: - - Use semantic versioning - - Include SourceLink: `true` - - Embed untracked sources: `true` - -4. **Internal Visibility**: Use `InternalsVisibleTo` for test and console access - ```xml - - - - - ``` - -5. **Directory.Build.props**: Common MSBuild properties shared across all projects - (`TargetFramework`, `Nullable`, `ImplicitUsings`, `AnalysisLevel`, `AnalysisMode`, - `EnableNETAnalyzers`, `ArtifactsPath`, `IsPackable`, `ManagePackageVersionsCentrally`) - live here at the solution root. Only add a property to a `.csproj` when it is - specific to that project or requires an explicit override of the shared default. - -6. **Directory.Packages.props**: All NuGet package versions are centralised here via - `PackageVersion` items. Individual `.csproj` files use `PackageReference Include="..."` - with no `Version` attribute. Asset metadata (`PrivateAssets`, `IncludeAssets`) stays - in the `.csproj` `PackageReference` element. Use `VersionOverride` only when a project - genuinely requires a different version from the central default. - -### Code Formatting Tools - -1. **CSharpier**: Primary code formatter - - Run before committing: `dotnet csharpier format --log-level=debug .` - -2. **dotnet format**: Style verification - - Verify no changes: `dotnet format style --verify-no-changes --severity=info --verbosity=detailed` - -3. **Husky.Net**: Git hooks for automated checks - - Installed via restore target in `.csproj` - - Pre-commit hooks run formatting checks - -## Dependencies and Packages - -### Core Dependencies - -- **CliWrap**: Command-line process execution -- **System.CommandLine**: Command-line argument parsing -- **Serilog**: Structured logging with sinks (Console, File, Async) -- **Microsoft.Extensions.Http.Resilience**: HTTP client with resilience -- **Microsoft.SourceLink.GitHub**: Source link for debugging - -### Testing Dependencies - -- **xUnit**: Test framework -- **AwesomeAssertions**: Fluent assertion library -- **BenchmarkDotNet**: Performance benchmarking - -### Development Tools - -- **CSharpier**: Code formatter -- **Husky.Net**: Git hooks -- **dotnet-outdated-tool**: Dependency update checks -- **Nerdbank.GitVersioning**: Version management - -## Docker - -- Base images: Ubuntu Rolling -- Multi-platform support: linux/amd64, linux/arm64 -- Build script: `Build.sh` -- Debug tools: `InstallDebugTools.sh` - -## Project Structure - -- `.config/` - .NET tools configuration -- `.github/` - GitHub Actions workflows and Copilot instructions -- `.husky/` - Husky.Net git hooks -- `.vscode/` - Visual Studio Code settings and launch configurations -- `Benchmarks/` - BenchmarkDotNet performance measurement project -- `CodeGen/` - Code generation utilities (internal tooling) -- `Console/` - Console/CLI application using System.CommandLine -- `Docker/` - Docker build scripts and Dockerfile -- `NuGetLibrary/` - Core reusable .NET NuGet library (published as `ptr727.ProjectTemplate.Library`) -- `Tests/` - Unit tests using xUnit and AwesomeAssertions - -## Best Practices - -1. **Immutability**: Prefer `readonly` and `required` for fields and properties -2. **Async/Await**: Use async patterns consistently -3. **Cancellation Tokens**: Support cancellation in async methods -4. **Parallel Processing**: Use `ParallelOptions` for controlled parallelism -5. **HTTP Clients**: Use `HttpClientFactory` for HTTP client creation -6. **Dispose Pattern**: Implement IDisposable/IAsyncDisposable when managing resources -7. **Static Analysis**: Address all analyzer warnings - zero warnings policy -8. **Code Reviews**: All changes go through pull requests -9. **Git Versioning**: Use Nerdbank.GitVersioning for version management -10. **No Regions**: Avoid code regions - use logical file separation instead - - -## Editor Configuration - -The project includes comprehensive `.editorconfig` settings that enforce: -- Character encoding (UTF-8) -- Indentation rules -- Line ending conventions -- C# style preferences -- Naming conventions -- Code analysis settings - -**Always respect the .editorconfig settings** - these are verified by the build process. - -## Git and Commit Rules - -**These rules are absolute — no exceptions:** - -- **Never make git commits.** All commits must be cryptographically signed (SSH/GPG). AI coding agents cannot produce signed commits. Stage changes with `git add` and leave `git commit` to the developer, who must run it in their own environment where signing keys are available. -- **Never force push.** Do not run `git push --force` or `git push --force-with-lease`. Force pushing rewrites shared branch history and is blocked by branch protection rules. -- **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 -2. **During development**: Use CSharpier for formatting as you go -3. **Before committing**: - - Run `.Net Format` task to verify compliance - - Husky hooks will run automatically -4. **Dependency updates**: Run `.Net Outdated Upgrade` task (`dotnet outdated --upgrade:prompt`) regularly -5. **Testing**: Run tests via VS Code test explorer or `dotnet test` - -## Reference Links - -- [Microsoft C# Coding Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions) -- [.NET Runtime Coding Style](https://github.com/dotnet/runtime/blob/main/docs/coding-guidelines/coding-style.md) -- [dotnet format Documentation](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-format) -- [EditorConfig Documentation](https://editorconfig.org) -- [CSharpier Documentation](https://csharpier.com) -- [Husky.Net Documentation](https://alirezanet.github.io/Husky.Net) -- [xUnit Documentation](https://xunit.net) -- [AwesomeAssertions Documentation](https://awesomeassertions.org/) -- [BenchmarkDotNet Documentation](https://benchmarkdotnet.org) -- [System.CommandLine Documentation](https://learn.microsoft.com/en-us/dotnet/standard/commandline/) -- [Serilog Documentation](https://serilog.net) - +# Copilot Instructions + +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. It covers project layout, branch flow, PR review etiquette, the release pipeline, devcontainer behavior, workflow YAML conventions, and what NOT to touch. + +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. + +For language-specific style rules, see: + +- .NET — [`CODESTYLE.md`](../CODESTYLE.md) at the repo root. +- Python — [`PyPiLibrary/CODESTYLE.md`](../PyPiLibrary/CODESTYLE.md). + +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. + +### 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 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 +``` + +## GitHub Copilot Review Runbook + +Use this section for provider-specific mechanics. The expected review loop *contract* (request review on every push, verify head-SHA coverage, triage findings, reply + resolve, escalate when stuck) is defined in [AGENTS.md → PR Review Etiquette](../AGENTS.md#pr-review-etiquette). This section only describes how to make GitHub Copilot reliably execute it. + +### 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. Request review explicitly through the GitHub PR UI (request `Copilot` as a reviewer) after every push. + +**Do NOT post `@Copilot review` as a PR comment.** That comment triggers the Copilot *coding agent* (`copilot-swe-agent[bot]`), which makes code changes rather than posting a review. + +Known non-working request paths (don't rely on them): + +- `POST /requested_reviewers` with `reviewers=[Copilot]` can return 200 but no-op. +- `copilot-pull-request-reviewer` as a requested reviewer slug returns 422. +- GraphQL `requestReviews` rejects Copilot's bot node. + +### Verify Review Covered Current Head + +Before merging, confirm Copilot reviewed the current PR head SHA. Copilot may respond as either a formal review (carries an exact commit SHA) or an issue comment (no SHA — use the most recent Copilot comment for manual confirmation). Check both. + +```sh +PR_HEAD=$(gh pr view --json headRefOid --jq '.headRefOid') + +# 1. Formal review — exact SHA match. +gh pr view --json reviews --jq \ + '.reviews[] | select(.author.login=="copilot-pull-request-reviewer") | .commit.oid' \ + | grep -q "$PR_HEAD" && echo "covered via formal review" + +# 2. Issue comment — show the most recent Copilot comment for manual confirmation. +gh api repos///issues//comments --jq \ + '[.[] | select(.user.login=="copilot-pull-request-reviewer")] | last | {created_at, body: .body[:200]}' +``` + +Coverage is confirmed when (1) exits 0. For issue comments (path 2), body content is the only reliable signal — `created_at` is not: `git log -1 --format=%cI` is the **commit** timestamp, not the push timestamp, so amended or rebased commits can have an earlier timestamp and an older Copilot comment could satisfy a time check even though Copilot never saw the current head. Treat path (2) as confirmed only when the comment body explicitly refers to the current changes. + +### Bounded Retry Workflow + +If a review did not run on the current head, retry: + +1. Wait briefly and check head-SHA coverage (see above). +1. Request review again via the GitHub PR UI. +1. Retry up to two more times (three total). +1. If still missing, mark review as blocked and escalate to the user/maintainer with what was attempted. + +### Reply and Thread Resolution Workflow + +List unresolved threads. Use `first: 100` with cursor-based pagination; if `hasNextPage` is true, re-run with `after: ""` to retrieve the next page: + +```sh +gh api graphql -f query=' +{ + repository(owner: "", name: "") { + pullRequest(number: ) { + reviewThreads(first: 100) { + nodes { + id isResolved path + comments(first: 1) { nodes { author { login } body } } + } + pageInfo { hasNextPage endCursor } + } + } + } +}' | jq ' + .data.repository.pullRequest.reviewThreads | + (.pageInfo | "hasNextPage=\(.hasNextPage) endCursor=\(.endCursor)"), + (.nodes[] | select(.isResolved == false)) +' +``` + +Reply on a thread, then resolve it: + +```sh +gh api graphql -f query=' +mutation($threadId: ID!, $body: String!) { + addPullRequestReviewThreadReply(input: { pullRequestReviewThreadId: $threadId, body: $body }) { + comment { id } + } +}' -F threadId="PRRT_..." -F body="Fixed in : ." + +gh api graphql -f query=' +mutation($threadId: ID!) { + resolveReviewThread(input: { threadId: $threadId }) { thread { id isResolved } } +}' -F threadId="PRRT_..." +``` + +Issue-level Copilot comments (those in `issues//comments`) have no resolution action — GitHub provides no API or UI to resolve them. Reply if the finding warrants it; no resolution step is needed or possible. + +Reply-body conventions: + +- Accepted bug/style fix: include fixing commit SHA and a one-line summary. +- Declined style comment: cite the rule (AGENTS.md or language CODESTYLE) and the existing-tree precedent. +- Declined architecture proposal: one-sentence rationale. + +After the final push, sweep-resolve stale older threads for removed code paths. + +## When in Doubt + +Read [AGENTS.md](../AGENTS.md) for the full picture (release flow, files you must not touch, branching, workflow YAML, devcontainer). For language-specific rules, the per-language CODESTYLE files are authoritative. Don't restate any of these files' rules in commit bodies or PR descriptions — keep those focused on the change itself. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 21030f41..66d6bd2b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -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: + - "*" diff --git a/.github/workflows/build-pypilibrary-task.yml b/.github/workflows/build-pypilibrary-task.yml new file mode 100644 index 00000000..f7419fc7 --- /dev/null +++ b/.github/workflows/build-pypilibrary-task.yml @@ -0,0 +1,71 @@ +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: + # Pin uv to the same version as `.devcontainer/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 + + - 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/* diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml index 27a0f6b8..bf5d90c8 100644 --- a/.github/workflows/build-release-task.yml +++ b/.github/workflows/build-release-task.yml @@ -34,6 +34,15 @@ jobs: # 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 @@ -51,7 +60,7 @@ jobs: name: Publish GitHub release job if: ${{ inputs.github }} runs-on: ubuntu-latest - needs: [get-version, build-nugetlibrary, build-executable, build-docker] + needs: [get-version, build-nugetlibrary, build-pypilibrary, build-executable, build-docker] steps: diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index ac31c745..c3ba0e21 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -23,6 +23,47 @@ jobs: nuget: true dockerhub: true + publish-pypi: + name: Publish PyPI library job + 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@6733eb7d741f0b11ec6a39b58540dab7590f9b7d # 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] diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml index 8bbf5e06..394ebc9a 100644 --- a/.github/workflows/test-pull-request.yml +++ b/.github/workflows/test-pull-request.yml @@ -25,7 +25,7 @@ jobs: [ test-release ] if: always() steps: - - name: Check workflow results + - name: Check workflow results step run: | exit_on_result() { if [[ "$2" == "failure" || "$2" == "cancelled" ]]; then diff --git a/.gitignore b/.gitignore index 193ff244..8e1e603e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,13 @@ .artifacts .DS_Store *.user + +# Python / uv +__pycache__/ +*.py[cod] +*.egg-info/ +.venv/ +dist/ +.pytest_cache/ +.ruff_cache/ +.pyright/ diff --git a/.husky/task-runner.json b/.husky/task-runner.json index 009e6b3a..c974f397 100644 --- a/.husky/task-runner.json +++ b/.husky/task-runner.json @@ -27,6 +27,32 @@ "include": [ "**/*.cs" ] + }, + { + "name": "Ruff Format", + "command": "bash", + "args": [ + "-c", + "command -v uv >/dev/null 2>&1 || { echo 'uv not on PATH; skipping ruff format' >&2; exit 0; }; exec uv run --project PyPiLibrary ruff format \"$@\"", + "--", + "${staged}" + ], + "include": [ + "PyPiLibrary/**/*.py" + ] + }, + { + "name": "Ruff Check", + "command": "bash", + "args": [ + "-c", + "command -v uv >/dev/null 2>&1 || { echo 'uv not on PATH; skipping ruff check' >&2; exit 0; }; exec uv run --project PyPiLibrary ruff check \"$@\"", + "--", + "${staged}" + ], + "include": [ + "PyPiLibrary/**/*.py" + ] } ] } diff --git a/AGENTS.md b/AGENTS.md index 7028ac11..7262f2e0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,135 +1,157 @@ -# Instructions for AI Coding Agents - -**ProjectTemplate** is a C# .NET template project demonstrating best practices. Developers use this as a baseline to create their own projects. - -For comprehensive coding standards and detailed conventions, refer to [`.github/copilot-instructions.md`](./.github/copilot-instructions.md) and [`CODESTYLE.md`](./CODESTYLE.md). - -## Git and Commit Rules - -**These rules are absolute — no exceptions:** - -- **Never make git commits.** AI coding agents cannot produce cryptographically signed commits. All commits must be signed (SSH/GPG) and must be made by the developer. Stage changes with `git add` and leave the commit to the developer. -- **Never force push.** Do not run `git push --force` or `git push --force-with-lease` under any circumstances. Force pushing rewrites shared history and can cause data loss. -- **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 - -- **Zero Warnings Policy**: All builds must complete without errors or warnings - - Use `CSharpier Format`, `.Net Format`, and `Husky.Net Run` tasks - -- **Code Analysis**: Enable all .NET analyzers - - `true` - - `latest-all` - -### Project Configuration - -- Common MSBuild properties (`TargetFramework`, `Nullable`, `ImplicitUsings`, `AnalysisLevel`, etc.) - live in `Directory.Build.props` at the solution root. Do not duplicate these in individual `.csproj` - files — only add a property to a `.csproj` when it is project-specific or overrides the shared default. -- All NuGet package versions are centralised in `Directory.Packages.props`. `PackageReference` elements - in `.csproj` files must not include a `Version` attribute. Asset metadata (`PrivateAssets`, - `IncludeAssets`) stays in the `.csproj` `PackageReference` element. - -### Development Environment - -- Target latest .NET SDK (currently .NET 10 with C# 14) -- Support Visual Studio Code (`.code-workspace`) and Visual Studio Community (`.slnx`) -- Support Linux, Windows, and macOS with correct line endings and permissions -- Use `.editorconfig` for style enforcement - -### Project Structure - -- **NuGetLibrary**: Core reusable .NET NuGet library (published as `ptr727.ProjectTemplate.Library`) -- **Console**: CLI application using System.CommandLine -- **Tests**: xUnit with AwesomeAssertions (Arrange-Act-Assert pattern) -- **Benchmarks**: BenchmarkDotNet performance measurements -- **Docker**: Multi-platform Linux containers - -### Testing - -- Use xUnit v3 and AwesomeAssertions -- Organize tests logically in separate files -- Follow Arrange-Act-Assert pattern -- Test naming: `MethodName_Scenario_ExpectedBehavior()` - -## Authoritative References - -For detailed specifications, see: - -- [`.github/copilot-instructions.md`](./.github/copilot-instructions.md) - Complete coding conventions and style guide -- [`CODESTYLE.md`](./CODESTYLE.md) - Code style and formatting rules -- [`.editorconfig`](./.editorconfig) - Automated style enforcement -- Project task definitions - `CSharpier Format`, `.Net Build`, `.Net Format`, `.Net Outdated Upgrade`, `Husky.Net Run` - -## Quick Start for Derived Projects - -1. **Clone this template** as baseline for your project -2. **Review** [`.github/copilot-instructions.md`](./.github/copilot-instructions.md) thoroughly -3. **Update** project-specific values: - - `PackageId`, `RootNamespace` in `.csproj` files - - Namespace conventions with your organization name - - `README.md`, `HISTORY.md`, `version.json`, `LICENSE` -4. **Run tools** before first commit: - - `dotnet tool restore` - - `.Net Format` task - - `CSharpier Format` task -5. **Enable Husky.Net** hooks: `dotnet husky install` +# Instructions for AI Coding Agents + +**ProjectTemplate** is a polyglot template repo. The .NET side ships under [`NuGetLibrary/`](./NuGetLibrary/) (plus `Console/`, `Tests/`, `Benchmarks/`, `CodeGen/`); the Python side ships under [`PyPiLibrary/`](./PyPiLibrary/). This file is the single source of truth for cross-cutting rules. Language-specific style guides live next to the code: + +- .NET — [`CODESTYLE.md`](./CODESTYLE.md) +- Python — [`PyPiLibrary/CODESTYLE.md`](./PyPiLibrary/CODESTYLE.md) + +Treat this file as authoritative for everything else; don't restate its rules elsewhere. + +## Git and Commit Rules + +**These rules are absolute — no exceptions:** + +- **Never make git commits.** AI coding agents cannot produce cryptographically signed commits. All commits must be signed (SSH/GPG) and must be made by the developer. Stage changes with `git add` and leave the commit to the developer. +- **Never force push.** Do not run `git push --force` or `git push --force-with-lease` under any circumstances. Force pushing rewrites shared history and can cause data loss. +- **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. + +## Branching Model + +- `develop` is the integration branch. Feature branches → `develop` is **squash-only**; develop 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, which is what makes the "release on every push" model 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. + +## 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. + +## PR Review Etiquette + +The repo runs a review loop on every PR: local agent iteration plus remote automated review (GitHub Copilot is the configured reviewer). Treat this as a contract regardless of which local agent authored the changes. + +### Expected Review Loop + +1. Push changes to the PR branch. +2. Confirm a review was requested for the **current head SHA** (auto-trigger is unreliable; request explicitly). +3. Wait for review activity on that head. +4. Triage findings. +5. Apply fixes or write a rationale for declines. +6. Reply to each thread and resolve what was addressed. +7. Re-run the loop after every fix push until no actionable findings remain. + +`mergeStateStatus: CLEAN` only checks required statuses; it does not block on bot review comments. Merge only after review on the latest head SHA is confirmed and actionable findings are closed. + +For provider-specific mechanics (how to request review, query review state, post replies, resolve threads), see the **GitHub Copilot Review Runbook** in [.github/copilot-instructions.md](./.github/copilot-instructions.md). This file owns the contract; that file owns the mechanics. + +### Triaging Review Comments + +For each comment, classify before responding: + +- **Bug** — wrong behavior, missing test coverage, or a real divergence between code and docs. Fix it. Reply with the fixing commit SHA when done. +- **Style/convention** — the comment cites a rule from this file or a language-specific style guide. Two cases: + - The cited rule matches what the existing codebase already does → fix the offending code. + - The cited rule contradicts what's in the tree, or industry norm → **update the rule instead of the code**. The rule is wrong, not the code. Bouncing the same code across rounds is the symptom of a wrong rule. Heuristic: three rounds on the same style category means the rule needs adjusting and the user should authorize the rule change. +- **Architectural opinion** — the comment proposes a different design ("constrain this to disabled-by-default", "move it elsewhere", "add a runtime guardrail"). This is judgement, not a bug. Surface it to the user with a recommendation; don't apply unilaterally. + +### Responding and Resolution Expectations + +Reply inline with either the fixing commit SHA (for accepted issues) or a concise rationale (for declines). Resolve review threads when addressed or intentionally declined with rationale. Issue-level comments (those at `repos/.../issues//comments` rather than tied to a specific line) have no resolution action — acknowledge with a reply if needed and move on. + +After the final push on a PR, sweep older threads from earlier rounds whose code paths no longer exist; otherwise stale unresolved markers remain in the review UI. + +### Escalating to the User + +Bring the user in when: + +- **Genuine design trade-off** surfaces (fail-open vs fail-closed, narrow vs broad refactor scope, "should we add a guardrail or trust the docstring"). Triage, recommend, ask. +- **Repeated friction** across rounds without convergence — that's the rule-needs-updating signal. Stop, summarize the pattern, and let the user authorize the rule change. +- **Architectural redesign** is requested rather than a bug fix. Surface with a recommendation; never apply unilaterally. + +Anti-pattern: don't keep flipping the code on the same style point. Flip the rule once and stick to the rule. + +## 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. +- **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. +- **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' }}`. +- **Reusable workflows**: job-level `permissions:` are validated *before* the `if:` evaluates, so even a skipped job needs valid permissions declared. A `release` job with `permissions: contents: write` and `if: ${{ inputs.publish }}` will still cause `startup_failure` on a caller that doesn't grant `contents: write`. Either declare permissions at the call site, or omit the inner block and inherit. +- **Allowlist `success` and `skipped` explicitly** when chaining jobs across optional dependencies — `!= 'failure'` lets `cancelled` through (timeout, runner failure, manual cancel). Use `(needs.X.result == 'success' || needs.X.result == 'skipped')`. +- **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. + +## Devcontainer + +[.devcontainer/devcontainer.json](./.devcontainer/devcontainer.json) bind-mounts the host SSH signing key's *public half* (`~/.ssh/id_ed25519.pub`), `~/.config/git/allowed_signers`, and `~/.config/gh` so commits inside the container are SSH-signed (signing happens via the forwarded `ssh-agent` socket — the private key never enters the container) and, *when the host's `gh` token is file-backed*, `gh` is pre-authenticated. On Keychain (macOS) or libsecret (Linux) hosts, `~/.config/gh/hosts.yml` carries no `oauth_token`, so container `gh` is unauthenticated until the contributor opts into `gh auth login` inside the container. See [docs/devcontainer.md](./docs/devcontainer.md) for full setup, [docs/host-setup.md](./docs/host-setup.md) for prerequisites, and [docs/ssh-signing.md](./docs/ssh-signing.md) for the SSH commit signing details. + +The unified container hosts both `.NET 10` (base image) and Python via uv (installed in `.devcontainer/post-create.sh` from a version-pinned URL). The extension list in `.devcontainer/devcontainer.json` and `recommendations` in [`ProjectTemplate.code-workspace`](./ProjectTemplate.code-workspace) are kept identical — when you add an extension to one, add it to the other. + +## Project Structure (Languages) + +- **.NET projects** (build with `dotnet build`, test with `dotnet test`): + - `NuGetLibrary/` — core reusable .NET NuGet library (published as `ptr727.ProjectTemplate.Library`) + - `Console/` — CLI app using System.CommandLine + - `Tests/` — xUnit + AwesomeAssertions + - `Benchmarks/` — BenchmarkDotNet + - `CodeGen/` — internal codegen tooling + - **Style guide: [`CODESTYLE.md`](./CODESTYLE.md)**. +- **Python project** (env/build/test with `uv` from inside `PyPiLibrary/`): + - `PyPiLibrary/` — PyPI library template, published as `ptr727-projecttemplate-library` + - **Style guide: [`PyPiLibrary/CODESTYLE.md`](./PyPiLibrary/CODESTYLE.md)**. +- **Cross-cutting**: + - `.github/` — workflows, Dependabot, Copilot instructions + - `.devcontainer/` — devcontainer config + post-create script + - `.vscode/` — debug configs and tasks (.NET-oriented) + - `Docker/` — multi-platform Linux container build for the Console app + +When you touch code in either language, also respect that language's style guide. Conventions in this file (PR titles, branching, US English, devcontainer behavior, workflow YAML) apply uniformly to both languages. + +## Quick Start for Derived Projects + +1. **Clone this template** as the baseline for your project. +2. **Decide** which language sides you need. If you need only one, delete the other folder and its references — see the relevant CODESTYLE for the deletion checklist. +3. **Read** [CODESTYLE.md](./CODESTYLE.md) (.NET) and/or [PyPiLibrary/CODESTYLE.md](./PyPiLibrary/CODESTYLE.md) (Python) for the per-language style. +4. **Update project-specific values** — `PackageId`/`RootNamespace` in `.csproj`, `name` in `pyproject.toml`, namespace conventions, `README.md`, `HISTORY.md`, `version.json`, `LICENSE`, NuGet/PyPI badge URLs. +5. **Run tools before first commit**: + - .NET: `dotnet tool restore` and `dotnet husky install`. + - Python: `cd PyPiLibrary && uv sync`. +6. **Wire up release credentials** when ready to publish — see the README's release notes section and [PyPiLibrary/README.md](./PyPiLibrary/README.md) for PyPI Trusted Publisher setup. diff --git a/CODESTYLE.md b/CODESTYLE.md index 8037f473..75371b10 100644 --- a/CODESTYLE.md +++ b/CODESTYLE.md @@ -1,4 +1,8 @@ -# Code Style and Formatting Rules +# Code Style and Formatting Rules — .NET + +This file is the style guide for the **.NET projects** in this repo: [`NuGetLibrary/`](./NuGetLibrary/), [`Console/`](./Console/), [`Tests/`](./Tests/), [`Benchmarks/`](./Benchmarks/), and [`CodeGen/`](./CodeGen/). It does NOT apply to the Python project (`PyPiLibrary/`) — see [`PyPiLibrary/CODESTYLE.md`](./PyPiLibrary/CODESTYLE.md) for that. + +Cross-cutting rules (PR titles, branching, US English, markdown style, workflow YAML, PR review etiquette) live in [AGENTS.md](./AGENTS.md) and apply to both languages. This file only documents what's specific to C# / .NET. ## Build Requirements diff --git a/ProjectTemplate.code-workspace b/ProjectTemplate.code-workspace index af8b355a..cfc1aa56 100644 --- a/ProjectTemplate.code-workspace +++ b/ProjectTemplate.code-workspace @@ -86,6 +86,14 @@ "editor.formatOnSave": true, "editor.defaultFormatter": "csharpier.csharpier-vscode" }, + "[python]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + } + }, + "python.terminal.activateEnvironment": false, "git.alwaysSignOff": true, "markdown.extension.toc.levels": "2..3" }, diff --git a/ProjectTemplate.slnx b/ProjectTemplate.slnx index 7e3cc9c6..e751e769 100644 --- a/ProjectTemplate.slnx +++ b/ProjectTemplate.slnx @@ -4,6 +4,7 @@ + diff --git a/PyPiLibrary/CODESTYLE.md b/PyPiLibrary/CODESTYLE.md new file mode 100644 index 00000000..c8ae2bd2 --- /dev/null +++ b/PyPiLibrary/CODESTYLE.md @@ -0,0 +1,125 @@ +# Code Style and Formatting Rules — Python + +This file is the style guide for the **Python project** in this repo: [`PyPiLibrary/`](./). It does NOT apply to the .NET projects — see [`CODESTYLE.md`](../CODESTYLE.md) at the repo root for those. + +Cross-cutting rules (PR titles, branching, US English, markdown style, workflow YAML, PR review etiquette) live in [`AGENTS.md`](../AGENTS.md) and apply to both languages. This file only documents what's specific to Python. + +## Toolchain + +| Tool | Role | Config | +|---|---|---| +| [uv](https://docs.astral.sh/uv/) | env, deps, build, publish | `pyproject.toml` `[dependency-groups]`, `uv.lock` | +| [hatchling](https://hatch.pypa.io/latest/) | build backend | `pyproject.toml` `[build-system]` | +| [ruff](https://docs.astral.sh/ruff/) | lint + format + import sort | `pyproject.toml` `[tool.ruff]` | +| [pyright](https://microsoft.github.io/pyright/) | type checker | `pyproject.toml` `[tool.pyright]` | +| [pytest](https://docs.pytest.org/) | test runner | `pyproject.toml` `[tool.pytest.ini_options]` | + +`pyright` is consumed in two places: as a dev dependency (`uv run pyright` for CI/scripted runs) and via VS Code's **Pylance** extension (which embeds pyright). The standalone `ms-pyright.pyright` extension is in `unwantedRecommendations` because Pylance covers it. `mypy` is **not used** here — don't introduce it. + +## Local Development Loop + +From inside `PyPiLibrary/`: + +```sh +uv sync # creates .venv, installs deps + dev group +uv run ruff format # auto-format +uv run ruff check --fix # auto-fix lint +uv run ruff check # verify lint clean +uv run ruff format --check # verify format clean +uv run pyright # verify types +uv run pytest # run tests +uv build # produce wheel + sdist in ./dist +``` + +CI runs the same commands via [`.github/workflows/build-pypilibrary-task.yml`](../.github/workflows/build-pypilibrary-task.yml). Husky.Net pre-commit hooks (configured in [`.husky/task-runner.json`](../.husky/task-runner.json)) run `ruff format` and `ruff check` against staged Python files when `uv` is on PATH. + +## Layout + +`src` layout — keeps the package out of the repo root and prevents accidental imports of unbuilt code: + +```text +PyPiLibrary/ + pyproject.toml + README.md + CODESTYLE.md # this file + uv.lock # committed for reproducible CI + src/ + ptr727_projecttemplate_library/ + __init__.py + _version.py + .py + tests/ + __init__.py + test_.py +``` + +## Code Style + +### Formatting and Linting + +- **`ruff format` is authoritative.** Don't argue with the formatter; if it reformats your code, that's the final form. Configure (line length, target version) in `pyproject.toml` `[tool.ruff]`, not via inline `# fmt:` directives. +- **Run `ruff check --fix` before committing.** Most ruff lint rules have safe autofixes; let the tool handle them. The configured rule families are listed under `[tool.ruff.lint]` `select`. Add new rule families project-wide rather than scattering inline `# noqa` markers. +- **`# noqa` is a last resort.** When you must use one, scope it narrowly (`# noqa: E501`, not bare `# noqa`) and add a short comment on the same line explaining why. False-positive patterns that recur across the codebase belong in `[tool.ruff.lint]` `ignore` or per-file `[tool.ruff.lint.per-file-ignores]`, with a comment. + +### Comments + +- **Inline `#` comments**: keep tight and local. One line is preferred, but multi-line is fine when you need to document a non-obvious implementation constraint, a local trade-off, or coupling that future edits could easily break. Keep that rationale next to the affected block so the reviewer/maintainer sees it at edit-time. +- **Don't explain *what* the code does** — well-named identifiers handle that. Don't reference the current task ("added for X", "used by Y"); that belongs in the PR description. + +### Docstrings + +- Follow [PEP 257](https://peps.python.org/pep-0257/). Focus docstrings primarily on the **behavior contract** (what callers and tests can rely on), public semantics, and edge-case expectations. Implementation-local rationale belongs in inline `#` comments, not docstrings. +- A short one-liner is fine for trivial functions and tests with self-documenting names. +- For non-trivial behavior — non-obvious test scenarios, contracts a test pins, edge cases callers must know about, design trade-offs that are load-bearing for future maintainers — write a one-line summary, blank line, then a details paragraph. Multi-paragraph docstrings are fine when the contract earns it. +- Design notes belong **in the code** (docstrings or inline comments). They do NOT belong in [`HISTORY.md`](../HISTORY.md) — that file is end-user release notes, not a design log. + +### Type Hints + +- **All public APIs are typed.** Pyright runs on `src/` in strict mode (`[tool.pyright]` `strict = ["src"]`); tests run in standard mode. +- **Use modern syntax**: `list[int]` not `List[int]`, `dict[str, X]` not `Dict[str, X]`, `X | None` not `Optional[X]`, `from __future__ import annotations` only when needed for forward references. +- **Don't add `# type: ignore` to silence pyright errors without a comment** explaining the constraint. If a recurring false positive needs suppression, configure it project-wide in `[tool.pyright]`. + +### Naming + +- `snake_case` for functions, methods, variables, modules, package directories. +- `PascalCase` for classes, type aliases, type vars, enum members. +- `UPPER_SNAKE_CASE` for module-level constants. +- Single leading underscore for module-private; double leading underscore for name-mangled (rare — usually means rethink the design). + +### Imports + +- **Let ruff sort imports.** `[tool.ruff.lint]` `select` includes the `I` rule family (isort-equivalent). Don't hand-sort. +- Standard library first, then third-party, then first-party (the project itself), each block separated by a blank line — ruff enforces this automatically. +- Avoid wildcard imports (`from x import *`) outside `__init__.py` re-exports. + +### Patterns to Avoid + +- **Don't add backward-compat shims, `# removed` markers, or rename-to-`_` for unused vars** — just delete. Git history is the audit trail. +- **Don't add error handling for impossible cases.** Trust internal code; only validate at boundaries (user input, parsed config, external APIs). +- **Don't use exceptions for expected control flow.** Exceptions are for *unexpected* states. +- **Don't suppress errors silently** (`except Exception: pass`). Either handle the specific exception and document why it's safe, or let it propagate. + +## Tests + +- `pytest` with the configuration in `[tool.pytest.ini_options]`. Default invocation: `uv run pytest`. +- One test file per module under test, named `test_.py`. +- Test functions named `test__` — descriptive, not numbered. +- Use fixtures (defined in `conftest.py` for shared ones, or per-test for narrowly-scoped) instead of setup/teardown methods. +- **Avoid mocking when fakes work.** Hand-rolled fakes that implement the protocol you depend on are usually clearer and break less than `unittest.mock` magic. +- **Test edge cases that the docstring promises**, not implementation details. If the test breaks when you refactor *without changing behavior*, the test is asserting on an implementation detail. + +## Versioning + +`_version.py` ships with `__version__ = "0.0.0"` as a placeholder. The publish workflow uses `skip-existing: true` so the workflow won't fail, but no new PyPI versions will land until you wire `_version.py` to something that increments. See the **Template Adoption** section of [`README.md`](./README.md) for the three usual options (`hatch-vcs`, version.json bridge, manual bumps). + +## Linter Cleanliness + +Before pushing or opening a PR: + +- VS Code's **Problems** pane should be quiet for the files you touched. The relevant linters are ruff (via the `charliermarsh.ruff` extension) and pyright (via the `ms-python.python` extension's bundled Pylance). +- The CI gate is `uv run ruff check && uv run ruff format --check && uv run pyright && uv run pytest` — same as the local commands above, run from `PyPiLibrary/`. +- For markdown files in this directory, follow the markdown style rules in [AGENTS.md](../AGENTS.md). The repo's markdownlint config applies; fix violations at the source rather than disabling rules. + +## Adopting This Template Without Python + +If your derived project does not need a Python side, delete the entire `PyPiLibrary/` folder, the `build-pypilibrary` job in `build-release-task.yml`, the `publish-pypi` job in `publish-release.yml`, the `build-pypilibrary-task.yml` workflow, the `uv` block in `.github/dependabot.yml`, the Python entries in `.husky/task-runner.json`, and the Python settings/extension recommendations in `ProjectTemplate.code-workspace` and `.devcontainer/devcontainer.json`. The .NET side stands alone. diff --git a/PyPiLibrary/README.md b/PyPiLibrary/README.md new file mode 100644 index 00000000..f099a2b7 --- /dev/null +++ b/PyPiLibrary/README.md @@ -0,0 +1,68 @@ +# PyPiLibrary + +Python PyPI template — companion to the .NET `NuGetLibrary` in this repo. Published to PyPI as [`ptr727-projecttemplate-library`](https://pypi.org/project/ptr727-projecttemplate-library/). + +## Stack + +- **Build backend** — [`hatchling`](https://hatch.pypa.io/latest/) via `pyproject.toml` +- **Env / deps / publish** — [`uv`](https://docs.astral.sh/uv/) (Astral) +- **Lint + format** — [`ruff`](https://docs.astral.sh/ruff/) +- **Type checker** — [`pyright`](https://microsoft.github.io/pyright/) +- **Tests** — [`pytest`](https://docs.pytest.org/) +- **Publish** — [PyPI Trusted Publishing](https://docs.pypi.org/trusted-publishers/) via `pypa/gh-action-pypi-publish` (no API token in repo secrets) + +## Layout + +```text +PyPiLibrary/ + pyproject.toml + README.md + src/ + ptr727_projecttemplate_library/ + __init__.py + _version.py + example.py + tests/ + __init__.py + test_example.py +``` + +## Local Development + +The repo's [devcontainer](../docs/devcontainer.md) installs `uv` automatically and runs `uv sync` for this project on first open. To work outside the devcontainer: + +```shell +# from the repo root +cd PyPiLibrary +uv sync # creates .venv, installs deps + dev group +uv run ruff check # lint +uv run ruff format --check # formatting check +uv run pyright # type check +uv run pytest # tests +uv build # wheel + sdist into ./dist +``` + +## Publishing + +Releases are produced by `.github/workflows/build-pypilibrary-task.yml` (called from `build-release-task.yml` to build, lint, type-check, test, and upload the wheel + sdist as a workflow-run artifact). Publishing is a separate top-level `publish-pypi` job in `publish-release.yml` that downloads the artifact by name and runs [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) — no `PYPI_API_TOKEN` secret is involved. The publish job has `id-token: write` only at that single job level, so the test-pull-request flow (which calls the same build task during PR validation) doesn't need to propagate that permission through the reusable workflow chain. + +First-time setup (one-time, on PyPI): + +1. PyPI → **Account settings** → **Publishing** → **Add a new pending publisher**. +2. Project name: `ptr727-projecttemplate-library`. Owner: `ptr727`. Repo: `ProjectTemplate`. Workflow: `publish-release.yml`. Environment: `pypi`. +3. GitHub repo → **Settings** → **Environments** → create `pypi` environment (optionally with required reviewers). +4. The first successful release converts the pending publisher to a real publisher. + +## Template Adoption + +When deriving a new project from this template: + +- Replace the package name `ptr727-projecttemplate-library` (in `pyproject.toml`, this README, and CI) with your name. +- Rename `src/ptr727_projecttemplate_library/` to your import name. +- Re-register the trusted publisher on PyPI under the new project name. +- **Wire up a versioning scheme before the first publish.** `_version.py` ships with `__version__ = "0.0.0"` as a placeholder. The publish workflow uses `skip-existing: true` so the workflow won't fail on duplicate uploads — but **no new versions will land on PyPI** until you replace `0.0.0` with something that increments. Common options: + - [`hatch-vcs`](https://github.com/ofek/hatch-vcs) — derive the version from git tags. Add it to `[build-system].requires` and switch `[tool.hatch.version]` to `source = "vcs"`. Pairs well with tag-driven releases. + - **Read from `version.json`** — the .NET side uses Nerdbank.GitVersioning which reads from `version.json`. A small custom Hatchling plugin or a CI step can pull the version into `_version.py` so .NET and Python ship with matching versions. + - **Manual bumps** — edit `_version.py` in each release PR. Simplest, but easy to forget. + +If you don't want a Python project at all, delete the `PyPiLibrary/` folder, the `build-pypilibrary-task.yml` workflow, the `build-pypilibrary` job in `build-release-task.yml`, the `publish-pypi` job in `publish-release.yml`, and the `uv` block in `.github/dependabot.yml`. diff --git a/PyPiLibrary/pyproject.toml b/PyPiLibrary/pyproject.toml new file mode 100644 index 00000000..f73f737c --- /dev/null +++ b/PyPiLibrary/pyproject.toml @@ -0,0 +1,82 @@ +[build-system] +requires = ["hatchling>=1.27"] +build-backend = "hatchling.build" + +[project] +name = "ptr727-projecttemplate-library" +description = "Python PyPI template library — companion to the .NET NuGetLibrary in this template repo." +readme = "README.md" +license = { text = "MIT" } +authors = [{ name = "Pieter Viljoen" }] +requires-python = ">=3.14" +keywords = ["template", "pypi", "library"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.14", + "Topic :: Software Development :: Libraries :: Python Modules", +] +dynamic = ["version"] +dependencies = [] + +[project.urls] +Homepage = "https://github.com/ptr727/ProjectTemplate" +Source = "https://github.com/ptr727/ProjectTemplate" +Issues = "https://github.com/ptr727/ProjectTemplate/issues" + +[dependency-groups] +dev = [ + "pytest>=8.3", + "ruff>=0.9", + "pyright>=1.1.390", +] + +[tool.hatch.version] +path = "src/ptr727_projecttemplate_library/_version.py" + +[tool.hatch.build.targets.wheel] +packages = ["src/ptr727_projecttemplate_library"] + +[tool.hatch.build.targets.sdist] +include = ["src", "tests", "README.md", "pyproject.toml"] + +[tool.ruff] +line-length = 120 +target-version = "py314" + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear + "UP", # pyupgrade + "N", # pep8-naming + "SIM", # flake8-simplify + "RUF", # ruff-specific +] + +[tool.ruff.format] +docstring-code-format = true + +[tool.pyright] +include = ["src", "tests"] +pythonVersion = "3.14" +typeCheckingMode = "standard" +# Per-path strictness: `strict` accepts directory paths and applies +# strict-mode type checking to everything under them — equivalent to +# placing `# pyright: strict` at the top of every file in those dirs. +# Public library surface (`src/`) needs tight types; tests inherit the +# standard mode set above (fixtures, mocks, and parametrize args are +# commonly looser). +strict = ["src"] + +[tool.pytest.ini_options] +minversion = "8.0" +testpaths = ["tests"] +addopts = ["-ra", "--strict-markers", "--strict-config"] diff --git a/PyPiLibrary/src/ptr727_projecttemplate_library/__init__.py b/PyPiLibrary/src/ptr727_projecttemplate_library/__init__.py new file mode 100644 index 00000000..8c603871 --- /dev/null +++ b/PyPiLibrary/src/ptr727_projecttemplate_library/__init__.py @@ -0,0 +1,6 @@ +"""Python PyPI template library.""" + +from ptr727_projecttemplate_library._version import __version__ +from ptr727_projecttemplate_library.example import greet + +__all__ = ["__version__", "greet"] diff --git a/PyPiLibrary/src/ptr727_projecttemplate_library/_version.py b/PyPiLibrary/src/ptr727_projecttemplate_library/_version.py new file mode 100644 index 00000000..66b584a9 --- /dev/null +++ b/PyPiLibrary/src/ptr727_projecttemplate_library/_version.py @@ -0,0 +1,8 @@ +"""Single-source-of-truth for the package version. + +Hatchling reads ``__version__`` from this module via ``[tool.hatch.version]``. +For tag-driven versioning, swap this for ``hatch-vcs`` and configure the build +backend to derive the version from git tags. +""" + +__version__ = "0.0.0" diff --git a/PyPiLibrary/src/ptr727_projecttemplate_library/example.py b/PyPiLibrary/src/ptr727_projecttemplate_library/example.py new file mode 100644 index 00000000..84e2fcfb --- /dev/null +++ b/PyPiLibrary/src/ptr727_projecttemplate_library/example.py @@ -0,0 +1,6 @@ +"""Trivial example module — replace with your library code.""" + + +def greet(name: str) -> str: + """Return a friendly greeting for ``name``.""" + return f"Hello, {name}!" diff --git a/PyPiLibrary/src/ptr727_projecttemplate_library/py.typed b/PyPiLibrary/src/ptr727_projecttemplate_library/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/PyPiLibrary/tests/__init__.py b/PyPiLibrary/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/PyPiLibrary/tests/test_example.py b/PyPiLibrary/tests/test_example.py new file mode 100644 index 00000000..fe97742d --- /dev/null +++ b/PyPiLibrary/tests/test_example.py @@ -0,0 +1,16 @@ +"""Tests for ``ptr727_projecttemplate_library.example``.""" + +from ptr727_projecttemplate_library import __version__, greet + + +def test_version_is_string() -> None: + assert isinstance(__version__, str) + assert len(__version__) > 0 + + +def test_greet_uses_name() -> None: + assert greet("world") == "Hello, world!" + + +def test_greet_with_empty_name() -> None: + assert greet("") == "Hello, !" diff --git a/PyPiLibrary/uv.lock b/PyPiLibrary/uv.lock new file mode 100644 index 00000000..cb734f96 --- /dev/null +++ b/PyPiLibrary/uv.lock @@ -0,0 +1,140 @@ +version = 1 +revision = 3 +requires-python = ">=3.14" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "ptr727-projecttemplate-library" +source = { editable = "." } + +[package.dev-dependencies] +dev = [ + { name = "pyright" }, + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "pyright", specifier = ">=1.1.390" }, + { name = "pytest", specifier = ">=8.3" }, + { name = "ruff", specifier = ">=0.9" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyright" +version = "1.1.409" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/51/4e/3aa27f74211522dba7e9cbc3e74de779c6d4b654c54e50a4840623be8014/pyright-1.1.409.tar.gz", hash = "sha256:986ee05beca9e077c165758ad123667c679e050059a2546aa02473930394bc93", size = 4430434, upload-time = "2026-04-23T11:02:03.799Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/6b/330d8ebae582b30c2959a1ef4c3bc344ebde48c2ff0c3f113c4710735e11/pyright-1.1.409-py3-none-any.whl", hash = "sha256:aa3ea228cab90c845c7a60d28db7a844c04315356392aa09fafcee98c8c22fb3", size = 6438161, upload-time = "2026-04-23T11:02:01.309Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/99/43/3291f1cc9106f4c63bdce7a8d0df5047fe8422a75b091c16b5e9355e0b11/ruff-0.15.12.tar.gz", hash = "sha256:ecea26adb26b4232c0c2ca19ccbc0083a68344180bba2a600605538ce51a40a6", size = 4643852, upload-time = "2026-04-24T18:17:14.305Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/6e/e78ffb61d4686f3d96ba3df2c801161843746dcbcbb17a1e927d4829312b/ruff-0.15.12-py3-none-linux_armv6l.whl", hash = "sha256:f86f176e188e94d6bdbc09f09bfd9dc729059ad93d0e7390b5a73efe19f8861c", size = 10640713, upload-time = "2026-04-24T18:17:22.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/08/a317bc231fb9e7b93e4ef3089501e51922ff88d6936ce5cf870c4fe55419/ruff-0.15.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e3bcd123364c3770b8e1b7baaf343cc99a35f197c5c6e8af79015c666c423a6c", size = 11069267, upload-time = "2026-04-24T18:17:30.105Z" }, + { url = "https://files.pythonhosted.org/packages/aa/a4/f828e9718d3dce1f5f11c39c4f65afd32783c8b2aebb2e3d259e492c47bd/ruff-0.15.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fe87510d000220aa1ed530d4448a7c696a0cae1213e5ec30e5874287b66557b5", size = 10397182, upload-time = "2026-04-24T18:17:07.177Z" }, + { url = "https://files.pythonhosted.org/packages/71/e0/3310fc6d1b5e1fdea22bf3b1b807c7e187b581021b0d7d4514cccdb5fb71/ruff-0.15.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:84a1630093121375a3e2a95b4a6dc7b59e2b4ee76216e32d81aae550a832d002", size = 10758012, upload-time = "2026-04-24T18:16:55.759Z" }, + { url = "https://files.pythonhosted.org/packages/11/c1/a606911aee04c324ddaa883ae418f3569792fd3c4a10c50e0dd0a2311e1e/ruff-0.15.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fb129f40f114f089ebe0ca56c0d251cf2061b17651d464bb6478dc01e69f11f5", size = 10447479, upload-time = "2026-04-24T18:16:51.677Z" }, + { url = "https://files.pythonhosted.org/packages/9d/68/4201e8444f0894f21ab4aeeaee68aa4f10b51613514a20d80bd628d57e88/ruff-0.15.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0c862b172d695db7598426b8af465e7e9ac00a3ea2a3630ee67eb82e366aaa6", size = 11234040, upload-time = "2026-04-24T18:17:16.529Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/8a6d6cf4ccc23fd67060874e832c18919d1557a0611ebef03fdb01fff11e/ruff-0.15.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2849ea9f3484c3aca43a82f484210370319e7170df4dfe4843395ddf6c57bc33", size = 12087377, upload-time = "2026-04-24T18:17:04.944Z" }, + { url = "https://files.pythonhosted.org/packages/85/f6/c669cf73f5152f623d34e69866a46d5e6185816b19fcd5b6dd8a2d299922/ruff-0.15.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e77c7e51c07fe396826d5969a5b846d9cd4c402535835fb6e21ce8b28fef847", size = 11367784, upload-time = "2026-04-24T18:17:25.409Z" }, + { url = "https://files.pythonhosted.org/packages/e8/39/c61d193b8a1daaa8977f7dea9e8d8ba866e02ea7b65d32f6861693aa4c12/ruff-0.15.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83b2f4f2f3b1026b5fb449b467d9264bf22067b600f7b6f41fc5958909f449d0", size = 11344088, upload-time = "2026-04-24T18:17:12.258Z" }, + { url = "https://files.pythonhosted.org/packages/c2/8d/49afab3645e31e12c590acb6d3b5b69d7aab5b81926dbaf7461f9441f37a/ruff-0.15.12-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9ba3b8f1afd7e2e43d8943e55f249e13f9682fde09711644a6e7290eb4f3e339", size = 11271770, upload-time = "2026-04-24T18:17:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/06/33f41fe94403e2b755481cdfb9b7ef3e4e0ed031c4581124658d935d52b4/ruff-0.15.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e852ba9fdc890655e1d78f2df1499efbe0e54126bd405362154a75e2bde159c5", size = 10719355, upload-time = "2026-04-24T18:17:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/0d/59/18aa4e014debbf559670e4048e39260a85c7fcee84acfd761ac01e7b8d35/ruff-0.15.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dd8aed930da53780d22fc70bdf84452c843cf64f8cb4eb38984319c24c5cd5fd", size = 10462758, upload-time = "2026-04-24T18:17:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/25/e7/cc9f16fd0f3b5fddcbd7ec3d6ae30c8f3fde1047f32a4093a98d633c6570/ruff-0.15.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:01da3988d225628b709493d7dc67c3b9b12c0210016b08690ef9bd27970b262b", size = 10953498, upload-time = "2026-04-24T18:17:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/72/7a/a9ba7f98c7a575978698f4230c5e8cc54bbc761af34f560818f933dafa0c/ruff-0.15.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9cae0f92bd5700d1213188b31cd3bdd2b315361296d10b96b8e2337d3d11f53e", size = 11447765, upload-time = "2026-04-24T18:17:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f9/0ae446942c846b8266059ad8a30702a35afae55f5cdc54c5adf8d7afdc27/ruff-0.15.12-py3-none-win32.whl", hash = "sha256:d0185894e038d7043ba8fd6aee7499ece6462dc0ea9f1e260c7451807c714c20", size = 10657277, upload-time = "2026-04-24T18:17:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/33/f1/9614e03e1cdcbf9437570b5400ced8a720b5db22b28d8e0f1bda429f660d/ruff-0.15.12-py3-none-win_amd64.whl", hash = "sha256:c87a162d61ab3adca47c03f7f717c68672edec7d1b5499e652331780fe74950d", size = 11837758, upload-time = "2026-04-24T18:17:00.113Z" }, + { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] diff --git a/README.md b/README.md index 4fb9c8ef..3b646feb 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ C# .NET project template. - **Versioned Releases**: [GitHub Releases][releases-link] - Version tagged source code and build artifacts. - **Docker Images**: [Docker Hub][docker-link] - Container images with all tools pre-installed. - **NuGet Packages** [NuGet Packages][nuget-link] - .NET libraries published to NuGet.org. +- **PyPI Packages** [PyPI Packages][pypi-link] - Python library published to PyPI.org. ### Build Status @@ -25,7 +26,8 @@ C# .NET project template. [![Docker Latest][dockerlatestversion-shield]][docker-link]\ [![Docker Develop][dockerdevelopversion-shield]][docker-link]\ [![NuGet Release][nugetreleaseversion-shield]][nuget-link]\ -[![NuGet Pre-Release][nugetprereleaseversion-shield]][nuget-link] +[![NuGet Pre-Release][nugetprereleaseversion-shield]][nuget-link]\ +[![PyPI Release][pypireleaseversion-shield]][pypi-link] ### Release Notes @@ -293,7 +295,8 @@ Licensed under the [MIT License][license-link]\ ### Template - TODO List -- [ ] Configure git for SSH signing and SSH forwarding in dev containers. +- [ ] Configure git for SSH signing and SSH forwarding in dev containers — see [docs/host-setup.md](./docs/host-setup.md), [docs/ssh-signing.md](./docs/ssh-signing.md), and [docs/devcontainer.md](./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 from `ProjectTemplate.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](#template---developer-environment-setup). - [ ] Open the project directory (*not the workspace*) in Visual Studio Code, and rename (Ctrl-Shift-H) all instances of `ProjectTemplate` to `[NewProject]` in code. @@ -480,43 +483,39 @@ Licensed under the [MIT License][license-link]\ - Bot generated pull requests (codegen, dependabot) always checkout from and merge into `main` directly. - If `develop` falls behind after a bot merge, re-run codegen or rebase `develop` on `main` before merging `develop` to `main`. - + -[github-link]: https://github.com/ptr727/ProjectTemplate [actions-link]: https://github.com/ptr727/ProjectTemplate/actions -[discussions-link]: https://github.com/ptr727/ProjectTemplate/discussions [commits-link]: https://github.com/ptr727/ProjectTemplate/commits/main -[issues-link]: https://github.com/ptr727/ProjectTemplate/issues -[releases-link]: https://github.com/ptr727/ProjectTemplate/releases - -[license-link]: ./LICENSE -[license-shield]: https://img.shields.io/github/license/ptr727/ProjectTemplate?label=License - +[discussions-link]: https://github.com/ptr727/ProjectTemplate/discussions [docker-link]: https://hub.docker.com/r/ptr727/projecttemplate -[dockerlatestversion-shield]: https://img.shields.io/docker/v/ptr727/projecttemplate/latest?label=Docker%20Latest&logo=docker -[dockerdevelopversion-shield]: https://img.shields.io/docker/v/ptr727/projecttemplate/develop?label=Docker%20Develop&logo=docker&color=orange [dockerbuildstatus-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/ProjectTemplate/publish-periodic-docker-release.yml?logo=github&label=Docker%20Build - +[dockerdevelopversion-shield]: https://img.shields.io/docker/v/ptr727/projecttemplate/develop?label=Docker%20Develop&logo=docker&color=orange +[dockerlatestversion-shield]: https://img.shields.io/docker/v/ptr727/projecttemplate/latest?label=Docker%20Latest&logo=docker +[github-link]: https://github.com/ptr727/ProjectTemplate +[issues-link]: https://github.com/ptr727/ProjectTemplate/issues [lastbuild-shield]: https://byob.yarr.is/ptr727/ProjectTemplate/lastbuild [lastcommit-shield]: https://img.shields.io/github/last-commit/ptr727/ProjectTemplate?logo=github&label=Last%20Commit - -[releaseversion-shield]: https://img.shields.io/github/v/release/ptr727/ProjectTemplate?logo=github&label=GitHub%20Release -[prereleaseversion-shield]: https://img.shields.io/github/v/release/ptr727/ProjectTemplate?include_prereleases&label=GitHub%20Pre-Release&logo=github -[releasebuildstatus-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/ProjectTemplate/publish-release.yml?logo=github&label=Releases%20Build - +[license-link]: ./LICENSE +[license-shield]: https://img.shields.io/github/license/ptr727/ProjectTemplate?label=License [nuget-link]: https://www.nuget.org/packages/ptr727.ProjectTemplate.Library/ -[nugetreleaseversion-shield]: https://img.shields.io/nuget/v/ptr727.ProjectTemplate.Library?logo=nuget&label=NuGet%20Release [nugetprereleaseversion-shield]: https://img.shields.io/nuget/vpre/ptr727.ProjectTemplate.Library?logo=nuget&&label=NuGet%20Pre-Release&color=orange +[nugetreleaseversion-shield]: https://img.shields.io/nuget/v/ptr727.ProjectTemplate.Library?logo=nuget&label=NuGet%20Release +[prereleaseversion-shield]: https://img.shields.io/github/v/release/ptr727/ProjectTemplate?include_prereleases&label=GitHub%20Pre-Release&logo=github +[pypi-link]: https://pypi.org/project/ptr727-projecttemplate-library/ +[pypireleaseversion-shield]: https://img.shields.io/pypi/v/ptr727-projecttemplate-library?logo=pypi&label=PyPI%20Release +[releasebuildstatus-shield]: https://img.shields.io/github/actions/workflow/status/ptr727/ProjectTemplate/publish-release.yml?logo=github&label=Releases%20Build +[releases-link]: https://github.com/ptr727/ProjectTemplate/releases +[releaseversion-shield]: https://img.shields.io/github/v/release/ptr727/ProjectTemplate?logo=github&label=GitHub%20Release - - -[devcontainers-link]: https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers + [apininjas-link]: https://api-ninjas.com/api/quotes [awesomeassertions-link]: https://awesomeassertions.org/ [byob-link]: https://github.com/marketplace/actions/bring-your-own-badge [createpr-link]: https://github.com/marketplace/actions/create-pull-request [csharpier-link]: https://csharpier.com/ +[devcontainers-link]: https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers [ghactions-link]: https://github.com/actions [ghautocommit-link]: https://github.com/marketplace/actions/git-auto-commit [ghdependabot-link]: https://github.com/dependabot diff --git a/docs/devcontainer.md b/docs/devcontainer.md index 4a7f0c07..e09e19c6 100644 --- a/docs/devcontainer.md +++ b/docs/devcontainer.md @@ -9,7 +9,7 @@ Prerequisite: complete [host setup](./host-setup.md) first — without git confi | Component | Source | Purpose | |---|---|---| | .NET 10 SDK | base image `mcr.microsoft.com/devcontainers/dotnet:1-10.0` | Build, test, pack the .NET projects | -| `uv` | `https://astral.sh/uv//install.sh` (version-pinned) downloaded by `.devcontainer/post-create.sh` | Python env, dependency, build, and publish manager for the PyPi sibling | +| `uv` | `https://astral.sh/uv//install.sh` (version-pinned) downloaded by `.devcontainer/post-create.sh` | Python env, dependency, build, and publish manager for the PyPI sibling | | `gh` CLI | `ghcr.io/devcontainers/features/github-cli:1` | Issue/PR/release management from inside the container | | Common utilities | `ghcr.io/devcontainers/features/common-utils:2` | bash, curl, wget, sudo, `vscode` user | | VS Code extensions | `customizations.vscode.extensions` in `devcontainer.json` | Mirrors `ProjectTemplate.code-workspace` recommendations so the container has the same tooling | diff --git a/docs/host-setup.md b/docs/host-setup.md index c48f228d..0cf46fb7 100644 --- a/docs/host-setup.md +++ b/docs/host-setup.md @@ -8,6 +8,8 @@ Supported hosts: - **macOS** — both the devcontainer flow and the host-install flow. - **Windows** — the devcontainer flow requires **WSL2**; native Windows (PowerShell + winget) is supported only for the host-install flow described in `README.md`. The bind-mounts in `.devcontainer/devcontainer.json` rely on POSIX paths and only work from Linux/macOS/WSL2. +> **Shell assumptions in this doc**: every command snippet below assumes a **POSIX shell** (bash/zsh) and POSIX path conventions (`~/.ssh/...`, `mkdir -p`, `$(...)` command substitution). On Windows, run them from **WSL2** or **Git Bash** — they will not work as-is in PowerShell or `cmd.exe`. The git config and `gh` commands are portable; only the file/path manipulation differs by shell. + ## Git Identity Configure your name and email — used for commit authorship. diff --git a/docs/ssh-signing.md b/docs/ssh-signing.md index be3b9c16..1a95e06a 100644 --- a/docs/ssh-signing.md +++ b/docs/ssh-signing.md @@ -92,12 +92,14 @@ If you must work on Windows directly without a devcontainer, OpenSSH for Windows ## Verify Signing +The `-S` flag and `-c gpg.format=ssh` override are explicit so the verification works even before `commit.gpgsign` and `gpg.format` are set globally — useful when verifying a fresh setup mid-configuration. + ```shell -git commit --allow-empty -m "verify-signing" +git -c gpg.format=ssh commit -S --allow-empty -m "verify-signing" git log --show-signature -1 ``` -Expected output includes `Good "git" signature for `. If you see `error: gpg.ssh.allowedSignersFile needs to be configured` or `No signature`, walk back through the host setup — most often `allowed_signers` is missing the entry, or `commit.gpgsign` is not set. +Expected output includes `Good "git" signature for `. If you see `error: gpg.ssh.allowedSignersFile needs to be configured` or `No signature`, walk back through the host setup — most often `allowed_signers` is missing the entry, or the `user.signingkey` and `gpg.ssh.allowedSignersFile` configs aren't set yet. ## Inside the Devcontainer