diff --git a/.github/workflows/NUGET_RELEASE.md b/.github/workflows/NUGET_RELEASE.md
new file mode 100644
index 00000000..42375089
--- /dev/null
+++ b/.github/workflows/NUGET_RELEASE.md
@@ -0,0 +1,155 @@
+# NuGet Release Workflow
+
+This document describes the automated NuGet release workflow for SharpCompress.
+
+## Overview
+
+The `nuget-release.yml` workflow automatically builds, tests, and publishes SharpCompress packages to NuGet.org when:
+- Changes are pushed to the `master` or `release` branch
+- A version tag (format: `MAJOR.MINOR.PATCH`) is pushed
+
+The workflow runs on both Windows and Ubuntu, but only the Windows build publishes to NuGet.
+
+## How It Works
+
+### Version Determination
+
+The workflow automatically determines the version based on whether the commit is tagged using C# code in the build project:
+
+1. **Tagged Release (Stable)**:
+ - If the current commit has a version tag (e.g., `0.42.1`)
+ - Uses the tag as the version number
+ - Published as a stable release
+
+2. **Untagged Release (Prerelease)**:
+ - If the current commit is NOT tagged
+ - Creates a prerelease version based on the next minor version
+ - Format: `{NEXT_MINOR_VERSION}-beta.{COMMIT_COUNT}`
+ - Example: `0.43.0-beta.123` (if last tag is 0.42.x)
+ - Published as a prerelease to NuGet.org (Windows build only)
+
+### Workflow Steps
+
+The workflow runs on a matrix of operating systems (Windows and Ubuntu):
+
+1. **Checkout**: Fetches the repository with full history for version detection
+2. **Setup .NET**: Installs .NET 10.0
+3. **Determine Version**: Runs `determine-version` build target to check for tags and determine version
+4. **Update Version**: Runs `update-version` build target to update the version in the project file
+5. **Build and Test**: Runs the full build and test suite on both platforms
+6. **Upload Artifacts**: Uploads the generated `.nupkg` files as workflow artifacts (separate for each OS)
+7. **Push to NuGet**: (Windows only) Runs `push-to-nuget` build target to publish the package to NuGet.org using the API key
+
+All version detection, file updates, and publishing logic is implemented in C# in the `build/Program.cs` file using build targets.
+
+## Setup Requirements
+
+### 1. NuGet API Key Secret
+
+The workflow requires a `NUGET_API_KEY` secret to be configured in the repository settings:
+
+1. Go to https://www.nuget.org/account/apikeys
+2. Create a new API key with "Push" permission for the SharpCompress package
+3. In GitHub, go to: **Settings** → **Secrets and variables** → **Actions**
+4. Create a new secret named `NUGET_API_KEY` with the API key value
+
+### 2. Branch Protection (Recommended)
+
+Consider enabling branch protection rules for the `release` branch to ensure:
+- Code reviews are required before merging
+- Status checks pass before merging
+- Only authorized users can push to the branch
+
+## Usage
+
+### Creating a Stable Release
+
+There are two ways to trigger a stable release:
+
+**Method 1: Push tag to trigger workflow**
+1. Ensure all changes are committed on the `master` or `release` branch
+2. Create and push a version tag:
+ ```bash
+ git checkout master # or release
+ git tag 0.43.0
+ git push origin 0.43.0
+ ```
+3. The workflow will automatically trigger, build, test, and publish `SharpCompress 0.43.0` to NuGet.org (Windows build)
+
+**Method 2: Tag after pushing to branch**
+1. Ensure all changes are merged and pushed to the `master` or `release` branch
+2. Create and push a version tag on the already-pushed commit:
+ ```bash
+ git checkout master # or release
+ git tag 0.43.0
+ git push origin 0.43.0
+ ```
+3. The workflow will automatically trigger, build, test, and publish `SharpCompress 0.43.0` to NuGet.org (Windows build)
+
+### Creating a Prerelease
+
+1. Push changes to the `master` or `release` branch without tagging:
+ ```bash
+ git checkout master # or release
+ git push origin master # or release
+ ```
+2. The workflow will automatically:
+ - Build and test the project on both Windows and Ubuntu
+ - Publish a prerelease version like `0.43.0-beta.456` to NuGet.org (Windows build)
+
+## Troubleshooting
+
+### Workflow Fails to Push to NuGet
+
+- **Check the API Key**: Ensure `NUGET_API_KEY` is set correctly in repository secrets
+- **Check API Key Permissions**: Verify the API key has "Push" permission for SharpCompress
+- **Check API Key Expiration**: NuGet API keys may expire; create a new one if needed
+
+### Version Conflict
+
+If you see "Package already exists" errors:
+- The workflow uses `--skip-duplicate` flag to handle this gracefully
+- If you need to republish the same version, delete it from NuGet.org first (if allowed)
+
+### Build or Test Failures
+
+- The workflow will not push to NuGet if build or tests fail
+- Check the workflow logs in GitHub Actions for details
+- Fix the issues and push again
+
+## Manual Package Creation
+
+If you need to create a package manually without publishing:
+
+```bash
+dotnet run --project build/build.csproj -- publish
+```
+
+The package will be created in the `artifacts/` directory.
+
+## Build Targets
+
+The workflow uses the following C# build targets defined in `build/Program.cs`:
+
+- **determine-version**: Detects version from git tags and outputs VERSION and PRERELEASE variables
+- **update-version**: Updates VersionPrefix, AssemblyVersion, and FileVersion in the project file
+- **push-to-nuget**: Pushes the generated NuGet packages to NuGet.org (requires NUGET_API_KEY)
+
+These targets can be run manually for testing:
+
+```bash
+# Determine the version
+dotnet run --project build/build.csproj -- determine-version
+
+# Update version in project file
+VERSION=0.43.0 dotnet run --project build/build.csproj -- update-version
+
+# Push to NuGet (requires NUGET_API_KEY environment variable)
+NUGET_API_KEY=your-key dotnet run --project build/build.csproj -- push-to-nuget
+```
+
+## Related Files
+
+- `.github/workflows/nuget-release.yml` - The workflow definition
+- `build/Program.cs` - Build script with version detection and publishing logic
+- `src/SharpCompress/SharpCompress.csproj` - Project file with version information
diff --git a/.github/workflows/TESTING.md b/.github/workflows/TESTING.md
new file mode 100644
index 00000000..6afaa8aa
--- /dev/null
+++ b/.github/workflows/TESTING.md
@@ -0,0 +1,120 @@
+# Testing Guide for NuGet Release Workflow
+
+This document describes how to test the NuGet release workflow.
+
+## Testing Strategy
+
+Since this workflow publishes to NuGet.org and requires repository secrets, testing should be done carefully. The workflow runs on both Windows and Ubuntu, but only the Windows build publishes to NuGet.
+
+## Pre-Testing Checklist
+
+- [x] Workflow YAML syntax validated
+- [x] Version determination logic tested locally
+- [x] Version update logic tested locally
+- [x] Build script works (`dotnet run --project build/build.csproj`)
+
+## Manual Testing Steps
+
+### 1. Test Prerelease Publishing (Recommended First Test)
+
+This tests the workflow on untagged commits to the master or release branch.
+
+**Steps:**
+1. Ensure `NUGET_API_KEY` secret is configured in repository settings
+2. Create a test commit on the `master` or `release` branch (e.g., update a comment or README)
+3. Push to the `master` or `release` branch
+4. Monitor the GitHub Actions workflow at: https://github.com/adamhathcock/sharpcompress/actions
+5. Verify:
+ - Workflow triggers and runs successfully on both Windows and Ubuntu
+ - Version is determined correctly (e.g., `0.43.0-beta.XXX` if last tag is 0.42.x)
+ - Build and tests pass on both platforms
+ - Package artifacts are uploaded for both platforms
+ - Package is pushed to NuGet.org as prerelease (Windows build only)
+
+**Expected Outcome:**
+- A new prerelease package appears on NuGet.org: https://www.nuget.org/packages/SharpCompress/
+- Package version follows pattern: `{NEXT_MINOR_VERSION}-beta.{COMMIT_COUNT}`
+
+### 2. Test Tagged Release Publishing
+
+This tests the workflow when a version tag is pushed.
+
+**Steps:**
+1. Prepare the `master` or `release` branch with all desired changes
+2. Create a version tag (must be a pure semantic version like `MAJOR.MINOR.PATCH`):
+ ```bash
+ git checkout master # or release
+ git tag 0.42.2
+ git push origin 0.42.2
+ ```
+3. Monitor the GitHub Actions workflow
+4. Verify:
+ - Workflow triggers and runs successfully on both Windows and Ubuntu
+ - Version is determined as the tag (e.g., `0.42.2`)
+ - Build and tests pass on both platforms
+ - Package artifacts are uploaded for both platforms
+ - Package is pushed to NuGet.org as stable release (Windows build only)
+
+**Expected Outcome:**
+- A new stable release package appears on NuGet.org
+- Package version matches the tag
+
+### 3. Test Duplicate Package Handling
+
+This tests the `--skip-duplicate` flag behavior.
+
+**Steps:**
+1. Push to the `release` branch without making changes
+2. Monitor the workflow
+3. Verify:
+ - Workflow runs but NuGet push is skipped with "duplicate" message
+ - No errors occur
+
+### 4. Test Build Failure Handling
+
+This tests that failed builds don't publish packages.
+
+**Steps:**
+1. Introduce a breaking change in a test or code
+2. Push to the `release` branch
+3. Verify:
+ - Workflow runs and detects the failure
+ - Build or test step fails
+ - NuGet push step is skipped
+ - No package is published
+
+## Verification
+
+After each test, verify:
+
+1. **GitHub Actions Logs**: Check the workflow logs for any errors or warnings
+2. **NuGet.org**: Verify the package appears with correct version and metadata
+3. **Artifacts**: Download and inspect the uploaded artifacts
+
+## Rollback/Cleanup
+
+If testing produces unwanted packages:
+
+1. **Prerelease packages**: Can be unlisted on NuGet.org (Settings → Unlist)
+2. **Stable packages**: Cannot be deleted, only unlisted (use test versions)
+3. **Tags**: Can be deleted with:
+ ```bash
+ git tag -d 0.42.2
+ git push origin :refs/tags/0.42.2
+ ```
+
+## Known Limitations
+
+- NuGet.org does not allow re-uploading the same version
+- Deleted packages on NuGet.org reserve the version number
+- The workflow requires the `NUGET_API_KEY` secret to be set
+
+## Success Criteria
+
+The workflow is considered successful if:
+
+- ✅ Prerelease versions are published correctly with beta suffix
+- ✅ Tagged versions are published as stable releases
+- ✅ Build and test failures prevent publishing
+- ✅ Duplicate packages are handled gracefully
+- ✅ Workflow logs are clear and informative
diff --git a/.github/workflows/nuget-release.yml b/.github/workflows/nuget-release.yml
new file mode 100644
index 00000000..fe7d404a
--- /dev/null
+++ b/.github/workflows/nuget-release.yml
@@ -0,0 +1,57 @@
+name: NuGet Release
+
+on:
+ push:
+ branches:
+ - 'master'
+ - 'release'
+ tags:
+ - '[0-9]+.[0-9]+.[0-9]+'
+
+permissions:
+ contents: read
+
+jobs:
+ build-and-publish:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [windows-latest, ubuntu-latest]
+
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0 # Fetch all history for versioning
+
+ - uses: actions/setup-dotnet@v5
+ with:
+ dotnet-version: 10.0.x
+
+ # Determine version using C# build target
+ - name: Determine Version
+ id: version
+ run: dotnet run --project build/build.csproj -- determine-version
+
+ # Update version in project file using C# build target
+ - name: Update Version in Project
+ run: dotnet run --project build/build.csproj -- update-version
+ env:
+ VERSION: ${{ steps.version.outputs.version }}
+
+ # Build and test
+ - name: Build and Test
+ run: dotnet run --project build/build.csproj
+
+ # Upload artifacts for verification
+ - name: Upload NuGet Package
+ uses: actions/upload-artifact@v6
+ with:
+ name: ${{ matrix.os }}-nuget-package
+ path: artifacts/*.nupkg
+
+ # Push to NuGet.org using C# build target (Windows only)
+ - name: Push to NuGet
+ if: success() && matrix.os == 'windows-latest'
+ run: dotnet run --project build/build.csproj -- push-to-nuget
+ env:
+ NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
diff --git a/.gitignore b/.gitignore
index 42a5e999..6c6a863d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@ tests/TestArchives/*/Scratch2
.vs
tools
.idea/
+artifacts/
.DS_Store
*.snupkg
diff --git a/build/Program.cs b/build/Program.cs
index 92613d11..47a86745 100644
--- a/build/Program.cs
+++ b/build/Program.cs
@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
using GlobExpressions;
using static Bullseye.Targets;
using static SimpleExec.Command;
@@ -13,6 +16,9 @@ const string Test = "test";
const string Format = "format";
const string CheckFormat = "check-format";
const string Publish = "publish";
+const string DetermineVersion = "determine-version";
+const string UpdateVersion = "update-version";
+const string PushToNuGet = "push-to-nuget";
Target(
Clean,
@@ -99,6 +105,164 @@ Target(
}
);
+Target(
+ DetermineVersion,
+ async () =>
+ {
+ var (version, isPrerelease) = await GetVersion();
+ Console.WriteLine($"VERSION={version}");
+ Console.WriteLine($"PRERELEASE={isPrerelease.ToString().ToLower()}");
+
+ // Write to environment file for GitHub Actions
+ var githubOutput = Environment.GetEnvironmentVariable("GITHUB_OUTPUT");
+ if (!string.IsNullOrEmpty(githubOutput))
+ {
+ File.AppendAllText(githubOutput, $"version={version}\n");
+ File.AppendAllText(githubOutput, $"prerelease={isPrerelease.ToString().ToLower()}\n");
+ }
+ }
+);
+
+Target(
+ UpdateVersion,
+ async () =>
+ {
+ var version = Environment.GetEnvironmentVariable("VERSION");
+ if (string.IsNullOrEmpty(version))
+ {
+ var (detectedVersion, _) = await GetVersion();
+ version = detectedVersion;
+ }
+
+ Console.WriteLine($"Updating project file with version: {version}");
+
+ var projectPath = "src/SharpCompress/SharpCompress.csproj";
+ var content = File.ReadAllText(projectPath);
+
+ // Get base version (without prerelease suffix)
+ var baseVersion = version.Split('-')[0];
+
+ // Update VersionPrefix
+ content = Regex.Replace(
+ content,
+ @"[^<]*",
+ $"{version}"
+ );
+
+ // Update AssemblyVersion
+ content = Regex.Replace(
+ content,
+ @"[^<]*",
+ $"{baseVersion}"
+ );
+
+ // Update FileVersion
+ content = Regex.Replace(
+ content,
+ @"[^<]*",
+ $"{baseVersion}"
+ );
+
+ File.WriteAllText(projectPath, content);
+ Console.WriteLine($"Updated VersionPrefix to: {version}");
+ Console.WriteLine($"Updated AssemblyVersion and FileVersion to: {baseVersion}");
+ }
+);
+
+Target(
+ PushToNuGet,
+ () =>
+ {
+ var apiKey = Environment.GetEnvironmentVariable("NUGET_API_KEY");
+ if (string.IsNullOrEmpty(apiKey))
+ {
+ Console.WriteLine(
+ "NUGET_API_KEY environment variable is not set. Skipping NuGet push."
+ );
+ return;
+ }
+
+ var packages = Directory.GetFiles("artifacts", "*.nupkg");
+ if (packages.Length == 0)
+ {
+ Console.WriteLine("No packages found in artifacts directory.");
+ return;
+ }
+
+ foreach (var package in packages)
+ {
+ Console.WriteLine($"Pushing {package} to NuGet.org");
+ try
+ {
+ // Note: API key is passed via command line argument which is standard practice for dotnet nuget push
+ // The key is already in an environment variable and not displayed in normal output
+ Run(
+ "dotnet",
+ $"nuget push \"{package}\" --api-key {apiKey} --source https://api.nuget.org/v3/index.json --skip-duplicate"
+ );
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Failed to push {package}: {ex.Message}");
+ throw;
+ }
+ }
+ }
+);
+
Target("default", [Publish], () => Console.WriteLine("Done!"));
await RunTargetsAndExitAsync(args);
+
+static async Task<(string version, bool isPrerelease)> GetVersion()
+{
+ // Check if current commit has a version tag
+ var currentTag = (await GetGitOutput("tag", "--points-at HEAD"))
+ .Split('\n', StringSplitOptions.RemoveEmptyEntries)
+ .FirstOrDefault(tag => Regex.IsMatch(tag.Trim(), @"^\d+\.\d+\.\d+$"));
+
+ if (!string.IsNullOrEmpty(currentTag))
+ {
+ // Tagged release - use the tag as version
+ var version = currentTag.Trim();
+ Console.WriteLine($"Building tagged release version: {version}");
+ return (version, false);
+ }
+ else
+ {
+ // Not tagged - create prerelease version based on next minor version
+ var allTags = (await GetGitOutput("tag", "--list"))
+ .Split('\n', StringSplitOptions.RemoveEmptyEntries)
+ .Where(tag => Regex.IsMatch(tag.Trim(), @"^\d+\.\d+\.\d+$"))
+ .Select(tag => tag.Trim())
+ .ToList();
+
+ var lastTag = allTags.OrderBy(tag => Version.Parse(tag)).LastOrDefault() ?? "0.0.0";
+ var lastVersion = Version.Parse(lastTag);
+
+ // Increment minor version for next release
+ var nextVersion = new Version(lastVersion.Major, lastVersion.Minor + 1, 0);
+
+ // Use commit count since the last version tag if available; otherwise, fall back to total count
+ var revListArgs = allTags.Any() ? $"--count {lastTag}..HEAD" : "--count HEAD";
+ var commitCount = (await GetGitOutput("rev-list", revListArgs)).Trim();
+
+ var version = $"{nextVersion}-beta.{commitCount}";
+ Console.WriteLine($"Building prerelease version: {version}");
+ return (version, true);
+ }
+}
+
+static async Task GetGitOutput(string command, string args)
+{
+ try
+ {
+ // Use SimpleExec's Read to execute git commands in a cross-platform way
+ var (output, _) = await ReadAsync("git", $"{command} {args}");
+ return output;
+ }
+ catch (Exception ex)
+ {
+ throw new Exception($"Git command failed: git {command} {args}\n{ex.Message}", ex);
+ }
+}