Skip to content

Use NuGet packages for HtmlTinkerX and OfficeIMO.Markdown#51

Merged
PrzemyslawKlys merged 1 commit intoweb/apidocs-creffrom
powerforge-nuget-deps-clean
Feb 5, 2026
Merged

Use NuGet packages for HtmlTinkerX and OfficeIMO.Markdown#51
PrzemyslawKlys merged 1 commit intoweb/apidocs-creffrom
powerforge-nuget-deps-clean

Conversation

@PrzemyslawKlys
Copy link
Member

Summary

  • Replace local project references with NuGet packages in PowerForge.Web
  • Use OfficeIMO.Markdown 0.5.2 and HtmlTinkerX 2.0.6

Testing

  • Not run (depends on published HtmlTinkerX package version)

Copilot AI review requested due to automatic review settings February 4, 2026 21:46
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbbabad59e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +215 to +218
var expectedVersion = expectedGlobal;
var expectedSource = "global";
if (expectedMap.TryGetValue(project.ProjectName, out var overrideVersion) && !string.IsNullOrWhiteSpace(overrideVersion))
{

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply wildcard version overrides when map uses patterns

When ExpectedVersionMapUseWildcards is enabled, the docs say map keys can include */?, but the version override logic still uses expectedMap.TryGetValue(project.ProjectName, ...), which only matches exact keys. That means wildcard entries never influence the resolved version for matching projects; they only affect the include-only filtering earlier. For configurations that rely on wildcard version overrides (e.g., a single pattern for multiple projects), those projects will silently fall back to the global/CSProj version instead of the intended map value.

Useful? React with 👍 / 👎.

@claude
Copy link

claude bot commented Feb 4, 2026

Code Review - PR #51

Thank you for this contribution! I've conducted a comprehensive review of the changes. While there's significant functionality being added, I've identified several critical concerns that should be addressed.

Critical Issues

1. Scope Creep - PR Does Not Match Description

Severity: CRITICAL

The PR title states 'Use NuGet packages for HtmlTinkerX and OfficeIMO.Markdown' (suggesting a simple dependency update), but this PR actually includes:

  • Dependency change (2 lines in PowerForge.Web.csproj)
  • New 815-line cmdlet InvokeProjectBuildCommand
  • 534 net additions to DotNetRepositoryReleaseService
  • 479 net additions to ModulePipelineRunner
  • New JSON schema project.build.schema.json
  • GitHub release automation
  • Certificate signing support
  • Complete service rewrites

Recommendation: Split this into separate PRs for safer review, testing, and rollback.

2. API Key Exposure via Process Arguments

Severity: CRITICAL

Location: PowerForge/Services/DotNetRepositoryReleaseService.cs:636-637

API keys passed as command-line arguments are visible in process listings, audit logs, and memory dumps. Should use environment variables instead.

3. GitHub Token Exposure in PowerShell

Severity: HIGH

Location: PSPublishModule/Cmdlets/InvokeProjectBuildCommand.cs:252-255, 346

GitHub token passed as parameter could be exposed in PowerShell transcript logs and debug output. Use secure credential objects or environment variables.

4. Missing Test Coverage

Severity: CRITICAL

The new InvokeProjectBuildCommand.cs (815 lines) has zero test coverage for critical functionality including secret resolution, GitHub release modes, template token replacement, and error handling.

High Priority Issues

5. Bare Catch Blocks Swallowing Exceptions

Multiple instances in InvokeProjectBuildCommand.cs (lines 534, 544, 74) and DotNetRepositoryReleaseService.cs that silently hide errors, making debugging nearly impossible.

6. PowerShell Cmdlet Availability Not Validated

Code assumes Send-GitHubRelease cmdlet exists without validation. Should check availability before invocation.

7. Invalid Certificate Store Names Silently Default

ParseCertificateStore silently falls back to CurrentUser for typos in LocalMachine. Should throw on invalid values.

Medium Priority Issues

8. Race Condition in Staging Path Preparation

Between Directory.Delete and Directory.CreateDirectory, another process could interfere.

9. Template Token Validation Missing

No validation that template tokens are valid. Invalid tokens remain in output.

10. Path Traversal Risk in Zip Creation

Should validate relative paths do not contain .. sequences.

Summary

  • Security: 2 Critical, 1 High, 2 Medium
  • Bugs: 3 High, 3 Medium
  • Quality: 1 Critical, 1 High
  • Tests: 1 Critical
  • Scope: 1 Critical

Recommendations

  1. Split this PR - Separate dependency update from new features
  2. Add comprehensive tests for InvokeProjectBuildCommand
  3. Fix credential exposure - Use environment variables for secrets
  4. Replace bare catch blocks with proper error handling
  5. Validate all inputs - Certificate stores, template tokens, paths
  6. Add cmdlet availability checks before PowerShell invocation
  7. Expand documentation with multiple examples

Let me know if you would like me to elaborate on any of these issues!

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request replaces local project references with NuGet packages for HtmlTinkerX and OfficeIMO.Markdown in PowerForge.Web. However, the PR contains significantly more changes than described, including extensive new functionality for:

  • Module GUID tracking in PSResourceGet/PowerShellGet
  • Online module version resolution
  • ExternalModuleDependencies support
  • Extensive .NET repository build/release pipeline features
  • Package signing capabilities
  • GitHub release automation

Changes:

  • Replace local project references with NuGet packages (HtmlTinkerX 2.0.6, OfficeIMO.Markdown 0.5.2)
  • Add GUID tracking to PSResourceInfo and module discovery scripts
  • Implement online module version resolution without installation
  • Add comprehensive .NET project build/release pipeline with signing and GitHub integration

Reviewed changes

Copilot reviewed 30 out of 30 changed files in this pull request and generated 41 comments.

Show a summary per file
File Description
PowerForge.Web/PowerForge.Web.csproj Replaces local project references with NuGet package references
schemas/project.build.schema.json New JSON schema for project build configuration (257 lines)
schemas/powerforge.segments.schema.json Adds ResolveMissingModulesOnline, WarnIfRequiredModulesOutdated, FailOnMissingCommands
PowerForge/Services/*.cs Multiple services enhanced with GUID tracking, online resolution, signing, and release features
PowerForge/Scripts/*.ps1 PowerShell scripts updated to extract module GUIDs
PowerForge/Models/*.cs Models extended with ExternalModuleDependencies, signing config, GitHub release options
PSPublishModule/Cmdlets/*.cs Cmdlets updated for new features; InvokeProjectBuildCommand added (815 lines)
PowerForge.Web.Cli/Program.cs Web pipeline runner enhanced with progress tracking and duration reporting
Module/Docs/*.md Documentation updated for new parameters and cmdlets
Comments suppressed due to low confidence (1)

PowerForge/Services/ModulePipelineRunner.cs:251

                    if (moduleSeg.Kind == ModuleDependencyKind.RequiredModule)

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +271 to +384
foreach (var project in release.Projects)
{
if (!project.IsPackable) continue;
var r = new ProjectBuildGitHubResult { ProjectName = project.ProjectName };

if (string.IsNullOrWhiteSpace(project.NewVersion))
{
r.Success = false;
r.ErrorMessage = "Missing project version for GitHub release.";
result.GitHub.Add(r);
if (spec.PublishFailFast)
{
result.Success = false;
result.ErrorMessage = r.ErrorMessage;
break;
}
continue;
}

if (string.IsNullOrWhiteSpace(project.ReleaseZipPath))
{
r.Success = false;
r.ErrorMessage = "No release zip available for GitHub release.";
result.GitHub.Add(r);
if (spec.PublishFailFast)
{
result.Success = false;
result.ErrorMessage = r.ErrorMessage;
break;
}
continue;
}

if (!File.Exists(project.ReleaseZipPath))
{
r.Success = false;
r.ErrorMessage = $"Release zip not found: {project.ReleaseZipPath}";
result.GitHub.Add(r);
if (spec.PublishFailFast)
{
result.Success = false;
result.ErrorMessage = r.ErrorMessage;
break;
}
continue;
}

var tag = string.IsNullOrWhiteSpace(config.GitHubTagName)
? (config.GitHubIncludeProjectNameInTag == false ? $"v{project.NewVersion}" : $"{project.ProjectName}-v{project.NewVersion}")
: config.GitHubTagName!;
if (!string.IsNullOrWhiteSpace(config.GitHubTagTemplate))
{
tag = ApplyTemplate(
config.GitHubTagTemplate!,
project.ProjectName,
project.NewVersion ?? project.OldVersion ?? string.Empty,
config.GitHubPrimaryProject ?? project.ProjectName,
project.NewVersion ?? project.OldVersion ?? string.Empty,
repoName,
dateToken,
utcDateToken);
}

var releaseName = string.IsNullOrWhiteSpace(config.GitHubReleaseName)
? tag
: ApplyTemplate(
config.GitHubReleaseName!,
project.ProjectName,
project.NewVersion ?? project.OldVersion ?? string.Empty,
config.GitHubPrimaryProject ?? project.ProjectName,
project.NewVersion ?? project.OldVersion ?? string.Empty,
repoName,
dateToken,
utcDateToken);

var output = sb.Invoke(config.GitHubUsername, config.GitHubRepositoryName, gitHubToken, tag, releaseName, new[] { project.ReleaseZipPath }, config.GitHubIsPreRelease, config.GitHubGenerateReleaseNotes);
var status = output.Count > 0 ? output[0]?.BaseObject : null;

bool ok = false;
string? releaseUrl = null;
string? errorMessage = null;

if (status is SendGitHubReleaseCommand.GitHubReleaseResult gr)
{
ok = gr.Succeeded;
releaseUrl = gr.ReleaseUrl;
errorMessage = gr.Succeeded ? null : gr.ErrorMessage;
}
else if (status is PSObject pso)
{
var succeeded = pso.Properties["Succeeded"]?.Value as bool?;
ok = succeeded ?? false;
releaseUrl = pso.Properties["ReleaseUrl"]?.Value?.ToString();
errorMessage = pso.Properties["ErrorMessage"]?.Value?.ToString();
}
else
{
errorMessage = "Unexpected result from Send-GitHubRelease.";
}

r.Success = ok;
r.TagName = tag;
r.ReleaseUrl = releaseUrl;
r.ErrorMessage = errorMessage;
result.GitHub.Add(r);

if (!ok)
{
result.Success = false;
result.ErrorMessage = errorMessage ?? "GitHub publish failed.";
if (spec.PublishFailFast)
break;
}
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +389 to +394
foreach (var project in release.Projects)
{
if (!project.IsPackable) continue;
var zip = project.ReleaseZipPath;
if (string.IsNullOrWhiteSpace(zip))
continue;
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Suggested change
foreach (var project in release.Projects)
{
if (!project.IsPackable) continue;
var zip = project.ReleaseZipPath;
if (string.IsNullOrWhiteSpace(zip))
continue;
foreach (var project in release.Projects.Where(p => p.IsPackable && !string.IsNullOrWhiteSpace(p.ReleaseZipPath)))
{
var zip = project.ReleaseZipPath;

Copilot uses AI. Check for mistakes.
Comment on lines +414 to +420
foreach (var line in SplitLines(stdout))
{
if (!line.StartsWith("PFDOCS::ERROR::", StringComparison.Ordinal)) continue;
var b64 = line.Substring("PFDOCS::ERROR::".Length);
try { return Encoding.UTF8.GetString(Convert.FromBase64String(b64)); }
catch { return null; }
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +674 to +678
foreach (var item in items)
{
if (string.IsNullOrWhiteSpace(item)) continue;
set.Add(item.Trim());
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +1040 to +1044
foreach (var kvp in expectedMap)
{
if (MatchesPattern(projectName, kvp.Key, allowWildcards))
return true;
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This foreach loop implicitly filters its target sequence - consider filtering the sequence explicitly using '.Where(...)'.

Copilot uses AI. Check for mistakes.
Comment on lines +373 to +387
if (!spec.WhatIf)
{
if (!TryCreateReleaseZip(project, spec.Configuration, zipPath, out var zipError))
{
project.ErrorMessage = zipError;
_logger.Warn($"{project.ProjectName}: {zipError}");
result.Success = false;
if (spec.PublishFailFast)
return result;
}
else
{
_logger.Success($"{project.ProjectName}: release zip created.");
}
}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Condition is always false because of access to property WhatIf.

Suggested change
if (!spec.WhatIf)
{
if (!TryCreateReleaseZip(project, spec.Configuration, zipPath, out var zipError))
{
project.ErrorMessage = zipError;
_logger.Warn($"{project.ProjectName}: {zipError}");
result.Success = false;
if (spec.PublishFailFast)
return result;
}
else
{
_logger.Success($"{project.ProjectName}: release zip created.");
}
}
if (!TryCreateReleaseZip(project, spec.Configuration, zipPath, out var zipError))
{
project.ErrorMessage = zipError;
_logger.Warn($"{project.ProjectName}: {zipError}");
result.Success = false;
if (spec.PublishFailFast)
return result;
}
else
{
_logger.Success($"{project.ProjectName}: release zip created.");
}

Copilot uses AI. Check for mistakes.
var synopsisPercent = Percent(synopsisCount, total);
var descriptionPercent = Percent(descriptionCount, total);
var examplesPercent = Percent(exampleCount, total);
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to examplesPercent is useless, since its value is never read.

Suggested change
var examplesPercent = Percent(exampleCount, total);

Copilot uses AI. Check for mistakes.
var repos = ParseRepositoryList(repository);
var runner = new PowerShellRunner();

IReadOnlyList<PSResourceInfo> items = Array.Empty<PSResourceInfo>();
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assignment to items is useless, since its value is never read.

Suggested change
IReadOnlyList<PSResourceInfo> items = Array.Empty<PSResourceInfo>();
IReadOnlyList<PSResourceInfo> items;

Copilot uses AI. Check for mistakes.
Comment on lines +150 to +153
!string.IsNullOrWhiteSpace(root))
{
if (!File.Exists(Path.Combine(moduleRoot, root)))
issues.Add($"RootModule missing: {root}");
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These 'if' statements can be combined.

Suggested change
!string.IsNullOrWhiteSpace(root))
{
if (!File.Exists(Path.Combine(moduleRoot, root)))
issues.Add($"RootModule missing: {root}");
!string.IsNullOrWhiteSpace(root) &&
!File.Exists(Path.Combine(moduleRoot, root)))
{
issues.Add($"RootModule missing: {root}");

Copilot uses AI. Check for mistakes.
}

var tag = string.IsNullOrWhiteSpace(config.GitHubTagName)
? (config.GitHubIncludeProjectNameInTag == false ? $"v{project.NewVersion}" : $"{project.ProjectName}-v{project.NewVersion}")
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expression 'A == false' can be simplified to '!A'.

Suggested change
? (config.GitHubIncludeProjectNameInTag == false ? $"v{project.NewVersion}" : $"{project.ProjectName}-v{project.NewVersion}")
? (!config.GitHubIncludeProjectNameInTag ? $"v{project.NewVersion}" : $"{project.ProjectName}-v{project.NewVersion}")

Copilot uses AI. Check for mistakes.
@PrzemyslawKlys PrzemyslawKlys force-pushed the powerforge-nuget-deps-clean branch from dbbabad to df84758 Compare February 5, 2026 08:09
@PrzemyslawKlys
Copy link
Member Author

Rebased onto origin/web/apidocs-cref and force-pushed. PR now only changes PowerForge.Web.csproj (2 insertions/2 deletions). Previous review comments about extra files should now be outdated. Please re-review.

@claude
Copy link

claude bot commented Feb 5, 2026

Pull Request Review: Use NuGet packages for HtmlTinkerX and OfficeIMO.Markdown

Summary

This PR replaces local project references with NuGet package references for two dependencies:

  • HtmlTinkerX: 2.0.6
  • OfficeIMO.Markdown: 0.5.2

Code Quality ✅

  • Clean change: The modification is straightforward and follows standard .NET project practices
  • Proper structure: Dependencies correctly moved from ProjectReference to PackageReference
  • Consistent formatting: The .csproj file maintains proper XML structure and formatting

Dependency Impact Analysis 📦

The change affects three key services in PowerForge.Web:

  1. MarkdownRenderer.cs (OfficeIMO.Markdown)

    • Uses: MarkdownReader.Parse(), ToHtmlFragment(), HtmlOptions, PrismOptions
    • Impact: Core markdown rendering functionality
  2. WebAssetOptimizer.cs (HtmlTinkerX)

    • Uses: HtmlOptimizer.OptimizeHtml(), OptimizeCss(), OptimizeJavaScript()
    • Impact: HTML/CSS/JS minification and optimization
    • Lines: 115, 136, 157, 469
  3. WebSiteAuditor.cs (HtmlTinkerX)

    • Uses: HtmlParser.ParseWithAngleSharp(), HtmlBrowser, HtmlBrowserTester
    • Impact: HTML validation, parsing, and browser testing
    • Lines: 142, 280, 286, 309-316

Potential Issues ⚠️

1. Testing Status

The PR body states: Not run (depends on published HtmlTinkerX package version)

  • Critical: No tests have been run to verify compatibility
  • Risk: API changes between local development version and published NuGet package could break functionality
  • Recommendation: The specified versions MUST be published to NuGet before merging, and full testing should be performed

2. Version Verification

  • ❓ Need to confirm that HtmlTinkerX 2.0.6 and OfficeIMO.Markdown 0.5.2 are actually published on NuGet.org
  • ❓ Need to verify these versions are compatible with the current API usage

3. Build Validation

  • The change will cause build failures if:
    • The NuGet packages are not published yet
    • The package versions have breaking API changes
    • Package restore fails in CI/CD

4. No Test Coverage

  • No test files found in the repository for PowerForge.Web
  • Cannot verify if existing tests would catch compatibility issues

Security Considerations 🔒

No security concerns identified:

  • Moving from local references to published NuGet packages is standard practice
  • Reduces maintenance burden and simplifies the build process
  • Package versions are pinned (good practice)

Performance Considerations ⚡

No performance impact expected:

  • Same libraries, just different reference method
  • NuGet packages may actually improve build times by eliminating project-to-project compilation

Best Practices ✅

Follows best practices:

  • ✅ Pinned versions (not using floating versions)
  • ✅ Clean separation of concerns
  • ✅ Reduced build complexity

Recommendations 📋

Before merging, the following MUST be completed:

  1. Verify Package Availability

    • Check that HtmlTinkerX 2.0.6 is published on NuGet.org
    • Check that OfficeIMO.Markdown 0.5.2 is published on NuGet.org
  2. Run Full Build

    • Execute dotnet restore on PowerForge.Web.csproj
    • Execute dotnet build on PowerForge.Web.csproj
  3. Execute Tests (if available)

    • Run all integration tests
    • Verify markdown rendering functionality
    • Test HTML optimization features
    • Validate site auditing capabilities
  4. Check for API Compatibility

    • Ensure all methods used in the codebase exist in the NuGet package versions
    • Verify method signatures have not changed
    • Test actual runtime behavior
  5. Update CI/CD

    • Ensure CI pipeline can restore NuGet packages
    • Verify all dependent projects (PowerForge.Web.Cli, PowerForge.Tests, PowerForge.Blazor) still build

Blocking Issues 🚫

MUST RESOLVE BEFORE MERGE:

  1. ❌ Testing has not been performed - PR explicitly states Not run
  2. ❌ Package availability not confirmed
  3. ❌ No verification that the API surface matches current usage

Conclusion

This is a well-structured change that simplifies the build process, but it CANNOT be merged in its current state without:

  1. Confirming the NuGet packages are published
  2. Running comprehensive tests
  3. Verifying build success across all projects

Recommendation: ⏸️ HOLD - Complete testing checklist before approval.


Review conducted by Claude Code - covering code quality, security, performance, and best practices

@PrzemyslawKlys PrzemyslawKlys merged commit 8000ae8 into web/apidocs-cref Feb 5, 2026
1 check passed
@PrzemyslawKlys PrzemyslawKlys deleted the powerforge-nuget-deps-clean branch February 5, 2026 08:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants