diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 34953b2..90fca36 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -3,7 +3,11 @@
name: CI
permissions:
- contents: write
+ contents: read
+
+concurrency:
+ group: ci-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.ref }}
+ cancel-in-progress: ${{ github.event_name == 'pull_request' }}
on:
push:
@@ -26,6 +30,8 @@ jobs:
build:
if: github.event_name == 'pull_request' || (github.event_name == 'push' && contains(toJson(github.event.commits), '***NO_CI***') == false && contains(toJson(github.event.commits), '[ci skip]') == false && contains(toJson(github.event.commits), '[skip ci]') == false)
runs-on: ubuntu-latest
+ outputs:
+ publication_succeeded: ${{ steps.publish.outputs.succeeded }}
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1
@@ -37,6 +43,8 @@ jobs:
steps:
- name: checkout code
uses: actions/checkout@v5
+ with:
+ persist-credentials: false
- name: setup .net core sdk
uses: actions/setup-dotnet@v5
@@ -55,28 +63,87 @@ jobs:
- name: dotnet pack
run: dotnet pack solrevdev.seedfolder.sln -c Release --no-build --include-source --include-symbols
- - name: Publish NuGet
- if: github.event_name == 'push' && github.ref == 'refs/heads/master'
- env:
- NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
+ - name: verify versioned package
+ id: package
+ shell: bash
run: |
version=$(sed -n 's:.*\(.*\).*:\1:p' src/solrevdev.seedfolder.csproj)
package="src/nupkg/solrevdev.seedfolder.${version}.nupkg"
- tag="v${version}"
- if [[ -z "$version" || ! -f "$package" ]]; then
- echo "Unable to find the package version or package file." >&2
+ if [[ -z "$version" ]]; then
+ echo "Unable to find the package version." >&2
+ exit 1
+ fi
+
+ if [[ ! -f "$package" ]]; then
+ echo "Expected package does not exist: ${package}" >&2
exit 1
fi
- dotnet nuget push "$package" \
+ echo "version=${version}" >> "$GITHUB_OUTPUT"
+ echo "package=${package}" >> "$GITHUB_OUTPUT"
+
+ - name: publish NuGet
+ id: publish
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master'
+ env:
+ NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
+ PACKAGE: ${{ steps.package.outputs.package }}
+ run: |
+ dotnet nuget push "$PACKAGE" \
--api-key "$NUGET_API_KEY" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate
- if git ls-remote --exit-code --tags origin "refs/tags/${tag}" >/dev/null 2>&1; then
- echo "Tag ${tag} already exists; no tag update needed."
- else
- git tag "$tag"
- git push origin "$tag"
+ echo "succeeded=true" >> "$GITHUB_OUTPUT"
+
+ tag-release:
+ needs: build
+ if: github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.build.result == 'success' && needs.build.outputs.publication_succeeded == 'true'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ steps:
+ - name: checkout published commit
+ uses: actions/checkout@v5
+ with:
+ ref: ${{ github.sha }}
+
+ - name: verify or create release tag
+ shell: bash
+ run: |
+ expected_commit="$GITHUB_SHA"
+ version=$(sed -n 's:.*\(.*\).*:\1:p' src/solrevdev.seedfolder.csproj)
+ tag="v${version}"
+
+ if [[ -z "$version" ]]; then
+ echo "Unable to find the package version." >&2
+ exit 1
fi
+
+ if [[ "$(git rev-parse HEAD)" != "$expected_commit" ]]; then
+ echo "Checked-out commit does not match the intended release commit." >&2
+ exit 1
+ fi
+
+ if git ls-remote --exit-code --refs origin "refs/tags/${tag}" >/dev/null 2>&1; then
+ git fetch --no-tags origin "refs/tags/${tag}:refs/tags/${tag}"
+ tag_commit=$(git rev-list -n 1 "refs/tags/${tag}")
+ tag_version=$(git show "refs/tags/${tag}:src/solrevdev.seedfolder.csproj" | sed -n 's:.*\(.*\).*:\1:p')
+
+ if [[ "$tag_commit" != "$expected_commit" ]]; then
+ echo "Tag ${tag} resolves to ${tag_commit}, expected ${expected_commit}." >&2
+ exit 1
+ fi
+
+ if [[ "$tag_version" != "$version" ]]; then
+ echo "Tag ${tag} contains version ${tag_version}, expected ${version}." >&2
+ exit 1
+ fi
+
+ echo "Tag ${tag} already identifies the published commit and version; safe retry."
+ exit 0
+ fi
+
+ git tag "$tag" "$expected_commit"
+ git push origin "refs/tags/${tag}:refs/tags/${tag}"
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..4cf087d
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,54 @@
+# Repository Agent Guide
+
+This file documents the project-specific guidance for coding agents working in this repository.
+
+## Project overview
+
+`seedfolder` is a .NET Global Tool that creates project directories from embedded templates. The application is a single-file console program in `src/Program.cs` and targets `net8.0`, `net9.0`, and `net10.0`.
+
+## Required validation
+
+Run the same Release-mode sequence used by CI:
+
+```bash
+dotnet build solrevdev.seedfolder.sln --configuration Release
+./tests/integration-test.sh
+dotnet pack solrevdev.seedfolder.sln -c Release --no-build --include-source --include-symbols
+```
+
+Packages are written to `src/nupkg/`. For interactive local install/uninstall testing, see `build/test-local.sh` and review its global-tool side effects before running it.
+
+Run the tool directly with:
+
+```bash
+dotnet run --project src/solrevdev.seedfolder.csproj
+dotnet run --project src/solrevdev.seedfolder.csproj -- myfolder
+dotnet run --project src/solrevdev.seedfolder.csproj -- --dry-run -t node myfolder
+```
+
+## Architecture
+
+- `src/Program.cs` contains CLI parsing, interactive prompts, validation, rendering, and template extraction.
+- `McMaster.Extensions.CommandLineUtils` supplies prompt helpers; option parsing is implemented in `Main()` and accepts options before or after the single folder-name argument.
+- `Figgle` and `Figgle.Fonts` render the ASCII header; colored output uses `Console.ForegroundColor` through the local `WriteLine()` helper.
+- `src/Data/` files are embedded by `src/solrevdev.seedfolder.csproj` and extracted with `WriteFileAsync()`.
+- `TemplateFile` and `ProjectType` define the six templates: markdown, dotnet, node, python, ruby, and universal.
+- Folder names pass through `RemoveSpaces()` and `SafeNameForFileSystem()` before use.
+
+## Release model
+
+- `` in `src/solrevdev.seedfolder.csproj` is the release source of truth and is bumped manually.
+- Pull requests and `release/*` pushes build, test, pack, and verify the exact versioned package; only a push to `master` publishes to NuGet.
+- A publish-capable change must use a version that is not already associated with another commit. After NuGet publication succeeds, CI creates the lightweight `v` tag for that exact master commit.
+- Release retries are accepted only when the existing tag resolves to the same commit and its csproj contains the same version.
+- Keep `actions/checkout@v5` and `actions/setup-dotnet@v5` unless a deliberate, validated action-major upgrade is required; both use the Node 24 action runtime.
+- NuGet publication uses native `dotnet nuget push --skip-duplicate` and requires the `NUGET_API_KEY` Actions secret.
+- Push commits containing `***NO_CI***`, `[ci skip]`, or `[skip ci]` skip the build job. Pull-request validation is not bypassed by these markers.
+
+## Commit messages
+
+Use Conventional Commits with an imperative subject, for example `ci: harden release workflow`. Add a scope when useful and mark breaking changes with `!` and/or a `BREAKING CHANGE:` footer.
+
+## Global versus repository guidance
+
+Keep machine-specific operational rules—Python environment management, browser tooling, SVG rasterization, safe deletion utilities, and Homebrew/.NET SDK repair—in global agent settings. They are useful defaults but are not requirements of this .NET project.
diff --git a/CLAUDE.md b/CLAUDE.md
index 08a8e29..4cf087d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,62 +1,54 @@
-# CLAUDE.md
+# Repository Agent Guide
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+This file documents the project-specific guidance for coding agents working in this repository.
-## Project Overview
+## Project overview
-This is a .NET Core Global Tool called `seedfolder` that creates directories and populates them with standard dotfiles. The tool is designed to quickly scaffold project folders with common configuration files.
+`seedfolder` is a .NET Global Tool that creates project directories from embedded templates. The application is a single-file console program in `src/Program.cs` and targets `net8.0`, `net9.0`, and `net10.0`.
-## Development Commands
+## Required validation
+
+Run the same Release-mode sequence used by CI:
-### Build and Test
```bash
-# Build the solution
dotnet build solrevdev.seedfolder.sln --configuration Release
+./tests/integration-test.sh
+dotnet pack solrevdev.seedfolder.sln -c Release --no-build --include-source --include-symbols
+```
-# Pack for local installation
-dotnet pack
-
-# Install locally for testing
-dotnet tool install --global --add-source ./nupkg solrevdev.seedfolder
+Packages are written to `src/nupkg/`. For interactive local install/uninstall testing, see `build/test-local.sh` and review its global-tool side effects before running it.
-# Uninstall after testing
-dotnet tool uninstall -g solrevdev.seedfolder
-```
+Run the tool directly with:
-### Run the Tool During Development
```bash
-# Run with interactive prompts
dotnet run --project src/solrevdev.seedfolder.csproj
-
-# Run with folder name argument
-dotnet run --project src/solrevdev.seedfolder.csproj myfolder
+dotnet run --project src/solrevdev.seedfolder.csproj -- myfolder
+dotnet run --project src/solrevdev.seedfolder.csproj -- --dry-run -t node myfolder
```
## Architecture
-### Single File Structure
-The entire application is contained in `src/Program.cs` - a single-file console application using:
-- **McMaster.Extensions.CommandLineUtils** for command-line parsing and prompts
-- **Figgle** / **Figgle.Fonts** for ASCII art header generation
-- **Console.ForegroundColor** for colored console output
-
-### Embedded Resources
-Template files are stored as embedded resources in `src/Data/` and copied to new folders:
-- `dockerignore` → `.dockerignore`
-- `editorconfig` → `.editorconfig`
-- `gitattributes` → `.gitattributes`
-- `gitignore` → `.gitignore`
-- `prettierignore` → `.prettierignore`
-- `prettierrc` → `.prettierrc`
-- `omnisharp.json` → `omnisharp.json`
-
-### Key Methods
-- `WriteFileAsync()` - Extracts embedded resources and writes them to the target directory
-- `RemoveSpaces()` - Sanitizes folder names by replacing spaces with dashes
-- `SafeNameForFileSystem()` - Removes invalid filesystem characters from folder names
-
-## Multi-Target Framework Support
-The project targets .NET 8.0 (LTS), 9.0 (STS), and 10.0 (LTS) to support current and modern .NET versions. .NET 8 provides long-term support until November 2026, .NET 9 offers the latest features with 18-month support, and .NET 10 is the latest LTS release providing long-term support until November 2028.
-
-## CI/CD
-GitHub Actions workflow builds, packs, and publishes to NuGet on pushes to master branch. Version is controlled by the `` property in the .csproj file.
\ No newline at end of file
+- `src/Program.cs` contains CLI parsing, interactive prompts, validation, rendering, and template extraction.
+- `McMaster.Extensions.CommandLineUtils` supplies prompt helpers; option parsing is implemented in `Main()` and accepts options before or after the single folder-name argument.
+- `Figgle` and `Figgle.Fonts` render the ASCII header; colored output uses `Console.ForegroundColor` through the local `WriteLine()` helper.
+- `src/Data/` files are embedded by `src/solrevdev.seedfolder.csproj` and extracted with `WriteFileAsync()`.
+- `TemplateFile` and `ProjectType` define the six templates: markdown, dotnet, node, python, ruby, and universal.
+- Folder names pass through `RemoveSpaces()` and `SafeNameForFileSystem()` before use.
+
+## Release model
+
+- `` in `src/solrevdev.seedfolder.csproj` is the release source of truth and is bumped manually.
+- Pull requests and `release/*` pushes build, test, pack, and verify the exact versioned package; only a push to `master` publishes to NuGet.
+- A publish-capable change must use a version that is not already associated with another commit. After NuGet publication succeeds, CI creates the lightweight `v` tag for that exact master commit.
+- Release retries are accepted only when the existing tag resolves to the same commit and its csproj contains the same version.
+- Keep `actions/checkout@v5` and `actions/setup-dotnet@v5` unless a deliberate, validated action-major upgrade is required; both use the Node 24 action runtime.
+- NuGet publication uses native `dotnet nuget push --skip-duplicate` and requires the `NUGET_API_KEY` Actions secret.
+- Push commits containing `***NO_CI***`, `[ci skip]`, or `[skip ci]` skip the build job. Pull-request validation is not bypassed by these markers.
+
+## Commit messages
+
+Use Conventional Commits with an imperative subject, for example `ci: harden release workflow`. Add a scope when useful and mark breaking changes with `!` and/or a `BREAKING CHANGE:` footer.
+
+## Global versus repository guidance
+
+Keep machine-specific operational rules—Python environment management, browser tooling, SVG rasterization, safe deletion utilities, and Homebrew/.NET SDK repair—in global agent settings. They are useful defaults but are not requirements of this .NET project.
diff --git a/src/solrevdev.seedfolder.csproj b/src/solrevdev.seedfolder.csproj
index fa1f950..19714e2 100644
--- a/src/solrevdev.seedfolder.csproj
+++ b/src/solrevdev.seedfolder.csproj
@@ -9,7 +9,7 @@
seedfolder
./nupkg
true
- 1.4.1
+ 1.4.2
solrevdev.seedfolder
.NET Core Global Tool that creates a folder and copies dotfiles into it therefore seeding a folder.
.NET Core Global Tool that creates a folder and copies dotfiles into it therefore seeding a folder.