Skip to content

Add PyPiLibrary Python sibling project - #64

Merged
ptr727 merged 27 commits into
developfrom
pypilibrary
May 3, 2026
Merged

Add PyPiLibrary Python sibling project#64
ptr727 merged 27 commits into
developfrom
pypilibrary

Conversation

@ptr727

@ptr727 ptr727 commented May 3, 2026

Copy link
Copy Markdown
Owner

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 (NuGetLibrary rename, merged) and #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-libraryno pypi qualifier, mirrors the NuGet identity
  • Python import name: ptr727_projecttemplate_library

New tree

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 settingsPublishingAdd a new pending publisher
    • Project name: ptr727-projecttemplate-library
    • Owner: ptr727
    • Repo: ProjectTemplate
    • Workflow: publish-release.yml
    • Environment: pypi
  2. GitHub repo → SettingsEnvironments → 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

  • uv sync clean (host: uv 0.11.8)
  • uv run ruff check — All checks passed
  • uv run ruff format --check — clean
  • uv run pyright — 0 errors, 0 warnings, 0 informations
  • uv run pytest — 3 passed
  • uv build — produces ptr727_projecttemplate_library-0.0.0.tar.gz and wheel
  • dotnet build — 0 warnings, 0 errors
  • 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

ptr727 added 3 commits May 3, 2026 08:22
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).
Copilot AI review requested due to automatic review settings May 3, 2026 15:37
- 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 → NuGetLibrary rename 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.

Comment thread .github/dependabot.yml Outdated
Comment thread .husky/task-runner.json
Comment thread .husky/task-runner.json
Comment thread .devcontainer/devcontainer.json
Comment thread .devcontainer/post-create.sh
ptr727 added 3 commits May 3, 2026 10:06
`${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.
Copilot AI review requested due to automatic review settings May 3, 2026 17:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 38 changed files in this pull request and generated 3 comments.

Comment thread .devcontainer/devcontainer.json
Comment thread .github/workflows/build-pypilibrary-task.yml Outdated
Comment thread PyPiLibrary/README.md Outdated
ptr727 added 2 commits May 3, 2026 10:20
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
Copilot AI review requested due to automatic review settings May 3, 2026 18:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 37 changed files in this pull request and generated 5 comments.

Comment thread PyPiLibrary/README.md Outdated
Comment thread PyPiLibrary/README.md Outdated
Comment thread .github/workflows/build-release-task.yml Outdated
Comment thread .husky/task-runner.json
Comment thread .husky/task-runner.json
ptr727 added 3 commits May 3, 2026 11:33
- 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.
Copilot AI review requested due to automatic review settings May 3, 2026 18:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 37 changed files in this pull request and generated 1 comment.

Comment thread .github/workflows/publish-release.yml
ptr727 added 2 commits May 3, 2026 12:02
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 22 changed files in this pull request and generated no new comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 26 changed files in this pull request and generated 2 comments.

Comment thread docs/host-setup.md
Comment thread docs/ssh-signing.md Outdated
ptr727 added 2 commits May 3, 2026 12:47
- 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 20 out of 23 changed files in this pull request and generated 3 comments.

Comment thread README.md Outdated
Comment thread PyPiLibrary/pyproject.toml Outdated
Comment thread PyPiLibrary/README.md Outdated
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.
@ptr727
ptr727 requested a review from Copilot May 3, 2026 20:02
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 25 changed files in this pull request and generated 2 comments.

Comment thread README.md Outdated
Comment thread PyPiLibrary/README.md Outdated
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 25 changed files in this pull request and generated 1 comment.

Comment thread PyPiLibrary/pyproject.toml Outdated
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 25 changed files in this pull request and generated 3 comments.

Comment thread PyPiLibrary/pyproject.toml
Comment thread README.md
Comment thread .github/workflows/publish-release.yml Outdated
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 26 changed files in this pull request and generated no new comments.

@ptr727
ptr727 merged commit ffb1bdd into develop May 3, 2026
25 checks passed
@ptr727
ptr727 deleted the pypilibrary branch May 3, 2026 21:37
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
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants