PRD: Deployment Architecture Refactor
PRD: Morphir .NET Deployment Architecture Refactor
Executive Summary
Refactor the morphir-dotnet deployment architecture to fix critical packaging issues, separate tool distribution from executable distribution, implement comprehensive build testing, and establish changelog-driven versioning as the single source of truth.
Problem: The current deployment failed due to package naming mismatches (lowercase “morphir” vs “Morphir”), inconsistent tool command naming, and lack of automated testing to catch these issues before CI deployment.
Solution: Separate concerns into distinct projects (Morphir.Tool for dotnet tool, Morphir for executables), reorganize build system following vertical slice architecture, implement Ionide.KeepAChangelog for version management, and add comprehensive build testing infrastructure.
Impact: Eliminates deployment failures, provides clear distribution strategy for different user personas, enables confident releases with automated validation, and establishes maintainable build architecture.
Table of Contents
- Background
- Problem Statement
- Goals & Non-Goals
- User Personas
- Design Decisions
- Architecture
- Implementation Plan
- BDD Acceptance Criteria
- Testing Strategy
- Risks & Mitigation
- Success Metrics
- Timeline
- References
Background
Current State
The morphir-dotnet project currently:
- Uses a single
Morphirproject for both tool and executable - Has AssemblyName “morphir” (lowercase) causing glob pattern mismatches
- Sets version via
RELEASE_VERSIONenvironment variable - Has no automated tests for packaging or deployment
- Suffers from configuration inconsistency (tool command as “morphir” vs “dotnet-morphir”)
Recent Failure
Deployment to main (run #20330271677) failed with:
System.Exception: Morphir tool package not found in /artifacts/packages
at Build.<get_PublishTool>b__71_1() in Build.cs:line 462
Root cause: Build.cs searches for Morphir.*.nupkg (capital M) but package is named morphir.*.nupkg (lowercase m) due to AssemblyName mismatch.
Research Conducted
Analyzed industry patterns including:
- Nuke build system’s own packaging strategy
- Other .NET CLI tools (dotnet-format, dotnet-ef, GitVersion)
- Ionide.KeepAChangelog for changelog-driven versioning
- TestContainers for local NuGet server testing
- Keep a Changelog specification for pre-release versioning
Problem Statement
Critical Issues
Package Naming Mismatch ⚠️ BLOCKER
- Build.cs expects
Morphir.*.nupkg - Actual package:
morphir.*.nupkg - Deployment fails at PublishTool step
- Build.cs expects
Tool Command Inconsistency
- Build.cs:
ToolCommandName=morphir - Deprecated scripts:
ToolCommandName=dotnet-morphir - Install scripts reference inconsistent command names
- Build.cs:
No Build Testing
- No validation of package structure
- No test of tool installation
- Issues only discovered in CI deployment
- Manual verification required
Architectural Confusion
- Single project serves both tool and executable
- Mixed concerns (dotnet tool + AOT compilation)
- Difficult to optimize for each use case
- Complex build configuration
Version Management Fragility
- Manual
RELEASE_VERSIONin workflow file - No validation or enforcement
- Risk of version drift between packages
- CHANGELOG.md not connected to versions
- Manual
User Impact
Before fix:
- Users confused about tool command name
- Documentation doesn’t match reality
- Deployment failures block releases
- Manual verification slows development
After fix:
- Clear persona-based installation paths
- Automated validation prevents failures
- Confident, fast releases
- Maintainable architecture
Goals & Non-Goals
Goals
✅ Fix immediate deployment failure
- Resolve package naming mismatch
- Successful deployment to NuGet.org and GitHub Releases
✅ Separate concerns
- Distinct
Morphir.Toolproject for dotnet tool Morphirproject for standalone executables- Clear boundaries and responsibilities
✅ Implement comprehensive testing
- Package structure validation
- Metadata correctness verification
- Local installation smoke tests
- Catch issues before CI deployment
✅ Establish changelog-driven versioning
- CHANGELOG.md as single source of truth
- Ionide.KeepAChangelog integration
- Support pre-release versions (alpha, beta, rc)
- Automated release preparation
✅ Dual distribution strategy
- NuGet tool package for .NET developers
- GitHub releases with executables for non-SDK users
- Persona-based documentation
✅ Organize build system
- Split Build.cs by domain (vertical slices)
- Extract helper classes for testability
- Align with Morphir.Tooling architecture
- Maintainable and scalable structure
Non-Goals
❌ Automated pre-release version bumping (Phase 2, future work) ❌ TestContainers integration (Phase 3 of testing, when needed) ❌ Package rename/migration (Keeping current names for backward compatibility) ❌ Breaking changes to public APIs (Maintain compatibility)
User Personas
Persona 1: .NET Developer
Profile:
- Has .NET SDK installed (development machine)
- Uses dotnet CLI regularly
- Works with Morphir in .NET projects
- Expects standard dotnet tooling experience
Needs:
dotnet tool install -g Morphir.Tool- Automatic updates via
dotnet tool update - Integration with IDEs and build tools
- Familiar dotnet conventions
Distribution: NuGet.org package
Command: morphir (after tool install)
Persona 2: Shell Script / Container User
Profile:
- Minimal environment (Alpine Linux, slim containers)
- Cannot install .NET SDK (size constraints)
- Uses Morphir as CLI utility in scripts
- Needs fast startup, small binary
Needs:
- Standalone executable (no SDK required)
- AOT-compiled for fast startup
- Small binary size (trimmed)
- Install via curl/wget script
Distribution: GitHub Releases with platform-specific executables
Command: morphir (or ./morphir-linux-x64)
Persona 3: CI/CD Pipeline
Profile:
- GitHub Actions, GitLab CI, Jenkins
- May or may not have .NET SDK pre-installed
- Speed and caching are priorities
- Reliability is critical
Needs:
- Flexible installation (either method works)
- Fast downloads and caching
- Consistent behavior across runs
- Clear error messages
Distribution: Either NuGet tool or GitHub release executable
Command: morphir
Design Decisions
All design decisions were made interactively with stakeholders. See Design Decision Rationale for full context.
Decision 1: Project Structure
Decision: Separate projects (Option B)
Create new src/Morphir.Tool/ project for dotnet tool, keep src/Morphir/ for standalone executable.
Rationale:
- Clear separation of concerns
- Industry pattern (matches Nuke, GitVersion)
- Easier to test independently
- Optimized for each use case
Alternatives considered:
- Keep single project with renamed package (doesn’t fix architecture)
- Keep current, fix naming only (addresses symptom, not cause)
Decision 2: Testing Strategy
Decision: Hybrid approach (Option C)
- Phase 1: Package structure + metadata validation (no Docker)
- Phase 2: Local folder smoke tests
- Phase 3: TestContainers + BaGet (future, when needed)
Rationale:
- Pragmatic - start simple, add complexity when needed
- Fast feedback loop (no Docker startup)
- Covers 80% of issues immediately
- Extensible for future enhancements
Alternatives considered:
- Folder-based only (insufficient validation)
- Full TestContainers immediately (overkill, slower)
Decision 3: Build Organization
Decision: Split by domain + extract helpers (Option B + D hybrid)
Split Build.cs into:
Build.cs- Entry point, core configurationBuild.Packaging.cs- Pack targetsBuild.Publishing.cs- Publish targetsBuild.Testing.cs- Test targetsHelpers/- PackageValidator, ChangelogHelper, etc.
Rationale:
- Aligns with vertical slice architecture (matches Morphir.Tooling)
- Clear feature boundaries
- Testable helper classes
- Scales well as features grow
Alternatives considered:
- Keep single file (will become unwieldy)
- Split by technical concern (doesn’t match domain boundaries)
Decision 4: Distribution Strategy
Decision: Dual distribution (Option B)
- NuGet.org:
Morphir.Toolpackage (dotnet tool) - GitHub Releases: Platform executables (linux-x64, win-x64, osx-arm64, etc.)
Rationale:
- Serves all user personas
- Industry standard pattern
- Optimal for each use case
- Flexible deployment options
Alternatives considered:
- Tool package only (excludes non-SDK users)
- Executable only (not idiomatic for .NET developers)
Decision 5: Version Management
Decision: CHANGELOG.md as single source of truth via Ionide.KeepAChangelog (Option D)
- Use Ionide.KeepAChangelog to extract version from CHANGELOG.md
- Support pre-release versions (alpha, beta, rc, preview)
- PrepareRelease target automates [Unreleased] → [X.Y.Z] promotion
- Auto-bump pre-release based on prior pre-release type
- Git tags use
vprefix (e.g.,v0.2.1)
Rationale:
- Respects Keep a Changelog workflow
- Enforces changelog updates before release
- Supports pre-release versioning fully
- Single source of truth (no version.json needed)
- Automates tedious changelog formatting
Alternatives considered:
- Environment variable only (error-prone, no validation)
- version.json (duplication with CHANGELOG.md)
- GitVersion only (doesn’t fit changelog-driven workflow)
Architecture
Project Structure
morphir-dotnet/
├── src/
│ ├── Morphir/ # Standalone executable (AOT)
│ │ ├── Morphir.csproj
│ │ ├── Program.cs
│ │ └── (Output: morphir-{rid} executables)
│ │
│ ├── Morphir.Tool/ # NEW - Dotnet tool (managed DLLs)
│ │ ├── Morphir.Tool.csproj
│ │ ├── Program.cs # Thin wrapper
│ │ └── (Output: Morphir.Tool.nupkg)
│ │
│ ├── Morphir.Core/ # Core domain
│ └── Morphir.Tooling/ # Tooling services
│
├── tests/
│ ├── Morphir.Build.Tests/ # NEW - Build system tests
│ │ ├── PackageStructureTests.cs
│ │ ├── PackageMetadataTests.cs
│ │ └── LocalInstallationTests.cs
│ ├── Morphir.Core.Tests/
│ ├── Morphir.Tooling.Tests/
│ └── Morphir.E2E.Tests/
│
├── build/
│ ├── Build.cs # Entry point
│ ├── Build.Packaging.cs # NEW - Pack targets
│ ├── Build.Publishing.cs # NEW - Publish targets
│ ├── Build.Testing.cs # NEW - Test targets
│ └── Helpers/ # NEW
│ ├── PackageValidator.cs
│ ├── ChangelogHelper.cs
│ └── PathHelper.cs
│
├── CHANGELOG.md # Single source of truth for versions
└── .github/workflows/
└── deployment.yml # Updated for new architecture
Package Relationships
Morphir.Tool (NuGet package)
└── depends on Morphir.Core
└── depends on Morphir.Tooling
Morphir.Core (NuGet package)
└── standalone library
Morphir.Tooling (NuGet package)
└── depends on Morphir.Core
Morphir executables (GitHub releases)
└── self-contained, no dependencies
└── AOT-compiled, trimmed
Build Flow
┌─────────────────────────────────────────────────────────────┐
│ Developer: Update CHANGELOG.md [Unreleased] │
│ Add changes to feature branch │
└────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Release Prep: ./build.sh PrepareRelease --version 0.2.1 │
│ Moves [Unreleased] → [0.2.1] - YYYY-MM-DD │
│ Creates release/0.2.1 branch │
└────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PR to main: Code review, approval, merge │
└────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Tag push: git tag -a v0.2.1 -m "Release 0.2.1" │
│ git push origin v0.2.1 │
└────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ CI Deployment: │
│ 1. Extract version from CHANGELOG.md (0.2.1) │
│ 2. Run build tests (validate packages) │
│ 3. Build Morphir.Tool.nupkg → NuGet.org │
│ 4. Build executables → GitHub Release v0.2.1 │
│ 5. Upload executables to release │
│ 6. Extract release notes from CHANGELOG.md │
└──────────────────────────────────────────────────────────────┘
Versioning Flow
CHANGELOG.md [Unreleased]
└── Developer adds changes here during development
PrepareRelease --version 0.2.1
└── [Unreleased] → [0.2.1] - 2025-12-20
└── Update comparison links
└── Stage changes (manual commit)
Ionide.KeepAChangelog
└── Parses CHANGELOG.md
└── Extracts latest version: 0.2.1
└── Extracts release notes for PackageReleaseNotes
Nuke Build
└── GetVersionFromChangelog() → SemVersion 0.2.1
└── All Pack targets use this version
└── All packages have same version
Pre-release Versioning
CHANGELOG.md:
## [0.2.1-alpha.1] - 2025-12-18
## [0.2.1-alpha.2] - 2025-12-19 ← Auto-bumped
## [0.2.1-beta.1] - 2025-12-20 ← Explicit release
## [0.2.1-beta.2] - 2025-12-21 ← Auto-bumped
## [0.2.1-rc.1] - 2025-12-22 ← Explicit release
## [0.2.1] - 2025-12-23 ← Final release
Auto-bump logic:
- Detect previous pre-release type (alpha/beta/preview/rc)
- Increment number: alpha.1 → alpha.2
- On new explicit type: beta.1 (resets number)
Implementation Plan
Phase 1: Project Structure & Build Organization (3-4 days)
Goal: Separate projects, reorganize build system
Tasks
1.1 Create Morphir.Tool Project
- Create
src/Morphir.Tool/directory - Create
Morphir.Tool.csproj:<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <OutputType>Exe</OutputType> <PackAsTool>true</PackAsTool> <ToolCommandName>morphir</ToolCommandName> <PackageId>Morphir.Tool</PackageId> <IsPackable>true</IsPackable> </PropertyGroup> <ItemGroup> <ProjectReference Include="../Morphir.Core/Morphir.Core.csproj" /> <ProjectReference Include="../Morphir.Tooling/Morphir.Tooling.csproj" /> </ItemGroup> </Project> - Create minimal
Program.cs:// Delegates to Morphir.Tooling return await Morphir.Tooling.CLI.RunAsync(args); - Add to solution file
1.2 Update Morphir Project
- Ensure
Morphir.csprojhasAssemblyName="morphir"(lowercase) - Verify
IsPackable=false(not published to NuGet) - Ensure AOT and trimming settings remain
- Keep current
Program.csunchanged
1.3 Split Build.cs
- Create
build/Build.Packaging.cs:partial class Build { Target PackLibs => _ => _... Target PackTool => _ => _... Target PackAll => _ => _... } - Create
build/Build.Publishing.cs:partial class Build { Target PublishLibs => _ => _... Target PublishTool => _ => _... Target PublishAll => _ => _... Target PublishLocalLibs => _ => _... Target PublishLocalTool => _ => _... } - Create
build/Build.Testing.cs:partial class Build { Target Test => _ => _... Target TestE2E => _ => _... Target TestBuild => _ => _... // NEW Target TestAll => _ => _... } - Keep
Build.csas main entry with:- Parameters
- Core configuration
- Main targets (Restore, Compile, Clean)
- CI orchestration targets
1.4 Create Helper Classes
- Create
build/Helpers/directory - Create
PackageValidator.cs:public static class PackageValidator { public static void ValidateToolPackage(AbsolutePath packagePath) { } public static void ValidateLibraryPackage(AbsolutePath packagePath) { } } - Create
ChangelogHelper.cs:public static class ChangelogHelper { public static SemVersion GetVersionFromChangelog(AbsolutePath changelogPath) { } public static string GetReleaseNotes(AbsolutePath changelogPath) { } public static void PrepareRelease(AbsolutePath changelogPath, string version) { } } - Create
PathHelper.cs:public static class PathHelper { public static AbsolutePath FindLatestPackage(AbsolutePath directory, string pattern) { } }
1.5 Remove Deprecated Code
- Delete
scripts/pack-tool-platform.cs - Delete
scripts/build-tool-dll.cs - Remove references from documentation
- Update
NUKE_MIGRATION.md
1.6 Update Build Targets
- Fix
PackToolto buildMorphir.Tool.csproj:Target PackTool => _ => _ .DependsOn(Compile) .Executes(() => { DotNetPack(s => s .SetProject(RootDirectory / "src" / "Morphir.Tool" / "Morphir.Tool.csproj") .SetConfiguration(Configuration) .SetVersion(Version.ToString()) .SetOutputDirectory(OutputDir)); }); - Fix
PublishToolglob pattern:var toolPackage = OutputDir.GlobFiles("Morphir.Tool.*.nupkg") .FirstOrDefault();
BDD Tests:
Feature: Project structure refactor
Scenario: Build Morphir.Tool package
Given Morphir.Tool project exists
When I run "./build.sh PackTool"
Then Morphir.Tool.*.nupkg should be created
And package should contain tools/net10.0/any/morphir.dll
Scenario: Build split successfully
Given Build.cs is split into partial classes
When I run "./build.sh --help"
Then all targets should be available
And no build errors should occur
Phase 2: Changelog-Driven Versioning (2-3 days)
Goal: Integrate Ionide.KeepAChangelog, implement PrepareRelease
Tasks
2.1 Add Ionide.KeepAChangelog
- Add package to
build/_build.csproj:cd build dotnet add package Ionide.KeepAChangelog --version 0.2.0 - Add using statement to Build.cs:
using KeepAChangelogParser; using Semver;
2.2 Implement Version Extraction
- Create
ChangelogHelper.GetVersionFromChangelog():public static SemVersion GetVersionFromChangelog(AbsolutePath changelogPath) { var content = File.ReadAllText(changelogPath); var parser = new ChangelogParser(); var result = parser.Parse(content); if (!result.IsSuccess) throw new Exception($"Failed to parse CHANGELOG.md: {result.Error}"); var changelog = result.Value; var latest = changelog.SectionCollection.FirstOrDefault() ?? throw new Exception("No releases found in CHANGELOG.md"); if (!SemVersion.TryParse(latest.MarkdownVersion, SemVersionStyles.Any, out var version)) throw new Exception($"Invalid version: {latest.MarkdownVersion}"); return version; }
2.3 Implement Release Notes Extraction
- Create
ChangelogHelper.GetReleaseNotes():public static string GetReleaseNotes(AbsolutePath changelogPath) { var content = File.ReadAllText(changelogPath); var parser = new ChangelogParser(); var result = parser.Parse(content); if (!result.IsSuccess) return string.Empty; var latest = result.Value.SectionCollection.FirstOrDefault(); if (latest == null) return string.Empty; var notes = new StringBuilder(); AppendSection("Added", latest.SubSections.Added); AppendSection("Changed", latest.SubSections.Changed); // ... other sections return notes.ToString(); }
2.4 Update Build.cs to Use Changelog
- Add property:
SemVersion Version => ChangelogHelper.GetVersionFromChangelog(ChangelogFile); string ReleaseNotes => ChangelogHelper.GetReleaseNotes(ChangelogFile); AbsolutePath ChangelogFile => RootDirectory / "CHANGELOG.md"; - Update all Pack targets to use
Versionproperty - Update all Pack targets to use
ReleaseNotesfor PackageReleaseNotes
2.5 Implement PrepareRelease Target
- Create target in
Build.Publishing.cs:[Parameter("Version to release")] readonly string ReleaseVersion; Target PrepareRelease => _ => _ .Description("Prepare a new release: moves [Unreleased] to [X.Y.Z]") .Requires(() => ReleaseVersion) .Executes(() => { // 1. Validate version if (!SemVersion.TryParse(ReleaseVersion, out var version)) throw new Exception($"Invalid version: {ReleaseVersion}"); // 2. Validate [Unreleased] has content if (!ChangelogHelper.HasUnreleasedContent(ChangelogFile)) throw new Exception("[Unreleased] section is empty"); // 3. Update CHANGELOG.md ChangelogHelper.PrepareRelease(ChangelogFile, ReleaseVersion); // 4. Stage changes Git("add CHANGELOG.md"); // 5. Show next steps Serilog.Log.Information("✓ Prepared release {0}", ReleaseVersion); Serilog.Log.Information("Next steps:"); Serilog.Log.Information(" 1. Review: git diff --staged"); Serilog.Log.Information(" 2. Commit: git commit -m 'chore: prepare release {0}'", ReleaseVersion); Serilog.Log.Information(" 3. Push: git push origin release/{0}", ReleaseVersion); Serilog.Log.Information(" 4. Create PR to main"); Serilog.Log.Information(" 5. After merge, tag: git tag -a v{0} -m 'Release {0}'", ReleaseVersion); Serilog.Log.Information(" 6. Push tag: git push origin v{0}", ReleaseVersion); });
2.6 Implement Changelog Manipulation
- Create
ChangelogHelper.HasUnreleasedContent():public static bool HasUnreleasedContent(AbsolutePath changelogPath) { var content = File.ReadAllText(changelogPath); var unreleasedPattern = @"\[Unreleased\][\s\S]*?(?=\[[\d\.]|\z)"; var match = Regex.Match(content, unreleasedPattern); return match.Success && (match.Value.Contains("- ") || match.Value.Contains("* ")); } - Create
ChangelogHelper.PrepareRelease():public static void PrepareRelease(AbsolutePath changelogPath, string version) { var content = File.ReadAllText(changelogPath); var date = DateTime.Now.ToString("yyyy-MM-dd"); // Extract [Unreleased] content var unreleasedPattern = @"## \[Unreleased\](.*?)(?=## \[|$)"; var match = Regex.Match(content, unreleasedPattern, RegexOptions.Singleline); if (!match.Success) throw new Exception("Could not find [Unreleased] section"); var unreleasedContent = match.Groups[1].Value.Trim(); // Create new sections var newUnreleased = "## [Unreleased]\n\n"; var newRelease = $"## [{version}] - {date}\n\n{unreleasedContent}\n\n"; // Replace [Unreleased] with both sections var updated = Regex.Replace( content, unreleasedPattern, newUnreleased + newRelease, RegexOptions.Singleline ); // Update comparison links updated = UpdateComparisonLinks(updated, version); File.WriteAllText(changelogPath, updated); }
2.7 Implement Auto Pre-release Bumping
- Create
ChangelogHelper.GetNextPreReleaseVersion():public static SemVersion GetNextPreReleaseVersion(AbsolutePath changelogPath) { var currentVersion = GetVersionFromChangelog(changelogPath); if (!currentVersion.IsPrerelease) throw new Exception("Cannot auto-bump non-prerelease version"); // Extract pre-release type and number // e.g., "alpha.1" → type: "alpha", number: 1 var prereleaseParts = currentVersion.Prerelease.Split('.'); var type = prereleaseParts[0]; // alpha, beta, preview, rc var number = int.Parse(prereleaseParts.Length > 1 ? prereleaseParts[1] : "0"); // Increment number number++; // Create new version var newPrerelease = $"{type}.{number}"; return new SemVersion( currentVersion.Major, currentVersion.Minor, currentVersion.Patch, newPrerelease ); } - Create target for auto-bump (used in CI):
Target BumpPreRelease => _ => _ .Description("Auto-bump pre-release version (CI only)") .Executes(() => { var currentVersion = Version; if (!currentVersion.IsPrerelease) { Serilog.Log.Information("Not a pre-release, skipping auto-bump"); return; } var nextVersion = ChangelogHelper.GetNextPreReleaseVersion(ChangelogFile); Serilog.Log.Information("Auto-bumping {0} → {1}", currentVersion, nextVersion); // Update CHANGELOG.md with empty section for next pre-release ChangelogHelper.AddPreReleaseSection(ChangelogFile, nextVersion.ToString()); });
BDD Tests:
Feature: Changelog-driven versioning
Scenario: Extract version from CHANGELOG
Given CHANGELOG.md has [0.2.1] - 2025-12-20
When I call GetVersionFromChangelog()
Then version should be 0.2.1
Scenario: Prepare release
Given CHANGELOG.md has [Unreleased] with content
When I run "./build.sh PrepareRelease --version 0.2.1"
Then CHANGELOG.md should have [0.2.1] - 2025-12-20
And [Unreleased] should be empty
And changes should be staged
Scenario: Block release without content
Given CHANGELOG.md [Unreleased] is empty
When I run "./build.sh PrepareRelease --version 0.2.1"
Then build should fail
And error should mention "empty"
Phase 3: Build Testing Infrastructure (3-4 days)
Goal: Create comprehensive build tests
Tasks
3.1 Create Test Project
- Create
tests/Morphir.Build.Tests/directory - Create
Morphir.Build.Tests.csproj:<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <IsPackable>false</IsPackable> </PropertyGroup> <ItemGroup> <PackageReference Include="TUnit" /> <PackageReference Include="FluentAssertions" /> <PackageReference Include="System.IO.Compression" /> </ItemGroup> </Project> - Create test infrastructure:
public class TestFixture { public AbsolutePath ArtifactsDir { get; } public AbsolutePath FindPackage(string pattern) { } }
3.2 Package Structure Tests
- Create
PackageStructureTests.cs:[Test] public async Task ToolPackage_HasCorrectStructure() { // Arrange var package = FindLatestPackage("Morphir.Tool.*.nupkg"); // Act using var archive = ZipFile.OpenRead(package); var entries = archive.Entries.Select(e => e.FullName).ToList(); // Assert entries.Should().Contain("tools/net10.0/any/morphir.dll"); entries.Should().Contain("tools/net10.0/any/DotnetToolSettings.xml"); entries.Should().Contain("tools/net10.0/any/Morphir.Core.dll"); entries.Should().Contain("tools/net10.0/any/Morphir.Tooling.dll"); } [Test] public async Task ToolPackage_HasCorrectToolSettings() { var package = FindLatestPackage("Morphir.Tool.*.nupkg"); using var archive = ZipFile.OpenRead(package); var entry = archive.GetEntry("tools/net10.0/any/DotnetToolSettings.xml"); using var reader = new StreamReader(entry.Open()); var xml = await reader.ReadToEndAsync(); xml.Should().Contain("<Command Name=\"morphir\""); xml.Should().Contain("EntryPoint=\"morphir.dll\""); } [Test] public async Task LibraryPackages_HaveCorrectStructure() { var corePackage = FindLatestPackage("Morphir.Core.*.nupkg"); using var archive = ZipFile.OpenRead(corePackage); var entries = archive.Entries.Select(e => e.FullName).ToList(); entries.Should().Contain(e => e.Contains("lib/net10.0/Morphir.Core.dll")); entries.Should().NotContain(e => e.Contains("tools/")); }
3.3 Package Metadata Tests
- Create
PackageMetadataTests.cs:[Test] public async Task AllPackages_HaveSameVersion() { var corePackage = FindLatestPackage("Morphir.Core.*.nupkg"); var toolingPackage = FindLatestPackage("Morphir.Tooling.*.nupkg"); var toolPackage = FindLatestPackage("Morphir.Tool.*.nupkg"); var coreVersion = GetPackageVersion(corePackage); var toolingVersion = GetPackageVersion(toolingPackage); var toolVersion = GetPackageVersion(toolPackage); coreVersion.Should().Be(toolingVersion); coreVersion.Should().Be(toolVersion); } [Test] public async Task AllPackages_HaveVersionFromChangelog() { var changelogVersion = GetVersionFromChangelog(); var toolPackage = FindLatestPackage("Morphir.Tool.*.nupkg"); var packageVersion = GetPackageVersion(toolPackage); packageVersion.Should().Be(changelogVersion); } [Test] public async Task ToolPackage_HasCorrectMetadata() { var package = FindLatestPackage("Morphir.Tool.*.nupkg"); var nuspec = GetNuspec(package); nuspec.Id.Should().Be("Morphir.Tool"); nuspec.Authors.Should().Contain("FINOS"); nuspec.License.Should().NotBeNullOrEmpty(); nuspec.ProjectUrl.Should().Contain("morphir-dotnet"); nuspec.PackageType.Should().Be("DotnetTool"); } [Test] public async Task ToolPackage_HasReleaseNotes() { var package = FindLatestPackage("Morphir.Tool.*.nupkg"); var nuspec = GetNuspec(package); nuspec.ReleaseNotes.Should().NotBeNullOrEmpty(); nuspec.ReleaseNotes.Should().Contain("### "); // Has sections }
3.4 Local Installation Tests (Phase 2 of testing strategy)
- Create
LocalInstallationTests.cs:[Test] public async Task ToolPackage_InstallsFromLocalFolder() { // Arrange var tempDir = CreateTempDirectory(); var localSource = Path.Combine(tempDir, "feed"); Directory.CreateDirectory(localSource); var toolPackage = FindLatestPackage("Morphir.Tool.*.nupkg"); File.Copy(toolPackage, Path.Combine(localSource, Path.GetFileName(toolPackage))); // Act var installResult = await RunDotNet( $"tool install --global --add-source {localSource} Morphir.Tool" ); // Assert installResult.ExitCode.Should().Be(0); var versionResult = await RunDotNet("morphir --version"); versionResult.ExitCode.Should().Be(0); versionResult.Output.Should().MatchRegex(@"\d+\.\d+\.\d+"); // Cleanup await RunDotNet("tool uninstall --global Morphir.Tool"); Directory.Delete(tempDir, true); } [Test] public async Task ToolCommand_IsAvailableAfterInstall() { // Assumes tool is installed var result = await RunCommand("morphir", "--help"); result.ExitCode.Should().Be(0); result.Output.Should().Contain("morphir"); result.Output.Should().Contain("Commands:"); }
3.5 Add TestBuild Target
- Create target in
Build.Testing.cs:Target TestBuild => _ => _ .DependsOn(PackAll) .Description("Run build system tests") .Executes(() => { DotNetTest(s => s .SetProjectFile(RootDirectory / "tests" / "Morphir.Build.Tests" / "Morphir.Build.Tests.csproj") .SetConfiguration(Configuration) .EnableNoRestore() .EnableNoBuild()); }); Target TestAll => _ => _ .DependsOn(Test, TestE2E, TestBuild) .Description("Run all tests");
3.6 Integrate into CI
- Update
.github/workflows/development.yml:- name: Run build tests run: ./build.sh TestBuild
BDD Tests:
Feature: Build testing infrastructure
Scenario: Validate tool package structure
Given Morphir.Tool package is built
When I run package structure tests
Then all required files should be present
And tool settings should be correct
Scenario: Validate version consistency
Given all packages are built
When I run metadata tests
Then all packages should have same version
And version should match CHANGELOG.md
Scenario: Test local installation
Given tool package is in local folder
When I install tool from local source
Then installation should succeed
And morphir command should be available
Phase 4: Deployment & Distribution (2-3 days)
Goal: Update workflows for dual distribution
Tasks
4.1 Update Deployment Workflow
- Update
.github/workflows/deployment.yml:name: Deployment on: push: tags: - 'v*' # Trigger on version tags (e.g., v0.2.1) workflow_dispatch: inputs: release_version: description: 'Version to deploy (optional, reads from CHANGELOG if not provided)' required: false jobs: validate-version: runs-on: ubuntu-latest outputs: version: ${{ steps.get-version.outputs.version }} steps: - uses: actions/checkout@v4 - name: Get version from CHANGELOG id: get-version run: | # Extract from tag name (v0.2.1 → 0.2.1) if [[ "${{ github.ref }}" == refs/tags/* ]]; then VERSION=${GITHUB_REF#refs/tags/v} echo "version=$VERSION" >> $GITHUB_OUTPUT elif [[ -n "${{ github.event.inputs.release_version }}" ]]; then echo "version=${{ github.event.inputs.release_version }}" >> $GITHUB_OUTPUT else echo "No version specified" exit 1 fi - name: Validate version in CHANGELOG run: | VERSION=${{ steps.get-version.outputs.version }} if ! grep -q "\[$VERSION\]" CHANGELOG.md; then echo "Version $VERSION not found in CHANGELOG.md" exit 1 fi build-executables: needs: validate-version # ... existing build-executables jobs ... release: needs: [validate-version, build-executables] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup .NET SDK uses: actions/setup-dotnet@v4 with: global-json-file: global.json - name: Restore dependencies run: ./build.sh Restore - name: Build run: ./build.sh Compile - name: Run tests run: ./build.sh TestAll # Includes build tests! - name: Download executables uses: actions/download-artifact@v4 - name: Pack packages run: ./build.sh PackAll - name: Run build tests run: ./build.sh TestBuild - name: Publish to NuGet run: ./build.sh PublishAll --api-key ${{ secrets.NUGET_TOKEN }} env: NUGET_TOKEN: ${{ secrets.NUGET_TOKEN }} create-github-release: needs: [validate-version, build-executables, release] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Download executables uses: actions/download-artifact@v4 with: path: artifacts/executables - name: Extract release notes from CHANGELOG id: release-notes run: | VERSION=${{ needs.validate-version.outputs.version }} # Extract section for this version from CHANGELOG.md awk '/## \['$VERSION'\]/,/## \[/ {print}' CHANGELOG.md | head -n -1 > release-notes.md - name: Create GitHub Release uses: softprops/action-gh-release@v1 with: tag_name: v${{ needs.validate-version.outputs.version }} name: Release v${{ needs.validate-version.outputs.version }} body_path: release-notes.md files: | artifacts/executables/morphir-* artifacts/executables/morphir.exe env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4.2 Update Install Scripts
- Verify
scripts/install-linux.shuses “morphir” command - Verify
scripts/install-macos.shuses “morphir” command - Verify
scripts/install-windows.ps1uses “morphir” command - Update download URLs to point to GitHub releases:
VERSION="0.2.1" URL="https://github.com/finos/morphir-dotnet/releases/download/v${VERSION}/morphir-linux-x64" - Test install scripts locally (manual)
4.3 Validate PublishTool Target
- Update glob pattern in
Build.Publishing.cs:Target PublishTool => _ => _ .DependsOn(PackTool) .Description("Publish Morphir.Tool to NuGet.org") .Executes(() => { if (string.IsNullOrEmpty(ApiKey)) throw new Exception("API_KEY required"); var toolPackage = OutputDir.GlobFiles("Morphir.Tool.*.nupkg") .FirstOrDefault(); if (toolPackage == null) throw new Exception($"Morphir.Tool package not found in {OutputDir}"); Serilog.Log.Information($"Publishing {toolPackage}"); DotNetNuGetPush(s => s .SetTargetPath(toolPackage) .SetSource(NuGetSource) .SetApiKey(ApiKey) .SetSkipDuplicate(true)); });
BDD Tests:
Feature: Deployment workflow
Scenario: Deploy on tag push
Given tag v0.2.1 is pushed
When deployment workflow runs
Then version should be extracted from CHANGELOG.md
And packages should be built
And build tests should run
And packages should be published to NuGet
And executables should be uploaded to GitHub release
Scenario: Block deployment if version not in CHANGELOG
Given tag v0.2.2 is pushed
But CHANGELOG.md doesn't have [0.2.2]
When deployment workflow runs
Then workflow should fail
And no packages should be published
Phase 5: Documentation (1-2 days)
Goal: Comprehensive documentation for all stakeholders
Tasks
5.1 Update AGENTS.md
Add section: “Build System Configuration”
## Build System Configuration ### Nuke Parameters The build system uses Nuke with these parameters: - `--configuration`: Build configuration (Debug/Release) - `--version`: Version override (reads from CHANGELOG.md by default) - `--api-key`: NuGet API key for publishing - `--nuget-source`: NuGet source URL - `--skip-tests`: Skip test execution ### Environment Variables - `NUGET_TOKEN`: NuGet API key (CI only) - `CONFIGURATION`: Build configuration override - `MORPHIR_EXECUTABLE_PATH`: E2E test executable pathAdd section: “Changelog-Driven Versioning”
## Changelog-Driven Versioning Morphir uses CHANGELOG.md as the single source of truth for versions. ### Version Format Follows [Semantic Versioning](https://semver.org/): - `MAJOR.MINOR.PATCH` for releases (e.g., `0.2.1`) - `MAJOR.MINOR.PATCH-TYPE.NUMBER` for pre-releases (e.g., `0.2.1-beta.2`) Supported pre-release types: alpha, beta, preview, rc ### Release Preparation Workflow 1. During development, add changes to `[Unreleased]` section 2. When ready to release, run: `./build.sh PrepareRelease --version X.Y.Z` 3. Review staged changes: `git diff --staged` 4. Commit: `git commit -m "chore: prepare release X.Y.Z"` 5. Create release branch: `git checkout -b release/X.Y.Z` 6. Push and create PR to main 7. After PR merge, create tag: `git tag -a vX.Y.Z -m "Release X.Y.Z"` 8. Push tag: `git push origin vX.Y.Z` (triggers deployment)Add section: “Dual Distribution Strategy”
## Dual Distribution Strategy Morphir provides two distribution channels: ### NuGet Tool Package (Morphir.Tool) **For**: .NET developers with SDK installed **Install**: `dotnet tool install -g Morphir.Tool` **Update**: `dotnet tool update -g Morphir.Tool` **Command**: `morphir` ### Platform Executables **For**: Shell scripts, containers, non-.NET environments **Install**: Use install scripts or download from GitHub releases **Platforms**: linux-x64, linux-arm64, win-x64, osx-arm64 **Command**: `morphir` or `./morphir-{platform}`
5.2 Update CLAUDE.md
- Add build organization guidance
- Document PrepareRelease workflow
- Add testing requirements
- Update commit message examples
5.3 Update README.md
Add persona-based installation instructions:
## Installation ### For .NET Developers If you have the .NET SDK installed: ```bash dotnet tool install -g Morphir.Tool morphir --versionFor Shell Scripts / Containers
If you don’t have .NET SDK or need a standalone executable:
Linux/macOS:
curl -sSL https://get.morphir.org | bashWindows:
irm https://get.morphir.org/install.ps1 | iexManual Download: Download from GitHub Releases
5.4 Create DEPLOYMENT.md
- Document release process for maintainers
- Add troubleshooting guide
- Document rollback procedures
- Add deployment checklist
5.5 Write BDD Feature Files
Create
tests/Morphir.E2E.Tests/Features/ToolInstallation.feature:Feature: Morphir Tool Installation As a .NET developer I want to install Morphir as a dotnet tool So that I can use it in my development workflow Scenario: Install from NuGet Given I am a .NET developer with SDK installed When I run "dotnet tool install -g Morphir.Tool" Then the tool should install successfully And I should be able to run "morphir --version" And the version should match CHANGELOG.md Scenario: Update tool Given Morphir.Tool is already installed When I run "dotnet tool update -g Morphir.Tool" Then the tool should update successfully And the new version should be activeCreate
tests/Morphir.E2E.Tests/Features/ExecutableDownload.feature:Feature: Morphir Executable Download As a shell script user I want to download a standalone executable So that I can use Morphir without installing .NET SDK Scenario: Download from GitHub releases Given I am using a minimal container When I download morphir-linux-x64 from GitHub releases Then I should be able to run "./morphir-linux-x64 --version" And the version should match CHANGELOG.md Scenario: Install via script Given I have curl available When I run the install script Then morphir should be installed to /usr/local/bin And morphir command should be in PATH
BDD Tests:
Feature: Documentation completeness
Scenario: All distribution methods documented
Given README.md exists
When I read installation instructions
Then I should see dotnet tool installation
And I should see executable download instructions
And I should see persona-based recommendations
Scenario: Release process documented
Given AGENTS.md exists
When I read the release preparation section
Then I should see PrepareRelease workflow
And I should see tag creation steps
And I should see deployment trigger explanation
BDD Acceptance Criteria
Epic-Level Scenarios
Feature: Morphir Deployment Architecture
As a Morphir maintainer
I want a robust deployment architecture
So that releases are reliable and users can install easily
Background:
Given the morphir-dotnet repository is up to date
And all dependencies are installed
Scenario: Successful deployment to NuGet and GitHub
Given CHANGELOG.md has [0.2.1] - 2025-12-20
And all changes are committed
When I create and push tag v0.2.1
Then deployment workflow should complete successfully
And Morphir.Tool.0.2.1.nupkg should be published to NuGet.org
And Morphir.Core.0.2.1.nupkg should be published to NuGet.org
And Morphir.Tooling.0.2.1.nupkg should be published to NuGet.org
And morphir-linux-x64 should be in GitHub release v0.2.1
And morphir-win-x64 should be in GitHub release v0.2.1
And morphir-osx-arm64 should be in GitHub release v0.2.1
And release notes should match CHANGELOG.md
Scenario: Build tests catch package issues
Given I modify package structure incorrectly
When I run "./build.sh TestBuild"
Then tests should fail
And I should see clear error message
And CI deployment should be blocked
Scenario: Version consistency across packages
Given I prepare release 0.2.1
When I build all packages
Then all packages should have version 0.2.1
And version should match CHANGELOG.md [0.2.1]
And all package release notes should match
Scenario: .NET developer installation
Given Morphir.Tool is published to NuGet
When .NET developer runs "dotnet tool install -g Morphir.Tool"
Then tool should install successfully
And "morphir --version" should work
And version should match published version
Scenario: Container user installation
Given morphir-linux-x64 is in GitHub releases
When container user downloads executable
Then "./morphir-linux-x64 --version" should work
And version should match release version
And no .NET SDK should be required
Component-Level Scenarios
See individual phase BDD tests in Implementation Plan sections.
Testing Strategy
Test Pyramid
/\
/E2E\ E2E Tests (Morphir.E2E.Tests)
/______\ - Full tool installation workflows
/ \ - Executable download and usage
/ Integration\ - Cross-platform verification
/______________\
/ \
/ Unit Tests \ Unit Tests (Morphir.Build.Tests)
/____________________\ - Package structure validation
- Metadata correctness
- Version extraction
- Changelog parsing
Test Categories
1. Build System Tests (tests/Morphir.Build.Tests/)
Package Structure Tests:
- Validate tool package contains correct files
- Validate library packages contain correct files
- Validate DotnetToolSettings.xml correctness
- Validate no unnecessary files included
Package Metadata Tests:
- Version consistency across packages
- Version matches CHANGELOG.md
- Authors, license, URLs set correctly
- Release notes extracted correctly
- PackageId naming conventions
Changelog Tests:
- Parse valid changelog
- Extract version correctly
- Extract release notes correctly
- Validate unreleased content detection
- Test PrepareRelease transformations
Local Installation Tests (Phase 2):
- Install tool from local folder
- Verify command is available
- Run –version and validate output
- Uninstall successfully
2. E2E Tests (tests/Morphir.E2E.Tests/)
Tool Installation Tests:
- Install from NuGet feed
- Update tool
- Uninstall tool
- Verify command availability
Executable Tests:
- Download from GitHub releases
- Execute on each platform
- Verify version output
- Test basic commands
Cross-Platform Tests:
- Linux x64
- Linux ARM64
- Windows x64
- macOS ARM64
3. Integration Tests
CI Workflow Tests (manual verification):
- Tag push triggers deployment
- Version validation passes
- Build tests run successfully
- Packages publish to NuGet
- GitHub release created with executables
Install Script Tests (manual verification):
- Linux install script works
- macOS install script works
- Windows install script works
- Scripts download correct version
Coverage Targets
- Build System: >= 80% code coverage
- Unit Tests: >= 80% code coverage (existing requirement)
- E2E Tests: All critical user journeys covered
- Manual Tests: Release checklist 100% complete
CI Integration
# .github/workflows/development.yml
jobs:
test:
steps:
- name: Run unit tests
run: ./build.sh Test
- name: Run build tests
run: ./build.sh TestBuild
- name: Run E2E tests
run: ./build.sh TestE2E
# .github/workflows/deployment.yml
jobs:
release:
steps:
- name: Run all tests
run: ./build.sh TestAll # Blocks deployment if tests fail
Risks & Mitigation
Risk 1: Version Drift Between Packages
Risk: Different packages published with different versions
Impact: HIGH - User confusion, installation failures
Probability: LOW (after mitigation)
Mitigation:
- ✅ Single source of truth (CHANGELOG.md)
- ✅ Automated extraction via Ionide.KeepAChangelog
- ✅ Build tests validate version consistency
- ✅ CI blocks if versions don’t match
Detection: Build tests fail, CI blocks deployment
Recovery: Fix CHANGELOG.md, rebuild packages
Risk 2: Breaking Existing Users
Risk: Users with current tool/executable can’t upgrade
Impact: MEDIUM - User frustration, support burden
Probability: LOW
Mitigation:
- ✅ Keep backward compatibility (command name stays “morphir”)
- ✅ Clear migration documentation
- ✅ Test installation on clean machines
- ✅ Announce changes in release notes
Detection: User reports, E2E tests
Recovery: Hotfix release, update documentation
Risk 3: Build Tests Add CI Time
Risk: CI takes longer, slows development
Impact: LOW - Developer velocity
Probability: MEDIUM
Mitigation:
- ✅ Run build tests in parallel with other tests
- ✅ Cache NuGet packages
- ✅ Optimize test execution
- ✅ Phase 2/3 tests are optional (local only)
Measurement: Monitor CI duration, target < 10 minutes total
Risk 4: Complex Release Process
Risk: Release preparation is error-prone
Impact: MEDIUM - Release delays
Probability: LOW (after automation)
Mitigation:
- ✅ Automated PrepareRelease target
- ✅ Clear documentation and checklists
- ✅ Validation steps prevent mistakes
- ✅ Dry-run capability
Detection: PrepareRelease validation failures
Recovery: Fix issues, re-run PrepareRelease
Risk 5: Ionide.KeepAChangelog Bugs
Risk: Parser fails or extracts incorrect version
Impact: HIGH - Deployment failure
Probability: VERY LOW (mature library)
Mitigation:
- ✅ Comprehensive changelog validation tests
- ✅ Fallback to manual version override
- ✅ CI validation before deployment
- ✅ Monitor parser errors
Detection: Build tests, CI validation
Recovery: Manual version override, report bug upstream
Success Metrics
Immediate Success Criteria (Phase 1-2)
- Zero deployment failures due to package naming
- All packages have same version from CHANGELOG.md
- Build tests catch 100% of package structure issues
- PrepareRelease target works without errors
- Documentation covers all user personas
Short-Term Success Criteria (Phase 3-4)
- CI deployment time < 10 minutes
- Build tests have >= 80% coverage
- GitHub releases created automatically
- Install scripts work on all platforms
- Zero user-reported installation issues
Long-Term Success Criteria (6 months)
- Deployment success rate >= 99%
- Release preparation time < 5 minutes
- User satisfaction with installation >= 90%
- Build system maintainability score >= 8/10
- Zero security vulnerabilities in packages
Key Performance Indicators (KPIs)
Reliability:
- Deployment success rate
- Build test pass rate
- Package validation pass rate
Efficiency:
- Average release preparation time
- CI execution time
- Time to fix deployment issues
Quality:
- Package structure defects found
- Version consistency violations
- User-reported installation issues
Developer Experience:
- Time to understand release process
- Number of manual steps required
- Documentation completeness score
Timeline
Gantt Chart
Phase 1: Project Structure & Build Organization [3-4 days]
├─ Create Morphir.Tool project [1 day]
├─ Split Build.cs by domain [1 day]
├─ Create helper classes [0.5 day]
├─ Remove deprecated code [0.5 day]
└─ Update build targets [1 day]
Phase 2: Changelog-Driven Versioning [2-3 days]
├─ Add Ionide.KeepAChangelog [0.5 day]
├─ Implement version extraction [0.5 day]
├─ Implement release notes extraction [0.5 day]
├─ Implement PrepareRelease target [1 day]
├─ Implement changelog manipulation [0.5 day]
└─ Implement auto pre-release bumping [0.5 day]
Phase 3: Build Testing Infrastructure [3-4 days]
├─ Create test project [0.5 day]
├─ Package structure tests [1 day]
├─ Package metadata tests [1 day]
├─ Local installation tests [1 day]
├─ Add TestBuild target [0.5 day]
└─ Integrate into CI [0.5 day]
Phase 4: Deployment & Distribution [2-3 days]
├─ Update deployment workflow [1 day]
├─ Create GitHub release automation [1 day]
├─ Update install scripts [0.5 day]
└─ Validate PublishTool target [0.5 day]
Phase 5: Documentation [1-2 days]
├─ Update AGENTS.md [0.5 day]
├─ Update CLAUDE.md [0.5 day]
├─ Update README.md [0.5 day]
├─ Create DEPLOYMENT.md [0.5 day]
└─ Write BDD feature files [0.5 day]
Total: 11-16 days
Milestones
M1: Core Architecture Complete (Day 4)
- Morphir.Tool project created
- Build.cs split and organized
- Deprecated code removed
- Build targets updated
M2: Version Management Complete (Day 7)
- Ionide.KeepAChangelog integrated
- PrepareRelease target working
- CHANGELOG.md is single source of truth
- Pre-release bumping implemented
M3: Testing Infrastructure Complete (Day 11)
- Build tests project created
- Package validation tests passing
- Local installation tests passing
- CI integration complete
M4: Deployment Ready (Day 14)
- Deployment workflow updated
- GitHub releases automated
- Install scripts validated
- End-to-end flow tested
M5: Documentation Complete (Day 16)
- All documentation updated
- BDD scenarios written
- Release process documented
- Ready for production release
Design Decision Rationale
Why Separate Projects?
Context: Single project tried to serve both tool and executable use cases.
Problem:
- Mixed concerns (tool packaging + AOT compilation)
- Complex build configuration
- Difficult to optimize for each scenario
- Package naming confusion
Decision: Create separate Morphir.Tool project
Reasoning:
- Industry pattern: Nuke.GlobalTool, GitVersion.Tool, etc.
- Clear boundaries: Tool project knows nothing about AOT
- Independent optimization: Tool package can be small, executable can be trimmed
- Easier testing: Can test tool installation separately from executable behavior
- Maintainability: Each project has single responsibility
Alternatives Rejected:
- Keep single project: Doesn’t address root cause
- Rename package only: Band-aid solution
- Use complex build conditions: Too fragile
Why Ionide.KeepAChangelog?
Context: Need changelog-driven versioning with pre-release support.
Problem:
- Manual version management is error-prone
- CHANGELOG.md and versions can drift
- Pre-release versions not standardized
- GitVersion doesn’t fit changelog-first workflow
Decision: Use Ionide.KeepAChangelog as single source of truth
Reasoning:
- Respects Keep a Changelog: Already following this standard
- Full SemVer support: Pre-release versions (alpha, beta, rc) via Semver library
- Mature library: Used in F# ecosystem, well-tested
- Single source of truth: No version.json duplication
- Release notes automation: Automatically extract for packages
Alternatives Rejected:
- version.json: Duplication with CHANGELOG.md
- GitVersion: Doesn’t fit changelog-driven approach
- Environment variable only: No validation, error-prone
- FAKE.Core.Changelog: Requires FAKE build system
Why Hybrid Testing Strategy?
Context: Need to catch packaging issues before CI.
Problem:
- No automated package validation
- TestContainers adds complexity
- Want fast feedback loop
Decision: Phase 1 (structure/metadata), Phase 2 (local install), Phase 3 (containers)
Reasoning:
- Pragmatic: Start simple, add complexity when needed
- Fast feedback: No Docker startup for basic validation
- Covers 80%: Structure/metadata tests catch most issues
- Incremental: Can add TestContainers later
- Low barrier: Easy for contributors to run
Alternatives Rejected:
- Folder-based only: Insufficient validation
- Full TestContainers immediately: Overkill, slower tests, complexity
- No tests: Unacceptable, issues found in CI only
Why Split Build.cs by Domain?
Context: Build.cs will grow with new features.
Problem:
- Single 900+ line file becomes unwieldy
- Vertical slice architecture used in Morphir.Tooling
- Want consistent patterns across codebase
Decision: Split by domain (Packaging, Publishing, Testing) + extract helpers
Reasoning:
- Aligns with architecture: Matches Morphir.Tooling vertical slices
- Clear boundaries: Related targets grouped together
- Scalable: Easy to add new domains (Documentation, Analysis)
- Testable: Helper classes can be unit tested
- Team familiarity: Same patterns they already use
Alternatives Rejected:
- Keep single file: Will become unmaintainable
- Split by technical concern: Doesn’t match feature boundaries
- Vertical slice with separate files: Overkill for current size
Why Dual Distribution?
Context: Different users have different needs.
Problem:
- .NET developers want dotnet tool
- Container users can’t install .NET SDK
- Shell scripts need standalone executable
Decision: Publish both tool package and executables
Reasoning:
- Serves all personas: No user excluded
- Industry standard: How major tools distribute
- Optimal for each: Tool for dev, AOT for production
- Flexible: Users choose what works for them
- Already building both: Just need to organize/document
Alternatives Rejected:
- Tool only: Excludes non-SDK users
- Executable only: Not idiomatic for .NET developers
- Force users to choose one: Why limit options?
References
Internal Documents
- AGENTS.md - Agent guidance and conventions
- CLAUDE.md - Claude Code specific instructions
- CHANGELOG.md - Keep a Changelog format
- NUKE_MIGRATION.md - Nuke migration guide
External References
Morphir:
Standards:
Tools:
Patterns:
Appendices
Appendix A: Current vs Future Package Structure
Current (Broken):
artifacts/packages/
├── morphir.0.2.0.nupkg ← lowercase (breaks glob)
├── Morphir.Core.0.2.0.nupkg
└── Morphir.Tooling.0.2.0.nupkg
Future (Fixed):
artifacts/packages/
├── Morphir.Tool.0.2.1.nupkg ← New, capital M
├── Morphir.Core.0.2.1.nupkg
└── Morphir.Tooling.0.2.1.nupkg
artifacts/executables/
├── morphir-linux-x64 ← Standalone
├── morphir-linux-arm64
├── morphir-win-x64
└── morphir-osx-arm64
Appendix B: CHANGELOG.md Format Examples
Valid pre-release entries:
## [0.2.1-alpha.1] - 2025-12-18
## [0.2.1-alpha.2] - 2025-12-19
## [0.2.1-beta.1] - 2025-12-20
## [0.2.1-beta.2] - 2025-12-21
## [0.2.1-rc.1] - 2025-12-22
## [0.2.1] - 2025-12-23
Invalid entries:
## [0.2.1-SNAPSHOT] - 2025-12-18 ❌ Not SemVer
## [0.2.1-beta] - 2025-12-19 ⚠️ Missing number (but parses)
## 0.2.1 - 2025-12-20 ❌ Missing brackets
## [0.2.1] ❌ Missing date
Appendix C: Build Target Dependency Graph
CI (full pipeline)
├── Restore
├── Compile
│ └── Restore
├── Test
│ └── Compile
├── TestE2E
│ └── Compile
├── PackAll
│ ├── PackLibs
│ │ └── Compile
│ └── PackTool
│ └── Compile
├── TestBuild
│ └── PackAll
└── PublishAll
├── PublishLibs
│ └── PackLibs
└── PublishTool
└── PackTool
PrepareRelease (standalone)
└── (validates CHANGELOG.md)
BumpPreRelease (CI only)
└── (updates CHANGELOG.md)
Appendix D: File Size Estimates
Tool Package (~5-10 MB):
- Managed DLLs only
- Dependencies: Morphir.Core, Morphir.Tooling, WolverineFx, etc.
- No native code
Executables (~50-80 MB each):
- AOT-compiled native code
- Self-contained (no .NET SDK required)
- Trimmed and optimized
- Platform-specific
Comparison:
- NuGet tool: Fast download for developers with SDK
- Executables: Larger but work everywhere
Appendix E: Version Comparison Matrix
| Scenario | Current | Future |
|---|---|---|
| Version source | RELEASE_VERSION env var | CHANGELOG.md |
| Pre-release | Manual string | SemVer (alpha.1, beta.2) |
| Validation | None | Automated (build tests) |
| Consistency | Manual verification | Enforced (single source) |
| Release notes | Manual copy-paste | Auto-extracted |
| Drift risk | HIGH | LOW |
Status Tracking
Feature Status
| Feature | Status | Notes |
|---|---|---|
| Morphir.Tool project | ⏳ Planned | Phase 1 |
| Build.cs split | ⏳ Planned | Phase 1 |
| Helper classes | ⏳ Planned | Phase 1 |
| Ionide.KeepAChangelog | ⏳ Planned | Phase 2 |
| PrepareRelease target | ⏳ Planned | Phase 2 |
| Build tests project | ⏳ Planned | Phase 3 |
| Package validation | ⏳ Planned | Phase 3 |
| Deployment workflow | ⏳ Planned | Phase 4 |
| GitHub releases | ⏳ Planned | Phase 4 |
| Documentation | ⏳ Planned | Phase 5 |
Blockers
| Blocker | Impact | Resolution |
|---|---|---|
| None yet | - | - |
Decisions Pending
| Decision | Options | Status |
|---|---|---|
| None | - | All approved |
PRD Version: 1.0 Last Updated: 2025-12-18 Status: Approved Owner: @morphir-maintainers
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.