Add reusable PowerForge publish bundles and hooks#331
Add reusable PowerForge publish bundles and hooks#331PrzemyslawKlys wants to merge 16 commits intomainfrom
Conversation
Code Review: PR #331 — PowerForge Reusable Publish Bundles and HooksOverviewThis PR is a well-scoped, meaningful addition to the PowerForge engine. It introduces four bundle composition primitives ( Bugs / Correctness Issues1. Dead code in // PowerForge/Services/DotNetPublishPipelineRunner.Plan.cs
private static string[] NormalizeArguments(IEnumerable<string>? values)
{
return (values ?? Array.Empty<string>())
.Where(v => v is not null)
.Select(v => v ?? string.Empty) // <- dead branch
.ToArray();
}2. Potential glob-matching gap in The exclude pattern logic: var rootlessGlobstar = pattern.StartsWith("**/") ? pattern.Substring(3) : null;
if (WildcardMatch(relative, pattern)
|| WildcardMatch(fileName, pattern)
|| (rootlessGlobstar is not null && WildcardMatch(relative, rootlessGlobstar)))For a pattern like
If 3. The new Code Quality4. Silent kill in timeout path // DotNetPublishPipelineRunner.Hooks.cs
try { process.Kill(); } catch { /* best effort */ }Swallowing the kill exception entirely means kill failures are invisible even in verbose/diagnostic runs. A 5. var raw = (command ?? string.Empty).Trim().Trim('"');This is probably fine for the common case of 6. Two-tier token syntax without a prominent doc callout Bundle path tokens use single-brace Design / Documentation7. The file opens with: Hardcoded local paths and a date-stamped analysis artifact make this look like a planning scratchpad rather than long-lived documentation. The content is genuinely useful context, but the machine path ( What's Working Well
Minor Nits
SummaryThe implementation is solid and the new features are well-tested. The two items most worth verifying before merge are the glob-matching depth for |
Code Review: Add Reusable PowerForge Publish Bundles and HooksOverview: This PR extracts common TierBridge/TestimoX build/package/MSI flows into reusable PowerForge primitives — command hooks (6 phases), bundle composition (CopyItems, ModuleIncludes, GeneratedScripts, PrimarySubdirectory), post-process signing, and MSI harvest exclude patterns. The architecture is sound and the motivation is solid. Below is my analysis. ✅ Strengths
|
| Area | Covered | Notes |
|---|---|---|
| Hook plan ordering (all 6 phases) | ✅ | Plan_AddsCommandHooksAroundPublishAndBundleSteps |
| Hook token expansion | ✅ | RunCommandHook_ExpandsTokensInArgumentsAndEnvVars |
| Bundle CopyItems / ModuleIncludes / GeneratedScripts | ✅ | BuildBundle_ComposesPackageWithAllBundlePrimitives |
| Signing profile resolution | ✅ | New bundle signing tests |
| MSI harvest excludes | ✅ | MsiPrepare_AppliesHarvestExcludePatterns |
| Hook timeout / deadlock path | ❌ | Not tested |
| Template path outside project root | ❌ | Not tested |
WildcardMatch with deep **/ paths |
❌ | Pattern matching edge cases |
Example.PackageBundleMsi.json round-trip |
❌ | Example drift |
Summary
The PR is well-structured, architecturally sound, and meaningfully tested. The three issues worth addressing before merge are: (1) the potential deadlock in the net472 timeout path, (2) the missing EnsurePathWithinRoot on TemplatePath, and (3) the CLI behavior change deserving a changelog entry. The remaining items are improvements to consider but not blockers.
Code Review — PR #331: Add reusable PowerForge publish bundles and hooksOverviewThis is a large, well-structured addition (2,844 lines) that moves bespoke build/package/MSI logic from downstream repos (TierBridge, TestimoX) into PowerForge as reusable engine primitives. The new surfaces — Specific Issues / Suggestions1. Local filesystem path left in shipped documentation
This is a local developer path that will be misleading for anyone reading the published docs. Consider removing or replacing with just the branch name. 2.
|
Code Review: PR #331 — Add reusable PowerForge publish bundles and hooksThis is a well-structured, high-quality PR that significantly expands the PowerForge publish pipeline. The additions are coherent, consistently styled, and backed by good test coverage. Below is my detailed feedback. OverviewThe PR delivers:
Code Quality & Style ✅
Specific Observations
|
Code Review — PR #331: PowerForge publish bundles and hooksOverviewSubstantial, well-structured addition that moves reusable package/MSI build mechanics out of downstream repos and into the PowerForge engine. The change surface is large but logically cohesive: six new bundle composition primitives, a command-hook system with six pipeline phases, harvest exclusion patterns, bundle post-process signing, target-ordering for bundle includes, and a pre-normalization fix for mixed line endings before PSScriptAnalyzer. Code Quality and StyleStrengths
Minor style inconsistencies
Potential Bugs / Issues1. Inconsistent basename matching between harvest exclude and bundle signing
|| WildcardMatch(relative, pattern)
|| WildcardMatch(fileName, pattern) // bare filename match anywhere
|| (rootlessGlobstar is not null && ...)
if (WildcardMatch(relativePath, pattern)) return true;
if (pattern.StartsWith("**/")) return WildcardMatch(relativePath, pattern.Substring(3));A pattern like 2. When 3. The local Performance Considerations
For each pattern, the method does a full SecurityThe new surfaces were reviewed and look sound:
Test CoverageGood coverage overall. The following gaps are worth considering before shipping:
Specific Suggestions// AddCommandHookSteps — simplify redundant guard (hook is already normalized)
HookTimeoutSeconds = hook.TimeoutSeconds, // was: hook.TimeoutSeconds <= 0 ? Default : Math.Max(1, ...)// Align error message style in NormalizeCommandHooks
if (string.IsNullOrWhiteSpace(id))
throw new ArgumentException("Hooks[].Id is required.");
if (string.IsNullOrWhiteSpace(hook.Command))
throw new ArgumentException($"Hooks['{id}'].Command is required."); // consistent JSON-path style// CopyBundlePath — surface a more actionable error when ClearDestination=false conflicts
if (!clearDestination && File.Exists(destinationFile))
throw new IOException($"{description}: destination already exists and ClearDestination=false: {destinationFile}");
File.Copy(source, destinationFile, overwrite: clearDestination);DocumentationDocs are thorough and accurate. The double-brace / single-brace distinction for template tokens is clearly explained in the quickstart. The SummaryThis is well-engineered work. The three items that are worth addressing before merge are:
Everything else is either correct, cosmetic, or a future-nice-to-have. The hook system, bundle primitives, and signing integration are clean additions that follow the existing patterns of the codebase. |
PR Review: Add reusable PowerForge publish bundles and hooksOverall Assessment: This is a well-structured, substantial PR that extracts reusable build/publish patterns from TierBridge and TestimoX into a shared PowerForge foundation. The design is sound, security guards are present, and test coverage is reasonable. I have a few specific findings worth addressing before merge. Strengths
Specific Concerns1. Token expansion — recursive substitution riskIn
2.
|
Code Review: PR 331 - Add reusable PowerForge publish bundles and hooksLarge, well-structured PR (+3423/-64) adding new engine primitives: Positives
Issues Medium: In Low: Multi-style CLI override silently drops profile style filter When the user passes Low: If a template file has a UTF-8 BOM, the BOM bytes appear at the start of the rendered script, producing an invisible character that can break PowerShell Low: The loop caps at 12 directory levels without explanation. A brief inline comment or named constant would clarify intent. Nit: Security
Summary Production-quality work. The implementation is clean, security boundaries are tested, and the design doc makes the migration path clear. The medium issue around in-place spec mutation is worth addressing before merge to avoid subtle bugs in multi-phase CLI flows; the rest are low-priority or stylistic. No blocking issues found. |
Code Review — PR #331: Add reusable PowerForge publish bundles and hooksThis is a well-structured PR that adds significant new infrastructure to PowerForge. The implementation is generally solid, with good test coverage, proper security hardening on path operations, and clean cross-platform compatibility handling. Notes below are organized by area. OverviewThe PR delivers five related features:
All five features are documented in the quickstart and schemas, and Strengths
Issues and SuggestionsMedium1. The step now carries 8 hook-specific properties ( public DotNetPublishCommandHookPayload? Hook { get; set; }…containing the hook fields, populated only when 2. Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!);
var dir = Path.GetDirectoryName(outputPath)
?? throw new InvalidOperationException($"Cannot determine parent directory of '{outputPath}'.");
Directory.CreateDirectory(dir);3.
Minor4. Redundant branch in if (WildcardMatch(relativePath, pattern)) return true;
var fileName = Path.GetFileName(relativePath);
if (WildcardMatch(fileName, pattern)) return true;
if (pattern.StartsWith("**/", StringComparison.Ordinal))
return WildcardMatch(relativePath, pattern.Substring("**/".Length));
return false;For a pattern like 5. var moduleTokens = tokens.ToDictionary(t => t.Key, t => t.Value, StringComparer.OrdinalIgnoreCase);
moduleTokens["moduleName"] = module.ModuleName;This is fine for typical module-include counts, but worth noting if the token dictionary grows large. An alternative is a lightweight overlay (a two-level lookup) that avoids copying. Not a blocking issue. 6. Single-quoted paths (less common on Windows but valid in PowerShell config) would not be stripped. Documenting this limitation in the method's context (or in the schema 7. Whitespace churn in The diff reformats the No Issues Found
SummaryThe implementation is ready to merge with the medium issues addressed or tracked. Issue #2 ( |
Code Review: PR #331 — Add reusable PowerForge publish bundles and hooksOverviewThis is a large, well-scoped PR (+3440/-65) that adds four major capabilities to the PowerForge publish pipeline:
The design is clean and follows established patterns in the codebase well. Most of the feedback below is minor. Positives
Issues / Suggestions1.
|
Code Review: PR #331 — Add reusable PowerForge publish bundles and hooksOverviewThis is a large, well-scoped PR that extracts shared build/package/MSI mechanics from TierBridge and TestimoX into PowerForge reusable primitives. The additions are:
The design rationale document ( Code Quality and StylePositive patterns throughout:
One nit on private static bool BundleSignPatternMatches(string relativePath, string pattern)
{
if (WildcardMatch(relativePath, pattern)) return true;
var fileName = Path.GetFileName(relativePath);
if (WildcardMatch(fileName, pattern)) return true;
if (pattern.StartsWith("**/", StringComparison.Ordinal))
return WildcardMatch(relativePath, pattern.Substring("**/".Length));
return false;
}The third branch ( CorrectnessLine-endings normalizer fix is correct. The old code CLI override ordering is a real correctness fix. Moving from
if (activeProfile is not null)
activeProfile.Style = styles.Length == 1 ? styles[0] : null;When the caller passes multiple Potential Issues1. if (Directory.Exists(source))
{
if (clearDestination && Directory.Exists(destination))
Directory.Delete(destination, recursive: true);
else if (clearDestination && File.Exists(destination))
File.Delete(destination);
else if (!clearDestination && (Directory.Exists(destination) || File.Exists(destination)))
throw new IOException(...);
DirectoryCopy(source, destination);
return;
}If 2. if (result.ExitCode == 0)
{
if (_logger.IsVerbose)
{
if (!string.IsNullOrWhiteSpace(result.StdOut)) _logger.Verbose(result.StdOut.TrimEnd());
if (!string.IsNullOrWhiteSpace(result.StdErr)) _logger.Verbose(result.StdErr.TrimEnd());
}
return;
}Hook stdout/stderr are only emitted at verbose level on success. If a hook writes meaningful progress to stdout, users running without 3. When loading from JSON config, version resolution falls back to 4. Timeout test reliability [Fact]
public void RunCommandHook_ReportsTimeoutExplicitly()
{
if (!CommandExists("pwsh")) return;
// ...
HookArguments = new[] { "-NoLogo", "-NoProfile", "-Command", "Start-Sleep -Seconds 10" },
HookTimeoutSeconds = 1,
SecurityHook command execution is inherently privileged, since hooks run arbitrary commands from the JSON config. The token expansion (
Test CoverageCoverage is strong:
Gaps worth noting:
Summary
The two actionable items I'd flag before merge:
Everything else is nits or informational. The PR is in good shape. |
Code Review — PR #331: Add reusable PowerForge publish bundles and hooksThis is a substantial, well-structured PR. The overall design is sound, validation is thorough, and test coverage is solid. The notes below are ordered by severity. Potential bugs1. When if (clearDestination && Directory.Exists(destination))
Directory.Delete(destination, recursive: true); // directory removed
var destinationFile = destination;
if (Directory.Exists(destinationFile)) // always false now
destinationFile = Path.Combine(destinationFile, Path.GetFileName(source));
// → copies source to `destination` path (no filename appended)If a user passes 2. if (pattern.StartsWith("**/", StringComparison.Ordinal))
return WildcardMatch(relativePath, pattern.Substring("**/".Length));
3. The timeout path correctly drains streams with a bounded Code quality & designPositive highlights
Minor issues
Test coverageCoverage is genuinely good — path-traversal rejection, cycle detection, token non-recursive expansion, and timeout reporting all have dedicated tests. A few gaps worth filling:
SecurityNo new attack surfaces beyond the existing model. The SummaryThe feature design is solid and the implementation is careful. The three items worth addressing before merge are: the |
Code Review: PR #331 — Add reusable PowerForge publish bundles and hooksThis is a well-designed, substantial addition (+3572/−63 lines) that generalises bespoke TierBridge/TestimoX build flows into reusable PowerForge primitives. Overall quality is high: security invariants are respected, the design is internally consistent, and test coverage is good. Below are the findings, grouped by severity. OverviewThe PR ships six cohesive capabilities:
Potential Bugs1. if (clearDestination && destinationWasDirectory)
{
Directory.Delete(destination, recursive: true);
Directory.CreateDirectory(destination); // recreate parent
}
if (clearDestination && File.Exists(destinationFile))
File.Delete(destinationFile); // then delete again?
File.Copy(source, destinationFile, overwrite: clearDestination);When 2. var preNormalizeResult = preNormalize.FirstOrDefault(x => string.Equals(x.Path, f, StringComparison.OrdinalIgnoreCase));
3. if (!process.WaitForExit(timeoutMs))
{
process.Kill(entireProcessTree: true);
Task.WhenAll(stdoutTask, stderrTask).Wait(TimeSpan.FromSeconds(5));
...
return (-1, stdout, stderr, fileName, TimedOut: true);
}
process.WaitForExit(); // ← this is the happy-pathThe second #if NET472
process.WaitForExit(30_000);
#else
process.WaitForExit();
#endifSecurity ObservationsGood: Worth noting: Hook
Design ObservationsSpec cloning before overrides (CLI path): var spec = CloneDotNetPublishSpec(loaded.Value);
ApplyDotNetPublishSpecOverrides(spec, overrideTargets, effectiveRids, effectiveFrameworks, effectiveStyles);
var plan = runner.Plan(spec, specPath);
// previously: ApplyDotNetPublishPlanOverrides(plan, ...)This is the correct fix — overrides now influence planning rather than only execution. The JSON round-trip clone is a pragmatic approach; the dependency on
private const int DefaultCommandHookTimeoutSeconds = DotNetPublishCommandHook.DefaultTimeoutSeconds;This alias adds no value — callers can reference Test CoverageCoverage is strong. Specific highlights:
One gap: there is no test for Minor Style Notes
Summary
The PR is in good shape. The items above are improvement suggestions rather than blockers — the most actionable ones are the |
Code Review: PR #331 — PowerForge Publish Bundles and HooksOverviewThis PR adds a substantial set of reusable build primitives to PowerForge: declarative command hooks at fixed publish phases, new bundle composition fields ( Code Quality and Best Practices ✅Strengths:
Minor style issue:
Potential Bugs / Issues
|
Summary
Why
TierBridge and TestimoX both had large bespoke PowerShell package/MSI flows. This moves the reusable pieces into PowerForge so downstream repos can use thin wrappers and shared JSON configs instead of reimplementing staging, bundle composition, signing, and MSI preparation.
Validation
dotnet test .\PowerForge.Tests\PowerForge.Tests.csproj -c Release --filter "FullyQualifiedName~DotNetPublish" --nologopassed: 101 tests.dotnet test .\PowerForge.Tests\PowerForge.Tests.csproj -c Release --filter "FullyQualifiedName~DotNetPublishPipelineRunnerBundleTests|FullyQualifiedName~PowerForgeBundlePostProcessServiceTests" --nologopassed: 12 tests.dotnet test .\PowerForge.Tests\PowerForge.Tests.csproj -c Release --filter "FullyQualifiedName~LineEndingsNormalizerTests|FullyQualifiedName~FormattingSummaryTests" --nologopassed: 40 tests.dotnet test .\PowerForge.Tests\PowerForge.Tests.csproj -c Release --filter "FullyQualifiedName~DotNetPublishPipelineRunnerBundleTests|FullyQualifiedName~PowerForgeBundlePostProcessServiceTests|FullyQualifiedName~LineEndingsNormalizerTests" --nologopassed: 14 tests..\Build\Build-Module.ps1passed locally..\Build\Build-Module.ps1 -NoBuildpassed locally after restoring the mixed source files, proving the formatter pre-normalization path.git diff --checkpassed.dotnet test .\PSPublishModule.sln -c Release --nologo, PowerForge CLI net10 build, PowerForge net472 build.