Skip to content

Add devcontainer + per-OS host and SSH signing docs - #63

Merged
ptr727 merged 12 commits into
developfrom
devcontainer-docs
May 3, 2026
Merged

Add devcontainer + per-OS host and SSH signing docs#63
ptr727 merged 12 commits into
developfrom
devcontainer-docs

Conversation

@ptr727

@ptr727 ptr727 commented May 3, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a single unified Dev Container hosting both the .NET 10 SDK and the upcoming Python uv toolchain, plus three focused docs files that decompose host setup, devcontainer setup, and SSH commit signing per-OS. On Windows, the devcontainer flow requires WSL2 (the bind-mounts use POSIX paths). The host-install flow in README.md supports native Windows via winget — see docs/host-setup.md for the per-flow scope.

Stacked on #62 (NuGetLibrary rename). The diff against develop will show PR #62's changes until that PR merges; once it does, the diff here cleans up to just the devcontainer + docs work.

Changes

New files:

  • .devcontainer/devcontainer.json — base mcr.microsoft.com/devcontainers/dotnet:1-10.0, gh and common-utils features. Bind-mounts ~/.ssh/id_ed25519.pub (read-only), ~/.config/git/allowed_signers (read-only), ~/.config/gh (read-write). ${localEnv:HOME}${localEnv:USERPROFILE} form covers Linux/macOS and WSL2 hosts. Extension list mirrors ProjectTemplate.code-workspace recommendations.
  • .devcontainer/post-create.sh (executable, mode 100755) — installs uv from astral.sh (pinned to a specific version via the version-prefixed install URL; re-installs on version mismatch so the pin holds even when uv is already on PATH), runs dotnet tool restore, installs Husky.Net hooks, pre-syncs PyPiLibrary if it exists (guarded so this script works before PR 5 lands).
  • docs/host-setup.md — git identity, Ed25519 SSH key, allowed_signers, gh auth login, per-OS ssh-agent setup, verify checklist.
  • docs/devcontainer.md — bind-mount table, lifecycle commands, gh credential-store nuance (Keychain vs libsecret vs file), verify checklist, troubleshooting.
  • docs/ssh-signing.md — per-OS deltas (systemd ssh-agent on Linux, Apple Keychain on macOS, WSL2 caveats), allowed_signers format, devcontainer interaction, troubleshooting.

Modified:

  • README.md — adds a "Recommended (devcontainer)" path to the Development Environment Setup section linking to the new docs; the existing host-install path stays. Template Project Setup section now points at the docs files for prerequisites instead of inlining them.
  • ProjectTemplate.code-workspace — adds ms-python.python and charliermarsh.ruff to recommendations (mirrors the devcontainer list); adds unwantedRecommendations for ms-pyright.pyright (deprecated; pyright is provided by Pylance which ms-python.python auto-installs), ms-python.mypy-type-checker, ms-python.pylint, ms-python.flake8, ms-python.isort, and ms-python.black-formatter so contributors aren't prompted to install tools that overlap with ruff + Pylance and would surface "could not find binary" connection errors against the venv. Also adds astral, devcontainer, hatchling, Keychain, libsecret, onCreateCommand, postCreateCommand, pyproject, pypi, pypilibrary, pyright, ruff to cSpell.words.

Why a single unified container

VS Code Dev Containers does not support per-folder containers in the same multi-root window — only a picker per session. A single image with both .NET and uv is the simplest mental model and lets downstream users delete the language they don't need by removing a feature line and a postCreateCommand step. See VS Code Dev Containers docs for the limitation.

Why bind-mount the public key, not the private key

The private key never enters the container. Signing happens via the SSH agent socket forwarded by VS Code Dev Containers (SSH_AUTH_SOCK). The public key plus allowed_signers is enough for git to know which key to delegate signing to and to verify signatures in git log --show-signature.

Why an onCreateCommand chown

On macOS hosts the bind-mount surfaces /home/vscode/.ssh as root-owned inside the container, which would block gh from updating known_hosts. The chown is idempotent on Linux and WSL2 so it stays unconditional rather than gated on host detection.

Test plan

  • gh pr create --base develop --head devcontainer-docs succeeded
  • CI green on the PR (test-pull-request workflow)
  • Build the devcontainer on Linux, run git -c gpg.format=ssh commit -S --allow-empty -m verify inside, verify it signs
  • Build the devcontainer on macOS host, verify the onCreateCommand chown lets gh auth status work
  • Build on WSL2 host, verify behavior matches Linux
  • Confirm recommendations in code-workspace and customizations.vscode.extensions in devcontainer.json are identical
  • Markdown lint passes on the new docs files

ptr727 added 2 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.
Copilot AI review requested due to automatic review settings May 3, 2026 15:32

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 unified VS Code Dev Container workflow (for .NET 10 + upcoming Python uv) and splits contributor setup guidance into focused docs, while also including the stacked NuGetLibrary rename/reference updates.

Changes:

  • Add .devcontainer/ configuration + post-create.sh to bootstrap tools (uv, dotnet local tools, Husky.Net hooks).
  • Add new docs for host setup, devcontainer usage, and SSH-based commit signing; wire them into README.md.
  • Update solution/workflows/projects/usings to reflect LibraryNuGetLibrary rename (stacked on #62).

Reviewed changes

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

Show a summary per file
File Description
docs/ssh-signing.md New per-OS SSH commit signing instructions and troubleshooting.
docs/host-setup.md New host prerequisites guide (git identity, SSH key, allowed_signers, gh auth).
docs/devcontainer.md New devcontainer usage docs (mounts, lifecycle commands, verification).
.devcontainer/devcontainer.json Adds unified devcontainer image/features, mounts, lifecycle commands, extensions.
.devcontainer/post-create.sh Installs uv, restores dotnet tools, installs Husky.Net hooks, pre-syncs PyPiLibrary.
README.md Links new docs and adds a recommended devcontainer-based setup path.
ProjectTemplate.code-workspace Adds Python-related extension recommendations + cSpell words.
ProjectTemplate.slnx Updates workflow path + project dependencies for NuGetLibrary rename.
.github/workflows/build-release-task.yml Updates reusable workflow call/needs to build-nugetlibrary.
.github/workflows/build-nugetlibrary-task.yml Renames/repoints build workflow from Library → NuGetLibrary + artifact naming.
Console/Console.csproj Updates ProjectReference to NuGetLibrary.
Console/Program.cs Updates using to ptr727.ProjectTemplate.NuGetLibrary.
Tests/Tests.csproj Updates ProjectReference to NuGetLibrary.
Tests/LoggingTests.cs Updates using to ptr727.ProjectTemplate.NuGetLibrary.
Benchmarks/Benchmarks.csproj Updates ProjectReference to NuGetLibrary.
NuGetLibrary/NuGetLibrary.csproj Updates RootNamespace to ptr727.ProjectTemplate.NuGetLibrary.
NuGetLibrary/Library.cs Updates namespace to NuGetLibrary.
NuGetLibrary/Options.cs Updates namespace to NuGetLibrary.
NuGetLibrary/LogOptions.cs Updates namespace to NuGetLibrary.
NuGetLibrary/Extensions.cs Updates namespace to NuGetLibrary.
NuGetLibrary/GlobalUsings.cs Adds global usings for logging types.
NuGetLibrary/.editorconfig Adds per-project analyzer suppression for missing XML docs.
AGENTS.md Updates project structure docs for NuGetLibrary rename.
.github/copilot-instructions.md Updates documentation/examples for NuGetLibrary rename.

Comment thread .devcontainer/post-create.sh
Comment thread .devcontainer/post-create.sh Outdated
Comment thread .devcontainer/devcontainer.json Outdated
@ptr727 ptr727 mentioned this pull request May 3, 2026
10 tasks
- 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.
ptr727 added a commit that referenced this pull request May 3, 2026
- 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.
`${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.
Copilot AI review requested due to automatic review settings May 3, 2026 17:06

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 24 changed files in this pull request and generated 2 comments.

Comment thread .devcontainer/devcontainer.json Outdated
Comment thread docs/ssh-signing.md
ptr727 added 2 commits May 3, 2026 10:13
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.
# Conflicts:
#	.github/workflows/build-release-task.yml
Copilot AI review requested due to automatic review settings May 3, 2026 18:20

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 24 changed files in this pull request and generated 3 comments.

Comment thread .devcontainer/post-create.sh Outdated
Comment thread docs/host-setup.md Outdated
Comment thread docs/host-setup.md Outdated
- 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.
ptr727 added a commit that referenced this pull request May 3, 2026
- 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.
@ptr727
ptr727 requested a review from Copilot May 3, 2026 18:56

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 24 changed files in this pull request and generated 2 comments.

Comment thread .devcontainer/post-create.sh Outdated
Comment thread docs/host-setup.md
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.

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 7 out of 7 changed files in this pull request and generated 4 comments.

Comment thread docs/devcontainer.md Outdated
Comment thread .devcontainer/post-create.sh Outdated
Comment thread README.md Outdated
Comment thread docs/devcontainer.md Outdated
ptr727 added a commit that referenced this pull request May 3, 2026
- docs/devcontainer.md: update the "What's Inside" table row for uv to
  show the version-pinned install URL and the actual script path
  (`.devcontainer/post-create.sh`). Earlier wording matched an older
  unpinned form.
- docs/devcontainer.md and README.md: soften "gh is pre-authenticated"
  wording. The bind-mount of `~/.config/gh` only carries file-backed
  tokens; macOS Keychain and Linux libsecret-backed tokens require an
  in-container `gh auth login`. Both docs now point at the credential-
  store nuance section in `docs/devcontainer.md` so contributors set
  expectations correctly.
- .devcontainer/post-create.sh: download the pinned uv installer to a
  tempfile, log its sha256 to stderr, then run it (instead of
  `curl … | sh`). The hash provides an audit trail of exactly what was
  executed and lets an operator pin a known-good checksum via
  `EXPECTED_SHA` later. Astral does not currently publish per-version
  installer checksums in a machine-verifiable form, so the check is
  opt-in for now; once they do, set `EXPECTED_SHA` and the script
  refuses to run on mismatch.
ptr727 added 2 commits May 3, 2026 12:11
- docs/devcontainer.md: update the "What's Inside" table row for uv to
  show the version-pinned install URL and the actual script path
  (`.devcontainer/post-create.sh`). Earlier wording matched an older
  unpinned form.
- docs/devcontainer.md and README.md: soften "gh is pre-authenticated"
  wording. The bind-mount of `~/.config/gh` only carries file-backed
  tokens; macOS Keychain and Linux libsecret-backed tokens require an
  in-container `gh auth login`. Both docs now point at the credential-
  store nuance section in `docs/devcontainer.md` so contributors set
  expectations correctly.
- .devcontainer/post-create.sh: download the pinned uv installer to a
  tempfile, log its sha256 to stderr, then run it (instead of
  `curl … | sh`). The hash provides an audit trail of exactly what was
  executed and lets an operator pin a known-good checksum via
  `EXPECTED_SHA` later. Astral does not currently publish per-version
  installer checksums in a machine-verifiable form, so the check is
  opt-in for now; once they do, set `EXPECTED_SHA` and the script
  refuses to run on mismatch.
- Remove `ms-pyright.pyright` from recommendations. Microsoft now ships
  pyright inside Pylance, which `ms-python.python` auto-installs;
  having both active causes the standalone pyright extension to fight
  Pylance for the same files. The standalone extension is in
  maintenance mode per Microsoft's own guidance.
- Add `unwantedRecommendations` for mypy, pylint, flake8, isort, and
  black. We use ruff (lint + format + import sort) and pyright (via
  Pylance) — every other Python linter/formatter overlaps and shows
  duplicate diagnostics or, worse, "could not find binary" connection
  errors when the venv doesn''t have it installed (which is the
  current symptom).

If a contributor already has any of these extensions installed
manually, VS Code now flags them as not recommended for this workspace
and offers a one-click disable.
Mirrors the workspace `recommendations` change in dd7073d. Pylance
(auto-installed with ms-python.python) provides pyright; the standalone
extension fights it for the same files.
Copilot AI review requested due to automatic review settings May 3, 2026 19: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 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread ProjectTemplate.code-workspace
@ptr727
ptr727 merged commit eb64228 into develop May 3, 2026
24 checks passed
ptr727 added a commit that referenced this pull request May 3, 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 4, 2026
## Summary

Adds a single unified [Dev Container](https://containers.dev/) hosting
both the .NET 10 SDK and the upcoming Python `uv` toolchain, plus three
focused docs files that decompose host setup, devcontainer setup, and
SSH commit signing per-OS. On Windows, the **devcontainer flow**
requires WSL2 (the bind-mounts use POSIX paths). The **host-install
flow** in `README.md` supports native Windows via winget — see
`docs/host-setup.md` for the per-flow scope.

> **Stacked on [#62](#62)
(NuGetLibrary rename)**. The diff against `develop` will show PR #62's
changes until that PR merges; once it does, the diff here cleans up to
just the devcontainer + docs work.

## Changes

**New files**:

- `.devcontainer/devcontainer.json` — base
`mcr.microsoft.com/devcontainers/dotnet:1-10.0`, `gh` and `common-utils`
features. Bind-mounts `~/.ssh/id_ed25519.pub` (read-only),
`~/.config/git/allowed_signers` (read-only), `~/.config/gh`
(read-write). `${localEnv:HOME}${localEnv:USERPROFILE}` form covers
Linux/macOS and WSL2 hosts. Extension list mirrors
`ProjectTemplate.code-workspace` `recommendations`.
- `.devcontainer/post-create.sh` (executable, mode 100755) — installs
`uv` from `astral.sh` (pinned to a specific version via the
version-prefixed install URL; re-installs on version mismatch so the pin
holds even when uv is already on PATH), runs `dotnet tool restore`,
installs Husky.Net hooks, pre-syncs `PyPiLibrary` if it exists (guarded
so this script works before PR 5 lands).
- `docs/host-setup.md` — git identity, Ed25519 SSH key,
`allowed_signers`, `gh auth login`, per-OS ssh-agent setup, verify
checklist.
- `docs/devcontainer.md` — bind-mount table, lifecycle commands, `gh`
credential-store nuance (Keychain vs libsecret vs file), verify
checklist, troubleshooting.
- `docs/ssh-signing.md` — per-OS deltas (systemd ssh-agent on Linux,
Apple Keychain on macOS, WSL2 caveats), `allowed_signers` format,
devcontainer interaction, troubleshooting.

**Modified**:

- `README.md` — adds a "Recommended (devcontainer)" path to the
Development Environment Setup section linking to the new docs; the
existing host-install path stays. Template Project Setup section now
points at the docs files for prerequisites instead of inlining them.
- `ProjectTemplate.code-workspace` — adds `ms-python.python` and
`charliermarsh.ruff` to `recommendations` (mirrors the devcontainer
list); adds `unwantedRecommendations` for `ms-pyright.pyright`
(deprecated; pyright is provided by Pylance which `ms-python.python`
auto-installs), `ms-python.mypy-type-checker`, `ms-python.pylint`,
`ms-python.flake8`, `ms-python.isort`, and `ms-python.black-formatter`
so contributors aren't prompted to install tools that overlap with ruff
+ Pylance and would surface "could not find binary" connection errors
against the venv. Also adds `astral`, `devcontainer`, `hatchling`,
`Keychain`, `libsecret`, `onCreateCommand`, `postCreateCommand`,
`pyproject`, `pypi`, `pypilibrary`, `pyright`, `ruff` to `cSpell.words`.

## Why a single unified container

VS Code Dev Containers does not support per-folder containers in the
same multi-root window — only a picker per session. A single image with
both .NET and `uv` is the simplest mental model and lets downstream
users delete the language they don't need by removing a feature line and
a postCreateCommand step. See [VS Code Dev Containers
docs](https://code.visualstudio.com/remote/advancedcontainers/connect-multiple-containers)
for the limitation.

## Why bind-mount the public key, not the private key

The private key never enters the container. Signing happens via the SSH
agent socket forwarded by VS Code Dev Containers (`SSH_AUTH_SOCK`). The
public key plus `allowed_signers` is enough for git to know which key to
delegate signing to and to verify signatures in `git log
--show-signature`.

## Why an `onCreateCommand` chown

On macOS hosts the bind-mount surfaces `/home/vscode/.ssh` as root-owned
inside the container, which would block `gh` from updating
`known_hosts`. The chown is idempotent on Linux and WSL2 so it stays
unconditional rather than gated on host detection.

## Test plan

- [x] `gh pr create --base develop --head devcontainer-docs` succeeded
- [ ] CI green on the PR (test-pull-request workflow)
- [ ] Build the devcontainer on Linux, run `git -c gpg.format=ssh commit
-S --allow-empty -m verify` inside, verify it signs
- [ ] Build the devcontainer on macOS host, verify the `onCreateCommand`
chown lets `gh auth status` work
- [ ] Build on WSL2 host, verify behavior matches Linux
- [ ] Confirm `recommendations` in `code-workspace` and
`customizations.vscode.extensions` in `devcontainer.json` are identical
- [ ] Markdown lint passes on the new docs files
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
ptr727 deleted the devcontainer-docs branch May 11, 2026 15:18
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