Add devcontainer + per-OS host and SSH signing docs - #63
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.
Contributor
There was a problem hiding this comment.
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.shto 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
Library→NuGetLibraryrename (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. |
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.
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
- 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.
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.
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.
- 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.
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
5 tasks
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
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 single unified Dev Container hosting both the .NET 10 SDK and the upcoming Python
uvtoolchain, 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 inREADME.mdsupports native Windows via winget — seedocs/host-setup.mdfor the per-flow scope.Changes
New files:
.devcontainer/devcontainer.json— basemcr.microsoft.com/devcontainers/dotnet:1-10.0,ghandcommon-utilsfeatures. 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 mirrorsProjectTemplate.code-workspacerecommendations..devcontainer/post-create.sh(executable, mode 100755) — installsuvfromastral.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), runsdotnet tool restore, installs Husky.Net hooks, pre-syncsPyPiLibraryif 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,ghcredential-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_signersformat, 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— addsms-python.pythonandcharliermarsh.rufftorecommendations(mirrors the devcontainer list); addsunwantedRecommendationsforms-pyright.pyright(deprecated; pyright is provided by Pylance whichms-python.pythonauto-installs),ms-python.mypy-type-checker,ms-python.pylint,ms-python.flake8,ms-python.isort, andms-python.black-formatterso 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 addsastral,devcontainer,hatchling,Keychain,libsecret,onCreateCommand,postCreateCommand,pyproject,pypi,pypilibrary,pyright,rufftocSpell.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
uvis 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 plusallowed_signersis enough for git to know which key to delegate signing to and to verify signatures ingit log --show-signature.Why an
onCreateCommandchownOn macOS hosts the bind-mount surfaces
/home/vscode/.sshas root-owned inside the container, which would blockghfrom updatingknown_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-docssucceededgit -c gpg.format=ssh commit -S --allow-empty -m verifyinside, verify it signsonCreateCommandchown letsgh auth statusworkrecommendationsincode-workspaceandcustomizations.vscode.extensionsindevcontainer.jsonare identical