Add PyPiLibrary Python sibling project - #64
Merged
Merged
Conversation
Disambiguate the .NET project name in preparation for adding a sibling Python PyPi project. The folder, csproj filename, RootNamespace, and namespace declarations move from `Library` to `NuGetLibrary`. The companion GitHub Actions reusable workflow `build-library-task.yml` is renamed to `build-nugetlibrary-task.yml` for the same reason; the artifact name and zip filename track the rename. The published NuGet package id is intentionally preserved as `ptr727.ProjectTemplate.Library` via an explicit `<PackageId>` element so existing consumers and the README NuGet badges continue to work without a new package or a 404 on the existing nuget.org URL. Class names `TemplateLibrary` and `StaticTemplateLibrary` are left alone — they describe the type, not the project, and are referenced by tests and benchmarks. dotnet build: 0 warnings, 0 errors. dotnet test: 15 passed, 0 failed. dotnet pack: produces ptr727.ProjectTemplate.Library.1.0.0-pre.nupkg as expected.
A single unified devcontainer hosts both .NET 10 and the upcoming PyPi sibling. Host SSH key, allowed_signers, and gh config are bind-mounted so commits sign correctly inside the container without the private key ever leaving the host. Lifecycle scripts install uv, restore .NET local tools, and set up Husky.Net hooks. Devcontainer extension list mirrors the workspace `recommendations` so the two stay in sync; Python tooling extensions are added now so they'll be installed when PyPiLibrary lands in PR 5. New docs decompose the verbose template setup section into focused files: - docs/host-setup.md: git identity, SSH key generation, allowed_signers, gh auth, per-OS ssh-agent / Keychain handling, verify checklist. - docs/devcontainer.md: bind-mount table, lifecycle commands, gh credential-store nuance (Keychain vs libsecret vs file), verify checklist, troubleshooting matrix. - docs/ssh-signing.md: per-OS deltas (systemd ssh-agent, Apple Keychain, WSL2 caveats), allowed_signers format, devcontainer interaction, troubleshooting matrix. README links to the new docs from the existing Development Environment Setup section; verbose host-setup snippets stay in the docs. Native Windows hosts are explicitly out-of-scope for the devcontainer — WSL2 is the supported Windows path, matching what Docker Desktop's WSL2 backend cleanly supports.
Adds a Python PyPi template project that lives alongside the .NET NuGetLibrary, so this template repo serves as a polyglot starting point. Modern stack: hatchling build backend, uv for env/deps/publish, ruff for lint and format, pyright for typing, pytest for tests, PyPI Trusted Publishing via OIDC (no PYPI_API_TOKEN). Folder is `PyPiLibrary/` (qualifier on disk to disambiguate from NuGetLibrary), but the published package name has no `pypi` qualifier and mirrors the NuGet identity: `ptr727-projecttemplate-library`. Import name `ptr727_projecttemplate_library`. Workflow plumbing: - New reusable `.github/workflows/build-pypilibrary-task.yml` that runs ruff check, ruff format --check, pyright, pytest, then `uv build`. Publish job uses `pypa/gh-action-pypi-publish` with `id-token: write` — Trusted Publishing requires no API token in repo secrets. - `build-release-task.yml` adds a `pypi: bool` input mirroring `nuget`, calls the new reusable workflow, and gates publishing on it. - `publish-release.yml` passes `pypi: true` so on-push releases include the PyPi publish. - `test-release-task.yml` passes `pypi: false` so PR validation exercises the build but skips publishing. Other plumbing: - `.github/dependabot.yml` adds the `uv` ecosystem targeting `/PyPiLibrary`. - `.husky/task-runner.json` adds Ruff Format and Ruff Check tasks scoped to `PyPiLibrary/**/*.py`, guarded by `command -v uv` so `.cs`-only commits do not fail when uv is not installed. - `ProjectTemplate.code-workspace` adds Python interpreter, ruff, and format-on-save settings scoped via the workspace file (not split into `.vscode/settings.json`). - `ProjectTemplate.slnx` adds the new workflow file under GitHub Actions. - `.gitignore` adds Python build artifacts (.venv, dist, __pycache__, .pytest_cache, .ruff_cache, .pyright). - `README.md` adds PyPI badge to the build/distribution and releases sections; template TODO list reminds the deriver to delete the unused language side. Verification (local, host has uv 0.11.8): - `uv sync` installs deps cleanly. - `uv run ruff check`: All checks passed. - `uv run pyright`: 0 errors, 0 warnings. - `uv run pytest`: 3 passed. - `uv build`: produces ptr727_projecttemplate_library-0.0.0.tar.gz and -0.0.0-py3-none-any.whl. - `dotnet build`: 0 warnings, 0 errors. - `dotnet test`: 15 passed (no regression on .NET side).
- Remove the hard-coded `workspaceFolder` from `devcontainer.json`. The
default `/workspaces/${localWorkspaceFolderBasename}` tracks the host
folder name automatically, so derived projects with a different repo
name don''t need to edit this config.
- Drop `|| true` from `dotnet husky install` in `post-create.sh`. Husky
hook installation failing silently would let the container come up
without pre-commit enforcement, masking a real setup problem. Let it
fail loudly instead.
- After installing uv, prepend `$HOME/.local/bin` to PATH for the rest
of the script and call `uv sync` from PATH instead of by hard-coded
path. Handles the case where uv is already installed elsewhere on PATH.
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a Python PyPiLibrary/ sibling project alongside the .NET NuGetLibrary/, plus supporting CI/devcontainer/tooling updates to make the template polyglot and publishable to both NuGet and PyPI.
Changes:
- Introduces
PyPiLibrary/(pyproject + src + tests + uv.lock) and a reusable GitHub Actions workflow to build/publish it via PyPI Trusted Publishing. - Wires PyPI into release/test workflows, Dependabot, Husky task runner, workspace settings, and repo docs/README.
- Finalizes the
.NET Library → NuGetLibraryrename plumbing across references (solution/workflows/projects/usings/namespaces).
Reviewed changes
Copilot reviewed 33 out of 38 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/ssh-signing.md | New documentation for SSH commit signing setup and troubleshooting. |
| docs/host-setup.md | New host prerequisites doc (git identity, SSH keys, allowed signers, gh auth). |
| docs/devcontainer.md | New devcontainer usage doc (what’s inside, mounts, lifecycle, troubleshooting). |
| Tests/Tests.csproj | Updates ProjectReference to NuGetLibrary/NuGetLibrary.csproj. |
| Tests/LoggingTests.cs | Updates using/namespace reference to ptr727.ProjectTemplate.NuGetLibrary. |
| README.md | Adds PyPI badge/link and updates dev environment/template TODO guidance for devcontainer + PyPI. |
| PyPiLibrary/uv.lock | Adds committed uv lockfile for reproducible Python CI builds. |
| PyPiLibrary/tests/test_example.py | Adds pytest coverage for __version__ and greet(). |
| PyPiLibrary/tests/init.py | Adds Python tests package marker file. |
| PyPiLibrary/src/ptr727_projecttemplate_library/example.py | Adds example Python API (greet). |
| PyPiLibrary/src/ptr727_projecttemplate_library/_version.py | Adds single-source version module. |
| PyPiLibrary/src/ptr727_projecttemplate_library/init.py | Exposes public package API (__version__, greet). |
| PyPiLibrary/pyproject.toml | Adds hatchling/uv/ruff/pyright/pytest configuration and metadata. |
| PyPiLibrary/README.md | Adds Python project README (local dev + publishing instructions). |
| ProjectTemplate.slnx | Adds new workflow file entries and updates build deps to NuGetLibrary. |
| ProjectTemplate.code-workspace | Adds Python/Ruff/Pyright workspace settings and extension recommendations. |
| NuGetLibrary/Options.cs | Updates namespace to ptr727.ProjectTemplate.NuGetLibrary. |
| NuGetLibrary/NuGetLibrary.csproj | Updates RootNamespace to ptr727.ProjectTemplate.NuGetLibrary. |
| NuGetLibrary/LogOptions.cs | Updates namespace to ptr727.ProjectTemplate.NuGetLibrary. |
| NuGetLibrary/Library.cs | Updates namespace to ptr727.ProjectTemplate.NuGetLibrary. |
| NuGetLibrary/GlobalUsings.cs | Adds global usings for logging namespaces. |
| NuGetLibrary/Extensions.cs | Updates namespace to ptr727.ProjectTemplate.NuGetLibrary. |
| NuGetLibrary/.editorconfig | Adds project-level editorconfig suppression for missing XML comment warnings. |
| Console/Program.cs | Updates using to ptr727.ProjectTemplate.NuGetLibrary. |
| Console/Console.csproj | Updates ProjectReference to NuGetLibrary/NuGetLibrary.csproj. |
| Benchmarks/Benchmarks.csproj | Updates ProjectReference to NuGetLibrary/NuGetLibrary.csproj. |
| AGENTS.md | Updates project structure docs to reference NuGetLibrary. |
| .husky/task-runner.json | Adds Ruff format/check tasks for staged Python files. |
| .gitignore | Adds Python/uv artifacts to ignore list (.venv, dist, caches, etc.). |
| .github/workflows/test-release-task.yml | Passes pypi: false into release task for PR validation. |
| .github/workflows/publish-release.yml | Passes pypi: true into release task for publishing on release pushes. |
| .github/workflows/build-release-task.yml | Adds pypi input and calls into build-pypilibrary-task.yml. |
| .github/workflows/build-pypilibrary-task.yml | New reusable workflow to build/test/package and optionally publish to PyPI via OIDC. |
| .github/workflows/build-nugetlibrary-task.yml | Renames/updates reusable workflow identifiers and paths for NuGet library build. |
| .github/dependabot.yml | Adds Dependabot configuration for uv ecosystem in /PyPiLibrary. |
| .github/copilot-instructions.md | Updates documentation examples to use NuGetLibrary names/namespaces. |
| .devcontainer/post-create.sh | New devcontainer post-create script (install uv, restore tools, husky install, pre-sync Python). |
| .devcontainer/devcontainer.json | New unified devcontainer (dotnet + uv + gh) with SSH/allowed_signers mounts. |
`${localEnv:HOME}${localEnv:USERPROFILE}` produces an invalid concatenated
path on hosts where both variables are set (e.g. native Windows shells).
The supported devcontainer hosts are Linux, macOS, and WSL2 — all of
which have HOME set unconditionally — so HOME alone covers every
supported case.
- Husky Ruff Format and Ruff Check tasks no longer mask failures with
`|| true`. Replace with `if command -v uv ...; then ...; fi` so
ruff errors surface and block the commit when uv IS installed; the
guard cleanly skips when uv is not on PATH (.cs-only commit on a
contributor without uv installed should not fail).
- Replace the `cd PyPiLibrary && uv run ruff ...` pattern with
`uv run --project PyPiLibrary ruff ...`. The `cd` form broke
because Husky.Net passes `${staged}` paths repo-relative
(`PyPiLibrary/src/...`); resolving those from inside `PyPiLibrary/`
produced a non-existent path.
- Normalize dependabot.yml indentation: nest the three update entries
under `updates:` with two-space indentation instead of leaving them
at column 0. Functionally equivalent for Dependabot but matches the
documented YAML style and removes ambiguity for human readers.
Reverts a regression introduced in 449d494. The `${localEnv:HOME}${localEnv:USERPROFILE}` pattern is the canonical devcontainer.json fallback idiom: at most one of the two is set in practice in the contexts where devcontainer.json `localEnv` is evaluated (Windows VS Code: USERPROFILE only; macOS / Linux / WSL2: HOME only). The "concatenation produces an invalid path" concern is theoretical for shells that set both, but those shells aren''t the context VS Code Dev Containers resolves variables in. Without the USERPROFILE half, Windows VS Code launching the container sees `${localEnv:HOME}` as empty and the mount source becomes `/.ssh/id_ed25519.pub` — broken signing and gh auth.
Fix `startup_failure` on the test-pull-request workflow. The original structure put `id-token: write` at the publish job inside the deeply- nested reusable workflow `build-pypilibrary-task.yml`. Per the workflow YAML conventions (see AGENTS.md), job-level permissions are validated *before* the `if:` evaluates, so even the gated publish job''s permission declaration had to be granted by every caller in the chain. The test path (test-pull-request → test-release-task → build-release-task → build-pypilibrary-task) does not need to publish, so granting id-token write up that whole chain was both unnecessary and a permission-scope smell. New structure: - `build-pypilibrary-task.yml`: build only (lint, typecheck, test, build, upload artifact). No publish job, no id-token. Same artifact name as before (`pypilibrary-build`). - `build-release-task.yml`: drops the `pypi: bool` input and the permissions block on the build-pypilibrary call. Now identical in shape to the build-nugetlibrary call. - `publish-release.yml`: gains a top-level `publish-pypi` job that runs after `create-release`, downloads the `pypilibrary-build` artifact by name (artifacts uploaded by reusable workflows are accessible to sibling jobs in the same run), and publishes via Trusted Publishing. `id-token: write` lives at this single job level. - `test-release-task.yml`: drops the `pypi: false` input (no longer needed; PyPi build runs unconditionally as part of build-release). The PyPi build still runs during PR validation (via test-release → build-release → build-pypilibrary), so lint, typecheck, test, and build all gate every PR. Publishing only happens on push to main/develop, in a job that has the minimal id-token: write permission scope.
# Conflicts: # .github/workflows/build-release-task.yml
- Pin uv to a specific version in `post-create.sh` via the version-prefixed Astral install URL (https://astral.sh/uv/<version>/ install.sh). The `latest` install script remains a supply-chain attack surface; pinning means a compromised `latest` cannot silently change what runs on contributors'' machines or CI. Bump `UV_VERSION` on upgrade after reviewing release notes. - Clarify in `docs/host-setup.md` that the WSL2-only constraint applies to the devcontainer flow specifically. The host-install path (`README.md` → "Alternative (host install)") supports native Windows with winget; the devcontainer flow does not because the bind-mounts rely on POSIX paths. - Strengthen the non-systemd ssh-agent snippet: probe the agent for at least one loaded key via `ssh-add -l`. The previous `[ -z "$SSH_AUTH_SOCK" ]`-only check missed the stale-socket and agent-running-but-empty cases.
- Rewrite Ruff Format and Ruff Check husky tasks to pass `${staged}` as
positional args via `bash -c "..." -- ${staged}` and reference them
through `"$@"` in the script. Husky.Net expands `${staged}` into
separate array elements, so threading them through positional args
preserves space-containing paths and prevents shell metacharacter
re-interpretation. The earlier embedded-string form would have broken
on a path containing whitespace.
- Update PyPiLibrary/README.md "Publishing" section to reflect the
current workflow shape (build in build-pypilibrary-task.yml; publish
in a top-level `publish-pypi` job in publish-release.yml). Removes
the stale `pypi: true` reference from the prior reusable-workflow
design that was reverted in 4c939f6 to fix the startup_failure.
- Update the Template Adoption "delete the Python side" instructions
to refer to the `uv` block in dependabot.yml (not `pip`) and to the
actual job names that need removing in build-release-task.yml and
publish-release.yml.
The previous workspace settings hard-coded
``${workspaceFolder}/PyPiLibrary/.venv/bin/{ruff,python}``, which is
broken in three ways:
- The ``.venv`` directory does not exist until a contributor has run
``uv sync`` inside ``PyPiLibrary/``. Until then, VS Code's ruff
extension shows a "could not find ruff binary" popup on every
Python file open.
- The path is Linux/macOS-only (``.venv/bin/...``). On native Windows
hosts, the binary lives at ``.venv\Scripts\ruff.exe``; the literal
``/bin/`` path resolves to nothing and the extension errors out.
- It pins to the venv-installed ruff. The Astral ruff VS Code extension
ships with a bundled ruff that works without any setup, and the venv
version is what CI uses anyway — so matching versions in the IDE was
optional, not required.
After this change:
- The ruff extension uses its bundled binary (``ruff.importStrategy``
default ``"useBundled"``). Works on every host out of the box.
- ``ruff`` auto-discovers ``[tool.ruff]`` from ``PyPiLibrary/pyproject.toml``
by walking up from the file being linted, so dropping
``ruff.configuration`` doesn't lose the project ruleset.
- The Python extension auto-detects ``PyPiLibrary/.venv`` once it
exists; contributors pick the interpreter via Command Palette →
"Python: Select Interpreter" instead of relying on a path that may
not resolve.
Compare the installed uv --version output to UV_VERSION; if they differ (including the case where uv was already on PATH from a prior install or a system package), re-install the pinned version. Without this check the pin only applied when uv was missing entirely, undermining the lockfile reproducibility goal — the lockfile is generated against a specific uv version, and a different installed uv could resolve different dependency graphs.
Adding a `permissions:` block to a job collapses every unspecified scope to `none`, so listing only `id-token: write` left the job without `contents: read` or `actions: read`. While same-run artifact downloads via `actions/download-artifact` happen to work without `actions: read` today, declaring the scopes the job actually needs makes the intent explicit and is the pattern the pypa/gh-action-pypi-publish docs recommend.
# Conflicts: # .github/workflows/build-release-task.yml # ProjectTemplate.slnx
- docs/host-setup.md: explicitly call out that the snippets below the Supported Hosts list assume a POSIX shell, with WSL2/Git Bash as the Windows path. The earlier "native Windows is supported for host-install" bullet was true for the .NET tooling but misleading next to ``mkdir -p``, ``grep -E``, and ``$(...)`` snippets that don''t work in PowerShell. - docs/ssh-signing.md: make the Verify Signing snippet explicitly sign via ``-S`` plus ``-c gpg.format=ssh``. The previous form relied on ``commit.gpgsign=true`` already being set globally, which is exactly the config the user is verifying — so the verification could create an unsigned empty commit and silently pass.
Address Copilot review on PR #64. - README.md: "PyPI Package" -> "PyPI Packages" so the bullet text and the link text agree (both plural, matching the adjacent NuGet line). - AGENTS.md, docs/devcontainer.md, PyPiLibrary/README.md, PyPiLibrary/pyproject.toml description, and the ``__init__.py`` module docstring: prose mentions of the registry are now "PyPI" (the official capitalization, https://pypi.org), not "PyPi". - Workflow display names (Build / Publish / Download PyPI library ...) use the same casing. The folder/project identifier ``PyPiLibrary`` (and import name ``ptr727_projecttemplate_library``) is unchanged — it''s an established camelcase identifier that disambiguates from ``NuGetLibrary`` on disk, not user-facing branding.
Audit of `.github/workflows/` against AGENTS.md surfaced two findings: - Filename and top-level workflow `name:` follow a clear pattern that AGENTS.md hadn''t spelled out: reusable workflows (those with `on: workflow_call`) use `-task.yml` and a `... task` display name, while entry-point workflows (push/pull_request/schedule/ workflow_dispatch) drop the `-task` suffix entirely (they end with what they DO — `-pull-request.yml`, `-release.yml`) and use a `... action` display name. The display-name suffix lets you tell orchestrators from callees at a glance in the GitHub Actions UI. AGENTS.md now documents this explicitly. - Job `name:` always ends in "job" and step `name:` always ends in "step", with one INTENTIONAL exception: a job whose name is bound to a branch-ruleset required-status-check `context:` value cannot be renamed without breaking enforcement. Currently that''s `Check pull request workflow status` in test-pull-request.yml. The AGENTS.md update calls this out explicitly so future agents don''t "fix" it. - One real step-suffix deviation remained: the `Check workflow results` step at test-pull-request.yml:28 had no ruleset binding — just an oversight when the convention was applied. Renamed to `Check workflow results step`. The two `Check` names sit on the same pair (job + step) by coincidence. The job name is ruleset-locked; the step name is not.
Both ``Template - TODO List`` (root README) and the PyPiLibrary/README deletion checklist referenced ``dependabot.yml``, but the Dependabot config lives at ``.github/dependabot.yml``. Adopters following either checklist would have looked for a non-existent file at the repo root. Path is now qualified.
Two changes bundled because they both touch pyproject.toml. Python 3.14: - ``requires-python = ">=3.14"`` (was ``>=3.13``). - ``Programming Language :: Python :: 3.14`` classifier. - Ruff ``target-version = "py314"`` so it generates / accepts 3.14 syntax. - Pyright ``pythonVersion = "3.14"`` so type-checking matches what the package will run on. - ``uv.lock`` regenerated against ``>=3.14``. Pyright per-path strict mode: - The original ``strict = ["src/**"]`` glob form is ineffective — pyright''s ``strict`` field accepts directory paths, not glob patterns. The previous attempt with ``[[tool.pyright.executionEnvironments]]`` set to ``typeCheckingMode = "strict"`` is also wrong — that key is not recognized inside an executionEnvironment. - Correct form is ``strict = ["src"]`` at the top level. That tells pyright to apply strict type-checking to everything under ``src/`` (equivalent to placing ``# pyright: strict`` at the top of every file under it). ``tests/`` continues to use the global ``typeCheckingMode = "standard"`` so fixture / mock / parametrize typing stays loose. - PyPiLibrary/CODESTYLE.md updated to match. Local validation: ruff check / format --check / pyright / pytest / uv build all clean against Python 3.14.
Address Copilot review on PR #64. - Add empty ``py.typed`` marker under ``src/ptr727_projecttemplate_library/`` per PEP 561. Without this marker, downstream type checkers (mypy/pyright in consumer projects) ignore the inline type information shipped in the wheel — the package is treated as untyped, which negates the strict-mode types this template enforces. Hatchling auto-includes files in the package directory, so the wheel inventory now contains ``ptr727_projecttemplate_library/py.typed`` alongside the modules (verified via ``uv build`` + zip listing). - Alphabetize the README reference-definition blocks. AGENTS.md says "alphabetize the reference definitions block" but the existing blocks were grouped by topic, not sorted. The two blocks (``Shields links`` and ``3rd Party tool links``) are now each alphabetized within themselves; the ``devcontainers-link`` entry moved into the 3rd-party block where it belongs (it''s a marketplace link, not a shields URL). - Switch the ``publish-pypi`` job''s ``environment.url`` from the short form ``https://pypi.org/p/ptr727-projecttemplate-library`` to the canonical ``/project/`` form used elsewhere in the repo (README ``[pypi-link]``). The short form redirects, but consistency matters for the Actions UI and avoids a future broken link if PyPI ever changes the short-form behavior.
5 tasks
ptr727
added a commit
that referenced
this pull request
May 4, 2026
## Summary Adds a Python PyPi template project alongside the .NET `NuGetLibrary`, completing the polyglot template. Modern 2026 stack: hatchling backend, `uv` for env/deps/publish, ruff for lint+format, pyright for typing, pytest for tests, PyPI Trusted Publishing via OIDC. > **Stacked on [#62](#62) (NuGetLibrary rename, merged) and [#63](#63) (devcontainer + docs)**. Once #63 merges this PR's diff cleans up to just the PyPiLibrary work. ## Naming - Folder: `PyPiLibrary/` — qualifier on disk to disambiguate from `NuGetLibrary/` - Published PyPI name: `ptr727-projecttemplate-library` — **no `pypi` qualifier**, mirrors the NuGet identity - Python import name: `ptr727_projecttemplate_library` ## New tree ```text PyPiLibrary/ pyproject.toml # hatchling backend, ruff/pyright/pytest config, PEP 735 [dependency-groups] README.md # what this PyPi template is + uv quickstart + Trusted Publisher setup uv.lock # committed for reproducible CI src/ ptr727_projecttemplate_library/ __init__.py _version.py # __version__ = "0.0.0" placeholder; see README.md "Template Adoption" for version-scheme options example.py # trivial greet() function tests/ test_example.py # 3 tests ``` ## Workflow plumbing The split-by-purpose layout was chosen so `id-token: write` (required by Trusted Publishing) only has to be granted on the entry-point job, not propagated through reusable-workflow chains: - **New** `.github/workflows/build-pypilibrary-task.yml` — reusable workflow that **only builds**: setup uv (pinned to `0.11.8` to match the devcontainer), sync, ruff check, ruff format --check, pyright, pytest, `uv build`, upload artifact. **No publish job here**, no `id-token: write`. - **Modified** `.github/workflows/build-release-task.yml` — adds a `build-pypilibrary` job calling the new reusable workflow, includes it in the `github-release` `needs:` list. Build runs unconditionally (matches the always-validate-on-PR semantic of the rest of the workflow). **No `pypi: bool` input** — would require id-token propagation through the test-pull-request chain (and triggered `startup_failure`, fixed in 4c939f6). - **Modified** `.github/workflows/publish-release.yml` — adds a top-level `publish-pypi` job that runs after `create-release`, downloads the `pypilibrary-build` artifact by name (artifacts uploaded by reusable workflows are accessible to sibling jobs in the same run), and publishes via Trusted Publishing. **`id-token: write` lives only here**, alongside the explicit `contents: read` and `actions: read` needed for `actions/download-artifact`. Uses `skip-existing: true` so the placeholder `0.0.0` version doesn't fail the workflow on repeated pushes. - **Modified** `.github/workflows/test-release-task.yml` — no PyPi-specific input needed; the build runs as part of the existing chain. ## Other plumbing - **`.github/dependabot.yml`** — adds `package-ecosystem: "uv"` targeting `/PyPiLibrary` with the `pypi-deps` group label. Existing `nuget` and `github-actions` blocks normalized to standard two-space indentation under `updates:`. - **`.husky/task-runner.json`** — adds `Ruff Format` and `Ruff Check` tasks scoped to `PyPiLibrary/**/*.py`. Both pass `${staged}` as positional args via `bash -c "..." -- ${staged}` so paths with spaces survive; both gate on `command -v uv` so a `.cs`-only commit on a contributor without uv installed doesn't fail. - **`ProjectTemplate.code-workspace`** — adds Python format-on-save with ruff, the `[python]` formatter binding, `python.terminal.activateEnvironment: false`. No hard-coded venv paths (those caused "could not find ruff binary" popups before `uv sync` ran). Adds `unwantedRecommendations` for mypy / pylint / flake8 / isort / black / standalone pyright so contributors aren't prompted to install tools that overlap with ruff and Pylance. - **`ProjectTemplate.slnx`** — adds `build-pypilibrary-task.yml` to the GitHub Actions folder. - **`.gitignore`** — adds `.venv/`, `dist/`, `__pycache__/`, `*.py[cod]`, `*.egg-info/`, `.pytest_cache/`, `.ruff_cache/`, `.pyright/`. - **`README.md`** — PyPI badge + link in the build/distribution and releases sections; template TODO list reminds the deriver to delete the unused language side. `gh` "pre-authenticated" wording softened to call out the Keychain/libsecret credential-store limitation. ## Trusted Publisher setup (one-time, on PyPI side) 1. PyPI → **Account settings** → **Publishing** → **Add a new pending publisher** - Project name: `ptr727-projecttemplate-library` - Owner: `ptr727` - Repo: `ProjectTemplate` - Workflow: `publish-release.yml` - Environment: `pypi` 2. GitHub repo → **Settings** → **Environments** → create `pypi` environment (optionally with required reviewers). The first successful release converts the pending publisher to a real publisher. ## Versioning gap `_version.py` ships with `__version__ = "0.0.0"`. Trusted Publishing with `skip-existing: true` means the workflow won't fail, but no new PyPI versions land until you wire `_version.py` to something that increments — see `PyPiLibrary/README.md` "Template Adoption" for the three usual options (hatch-vcs / version.json bridge / manual bumps). ## Test plan - [x] `uv sync` clean (host: uv 0.11.8) - [x] `uv run ruff check` — All checks passed - [x] `uv run ruff format --check` — clean - [x] `uv run pyright` — 0 errors, 0 warnings, 0 informations - [x] `uv run pytest` — 3 passed - [x] `uv build` — produces `ptr727_projecttemplate_library-0.0.0.tar.gz` and wheel - [x] `dotnet build` — 0 warnings, 0 errors - [x] `dotnet test` — 15 passed (no .NET regression) - [ ] CI green on the PR (test-release-task exercises ruff, pyright, pytest, uv build via the same reusable workflow that publish uses) - [ ] After Trusted Publisher is configured on PyPI and a real version scheme is wired in `_version.py`, next merge to `main` smoke-tests the publish path
3 tasks
ptr727
added a commit
that referenced
this pull request
May 11, 2026
## Summary Two follow-ups for [PR #66](#66) (the active `develop` → `main` release PR): ### 1. README.md — missing colons on NuGet/PyPI bullets Copilot review thread on PR #66 flagged that the NuGet and PyPI bullets in the **Build and Distribution** list are missing the colon after the bold label that every other bullet in the list uses. ```diff - - **NuGet Packages** [NuGet Packages][nuget-link] - .NET libraries published to NuGet.org. - - **PyPI Packages** [PyPI Packages][pypi-link] - Python library published to PyPI.org. + - **NuGet Packages**: [NuGet Packages][nuget-link] - .NET libraries published to NuGet.org. + - **PyPI Packages**: [PyPI Packages][pypi-link] - Python library published to PyPI.org. ``` ### 2. publish-release.yml — wrong SHA for `pypa/gh-action-pypi-publish@v1.14.0` The action was pinned to SHA `6733eb7d741f0b11ec6a39b58540dab7590f9b7d` with a `# v1.14.0` comment, but the upstream `v1.14.0` tag actually points at `cef221092ed1bacb1cc03d23a2d87d1d172e277b`. Because `ghcr.io/pypa/gh-action-pypi-publish` is tagged by release SHAs, no GHCR image existed at the wrong SHA — Docker bailed out with `manifest unknown`. This has caused **`Publish PyPI library job` to fail on every push to `develop`** since PR #64 added the action. CI evidence: - Run on `25c338b9` (May 4) — failed at the same step. - Run on `0da21b2` (today, the PR #68 merge) — failed at the same step. Fix: use the actual upstream `v1.14.0` SHA, keep the `# v1.14.0` comment. ## Why a new PR (not committed onto PR #66's branch) Standing project rule: no direct commits to `develop`. Once this PR merges to `develop`, PR #66's diff absorbs both fixes automatically (since #66 is `develop` → `main`), and the README Copilot thread on #66 can be resolved. ## Test plan - [ ] CI passes on this PR (in particular, the publish job won't run on a non-release push — but the resolution will only be observable on the next release push to `develop`). - [ ] After merge, PR #66's CI re-runs with both fixes and `Publish PyPI library job` succeeds. - [ ] PR #66 README Copilot thread can be replied/resolved citing this merge commit. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ptr727
added a commit
that referenced
this pull request
May 11, 2026
…me (#66) Release merge: brings five squashed PRs from develop into main. ## Squashed PRs included - **#61 — Pin release action SHA, target_commitish, agent conventions.** `softprops/action-gh-release` pinned to a commit SHA with `target_commitish: ${{ github.sha }}` so the release tag lands on the artifact's commit, not the default branch. Updated `AGENTS.md` workflow YAML conventions. - **#62 — Rename Library project to NuGetLibrary.** Project + folder renamed; `.slnx`, `.csproj`, build workflow, and references updated. Disambiguates from the new Python sibling. - **#63 — Add devcontainer + per-OS host and SSH signing docs.** New `docs/host-setup.md`, `docs/ssh-signing.md`, `docs/devcontainer.md`. Devcontainer bind-mounts SSH public key, allowed_signers, and `gh` config so commits sign correctly inside the container. - **#64 — Add PyPiLibrary Python sibling project.** New `PyPiLibrary/` template under `src/`-layout: pyproject.toml + uv.lock + ruff/pyright/pytest config + sample module + tests + `build-pypilibrary-task.yml` workflow + `publish-pypi` job in `publish-release.yml`. - **#65 — Split Devcontainer and Workspace per Language and Drop Husky.** `.devcontainer/dotnet/` + `DotNet.code-workspace` and `.devcontainer/python/` + `Python.code-workspace`. Husky.Net removed (CI is the lint backstop). Optional opt-in hooks documented in README. All Husky references removed from workflows, AGENTS, CODESTYLE, and tasks. ## Notes - Merge method: **merge-commit** (per [AGENTS.md branching model](https://github.com/ptr727/ProjectTemplate/blob/develop/AGENTS.md#branching-model)). Squash and rebase are blocked by the main ruleset. - Main currently has 6 codegen-update commits that develop doesn't have (#58, #59, #60 etc.). The merge-commit re-anchors develop on top of those; the next develop cycle will start by merging main back into develop to absorb them. ## Test plan - [ ] CI passes on the merge commit (test-release-task workflow, all build matrix legs). - [ ] Confirm release tag lands on the merge commit (target_commitish from #61). - [ ] Spot-check the new `.devcontainer/dotnet/` and `.devcontainer/python/` open and build cleanly. - [ ] Spot-check `cd PyPiLibrary && uv sync && uv run pytest` passes. - [ ] Confirm `.git/hooks/pre-commit` is absent in a fresh clone (Husky removed).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a Python PyPi template project alongside the .NET
NuGetLibrary, completing the polyglot template. Modern 2026 stack: hatchling backend,uvfor env/deps/publish, ruff for lint+format, pyright for typing, pytest for tests, PyPI Trusted Publishing via OIDC.Naming
PyPiLibrary/— qualifier on disk to disambiguate fromNuGetLibrary/ptr727-projecttemplate-library— nopypiqualifier, mirrors the NuGet identityptr727_projecttemplate_libraryNew tree
Workflow plumbing
The split-by-purpose layout was chosen so
id-token: write(required by Trusted Publishing) only has to be granted on the entry-point job, not propagated through reusable-workflow chains:.github/workflows/build-pypilibrary-task.yml— reusable workflow that only builds: setup uv (pinned to0.11.8to match the devcontainer), sync, ruff check, ruff format --check, pyright, pytest,uv build, upload artifact. No publish job here, noid-token: write..github/workflows/build-release-task.yml— adds abuild-pypilibraryjob calling the new reusable workflow, includes it in thegithub-releaseneeds:list. Build runs unconditionally (matches the always-validate-on-PR semantic of the rest of the workflow). Nopypi: boolinput — would require id-token propagation through the test-pull-request chain (and triggeredstartup_failure, fixed in 4c939f6)..github/workflows/publish-release.yml— adds a top-levelpublish-pypijob that runs aftercreate-release, downloads thepypilibrary-buildartifact by name (artifacts uploaded by reusable workflows are accessible to sibling jobs in the same run), and publishes via Trusted Publishing.id-token: writelives only here, alongside the explicitcontents: readandactions: readneeded foractions/download-artifact. Usesskip-existing: trueso the placeholder0.0.0version doesn't fail the workflow on repeated pushes..github/workflows/test-release-task.yml— no PyPi-specific input needed; the build runs as part of the existing chain.Other plumbing
.github/dependabot.yml— addspackage-ecosystem: "uv"targeting/PyPiLibrarywith thepypi-depsgroup label. Existingnugetandgithub-actionsblocks normalized to standard two-space indentation underupdates:..husky/task-runner.json— addsRuff FormatandRuff Checktasks scoped toPyPiLibrary/**/*.py. Both pass${staged}as positional args viabash -c "..." -- ${staged}so paths with spaces survive; both gate oncommand -v uvso a.cs-only commit on a contributor without uv installed doesn't fail.ProjectTemplate.code-workspace— adds Python format-on-save with ruff, the[python]formatter binding,python.terminal.activateEnvironment: false. No hard-coded venv paths (those caused "could not find ruff binary" popups beforeuv syncran). AddsunwantedRecommendationsfor mypy / pylint / flake8 / isort / black / standalone pyright so contributors aren't prompted to install tools that overlap with ruff and Pylance.ProjectTemplate.slnx— addsbuild-pypilibrary-task.ymlto the GitHub Actions folder..gitignore— adds.venv/,dist/,__pycache__/,*.py[cod],*.egg-info/,.pytest_cache/,.ruff_cache/,.pyright/.README.md— PyPI badge + link in the build/distribution and releases sections; template TODO list reminds the deriver to delete the unused language side.gh"pre-authenticated" wording softened to call out the Keychain/libsecret credential-store limitation.Trusted Publisher setup (one-time, on PyPI side)
ptr727-projecttemplate-libraryptr727ProjectTemplatepublish-release.ymlpypipypienvironment (optionally with required reviewers).The first successful release converts the pending publisher to a real publisher.
Versioning gap
_version.pyships with__version__ = "0.0.0". Trusted Publishing withskip-existing: truemeans the workflow won't fail, but no new PyPI versions land until you wire_version.pyto something that increments — seePyPiLibrary/README.md"Template Adoption" for the three usual options (hatch-vcs / version.json bridge / manual bumps).Test plan
uv syncclean (host: uv 0.11.8)uv run ruff check— All checks passeduv run ruff format --check— cleanuv run pyright— 0 errors, 0 warnings, 0 informationsuv run pytest— 3 passeduv build— producesptr727_projecttemplate_library-0.0.0.tar.gzand wheeldotnet build— 0 warnings, 0 errorsdotnet test— 15 passed (no .NET regression)_version.py, next merge tomainsmoke-tests the publish path