This document describes the JSON configuration consumed by Invoke-ProjectBuild and the behavior it drives.
For the unified repo-level entrypoint that combines package and downloadable tool releases in one file,
see Build/release.json and powerforge release.
For module-plus-NuGet repository releases such as PSParseHTML, where one run should publish packages,
the PowerShell module, and one GitHub asset set, see
Unified Module And Project Releases.
For a PowerShell-first authoring layer proposal that keeps the same engine but avoids raw CLI argument shaping,
see Docs/PSPublishModule.ProjectBuild.DslProposal.md.
PowerShell-authored project release objects
- A first PowerShell-first slice is now available through:
New-ConfigurationProjectReleaseNew-ConfigurationProjectTargetNew-ConfigurationProjectSigningNew-ConfigurationProjectWorkspaceNew-ConfigurationProjectOutputNew-ConfigurationProjectInstallerNew-ConfigurationProjectNew-ProjectReleaseConfigExport-ConfigurationProjectImport-ConfigurationProjectInvoke-ProjectReleaseInvoke-PowerForgeRelease -Project <ConfigurationProject>
- This stays on the same unified release engine used by
powerforge releaseandInvoke-PowerForgeRelease -ConfigPath .... - Relative target and installer paths are resolved from
ConfigurationProject.ProjectRootwhen provided. New-ConfigurationProjectReleasecan now also carry default release intent such as:-PublishToolGitHub-SkipRestore-SkipBuild-ToolOutput <Tool|Portable|Installer|Store>-SkipToolOutput <...>
- Project objects can now round-trip through JSON:
Export-ConfigurationProject -Project $project -OutputPath '.\Build\project.release.json'Import-ConfigurationProject -Path '.\Build\project.release.json'
- Starter JSON can now be scaffolded directly:
New-ProjectReleaseConfig -ProjectRoot '.' -PassThruNew-ProjectReleaseConfig -Project '.\src\App\App.csproj' -Portable -Force
- In the current first slice, tool/app targets should still declare an explicit runtime for DotNetPublish-backed plan/build flows.
Example:
Import-Module PSPublishModule -Force
$release = New-ConfigurationProjectRelease -Configuration Release
$signing = New-ConfigurationProjectSigning -Mode OnDemand
$output = New-ConfigurationProjectOutput -StageRoot '.\Artifacts\DslSmoke'
$target = New-ConfigurationProjectTarget `
-Name 'PowerForgeCli' `
-ProjectPath '.\PowerForge.Cli\PowerForge.Cli.csproj' `
-Runtime 'win-x64' `
-Framework 'net10.0' `
-Style PortableCompat `
-OutputType Tool, Portable
$project = New-ConfigurationProject `
-Name 'PSPublishModule' `
-ProjectRoot (Get-Location).Path `
-Release $release `
-Signing $signing `
-Output $output `
-Target $target
Invoke-ProjectRelease -Project $project -PlanFor module help/docs generation workflow (Invoke-ModuleBuild, New-ConfigurationDocumentation, about_* topics),
see Docs/PSPublishModule.ModuleDocumentation.md.
For project-specific actions that run at stable module pipeline stages, see
Docs/PSPublishModule.ModuleLifecycleActions.md.
Schema
- Location:
Schemas/project.build.schema.json
Unified release entrypoint
- Schema:
Schemas/powerforge.release.schema.json - Scaffolder:
New-PowerForgeReleaseConfig -ProjectRoot . -PassThru - PowerShell cmdlet:
Invoke-PowerForgeRelease -ConfigPath .\Build\release.json - Wrapper:
Build/Build-Project.ps1 - Transitional top-level wrapper:
Build/Build-Release.ps1 - Preview tool wrapper:
Build/Build-ToolsPreview.ps1 - CLI:
powerforge release --config .\Build\release.json - Packages continue to use
project.build.json/Invoke-ProjectBuild. - Tools/apps can now use either legacy
Tools.Targetsor the richerTools.DotNetPublish/Tools.DotNetPublishConfigPathpath backed bySchemas/powerforge.dotnetpublish.schema.json. - This repo now also includes a focused preview-binary config in
Build/release.tools-preview.json. Use it when you want to publishPowerForge/PowerForgeWebexecutables without also touching module/package release flow. The preview config intentionally:- limits release scope to tools only
- stages into
Artifacts/Preview - marks GitHub releases as prerelease
- uses stable
-previewtags per tool/version so reruns can reuse the same release and resume missing asset uploads - keeps the existing local
TokenFilePathpattern fromBuild/release.json; replace that path if you run the preview flow from another machine or CI environment
- Module release can now be declared directly in
release.jsonthrough the top-levelModulesection. In this repo that section shells out toModule/Build/Build-Module.ps1and stages the declared artefact folders. - Repositories that need one version across NuGet packages, a PowerShell module, and executable outputs can set
Module.SynchronizeVersionWithPackages: trueand identifyModule.VersionPrimaryProject. KeepModule.IncludesPackages: falseso the package lane has one owner. The package plan receives the module's next version as a floor, resolves the configured primary package project, and then passes that shared version to the module and executable lanes. During a real release this first pass is plan-only; NuGet execution is deferred until the module and executable builds succeed. UsePackages.AlignPackageVersions: truewhen all package projects must share the version. - Interactive PowerShell and CLI runs render the same phase plan, live elapsed time, and final release summary. Redirected, verbose, quiet, no-color, and JSON runs keep line-oriented or structured output for CI and agents.
Build/Build-Release.ps1still supports bridge mode for repos that have not adopted a nativeModulesection yet, but it automatically defers torelease.jsonwhenModuleis present.- When native
Modulemode is active, the same day-to-day overrides fromBuild/Build-Release.ps1can still flow into the module script:-NoDotnetBuild-ModuleVersion <version>-PreReleaseTag <tag>-NoSign-SignModule
- Unified release can also declare a reusable workspace preflight via
WorkspaceValidationbacked byworkspace.validation.jsonandpowerforge workspace validate. - Common release-time overrides:
--configuration Debug|Release--module-no-dotnet-build,--module-version,--module-prerelease-tag,--module-no-sign,--module-sign--skip-workspace-validation,--workspace-config,--workspace-profile--workspace-enable-feature,--workspace-disable-feature,--workspace-external-root <name=path>--target,--rid,--framework,--style--tool-output <Tool|Portable|Installer|Store>and--skip-tool-output <...>when the unified release should keep the high-level release intent simple while PowerForge decides which internal DotNetPublish steps still need to run--skip-restoreand--skip-buildfor DotNetPublish-backed tool/app flows--output-root <path>to remap DotNetPublish tool/app artefacts, manifests, bundle outputs, and installer staging under a different root--stage-root <path>to copy unified release assets into a categorized release folder (modules,nuget,portable,installer,tools,metadata) and writerelease-manifest.json/SHA256SUMS.txtthere by defaultOutputs.Staginginrelease.jsonfor default folder names when you want the same categorized layout without repeating CLI switchesOutputs.Staging.*Pathvalues may point multiple categories at the same folder when you want a flatUploadReadylayout such asNuGet+GitHubOutputs.Staging.*NameTemplatevalues let the staged copies use release-facing names instead of raw internal build names
Wingetinrelease.jsonwhen you want PowerForge to emit portable/signed release manifests from the same staged assets- set
Winget.Submit: true(or pass--submit-winget) to submit the generated manifests withwingetcreateafter the release assets are available
- set
- top-level
GitHubinrelease.jsonwhen you want the unified staged release itself uploaded as one repo release instead of using package-host or per-target tool release publishing--keep-symbolsfor symbol-preserving tool/app outputs--skip-release-checksumswhen you want the staged release folder but do not want a top-levelSHA256SUMS.txt--sign,--sign-profile, and raw overrides such as--sign-thumbprint,--sign-subject-name,--sign-timestamp-url,--sign-tool-path--sign-on-missing-tooland--sign-on-failure(Warn|Fail|Skip) for shared signing policy control--package-sign-thumbprint,--package-sign-store, and--package-sign-timestamp-urlfor package-signing overrides without editingrelease.json- signing now emits a heuristic interaction hint when PowerForge can infer the certificate provider: likely hardware-token/smart-card vs likely local software-backed certificate
Starter flow
- Generate a unified release config from existing repo configs:
New-PowerForgeReleaseConfig -ProjectRoot . -PassThru-
The generated
release.jsonnow includes:- a native
Modulesection pointing atModule/Build/Build-Module.ps1 - default module artefact roots for
Packed,PackedWithModules, andUnpacked - the existing package/tool sections when matching source configs are present
- a native
-
The
Modulesection can optionally carry module-specific defaults too:NoDotnetBuildModuleVersionPreReleaseTagNoSignSignModule
-
Plan a release with the generated config:
Invoke-PowerForgeRelease -ConfigPath .\Build\release.json -PlanExample staging layout for release uploads:
"Outputs": {
"Staging": {
"RootPath": "Artifacts/UploadReady",
"PackagesPath": "NuGet",
"PortablePath": "GitHub",
"InstallerPath": "GitHub",
"PackagesNameTemplate": "{PackageId}.{Version}{Extension}",
"PortableNameTemplate": "{Target}-{Version}-{Runtime}-portable{Extension}",
"InstallerNameTemplate": "{Target}-{Version}-{Runtime}-installer{Extension}"
}
}- checksums written for staged releases now follow the staged filenames/paths, not the raw internal build outputs
Example Winget generation from staged assets:
"Winget": {
"Enabled": true,
"OutputPath": "Artifacts/UploadReady/Winget",
"InstallerUrlTemplate": "https://github.com/ExampleOrg/ExampleApp/releases/download/v{PackageVersion}/{FileName}",
"Submit": true,
"Submission": {
"Mode": "Manifest",
"TokenEnvName": "WINGET_CREATE_GITHUB_TOKEN",
"PullRequestTitle": "Submit {PackageIdentifier} {PackageVersion}",
"NoOpen": true
},
"Packages": [
{
"PackageIdentifier": "ExampleOrg.ExampleApp.Tray",
"PackageVersion": "1.0.0",
"Publisher": "ExampleOrg",
"PackageName": "ExampleApp Tray",
"License": "MIT",
"ShortDescription": "Windows tray app for ExampleApp.",
"Installers": [
{
"Category": "Portable",
"Target": "ExampleApp.Tray",
"Runtime": "win-x64",
"InstallerType": "zip",
"NestedInstallerType": "portable",
"RelativeFilePath": "ExampleApp.Tray.exe"
}
]
}
]
}InstallerUrlTemplatetokens are URL-encoded automatically{FileName}resolves to the staged filename after any*NameTemplaterewrite, not the raw internal artifact filename- when
--stage-root(orOutputs.Staging.RootPath) is active, a relativeWinget.OutputPathis resolved under that active staged release root so per-runUploadReady\<release-id>\Wingetlayouts work without hard-coded absolute paths - set
NestedInstallerTypeexplicitly for archive-based installers when you want a reusable config that is not implicitly “portable zip only” Winget.Submission.Mode: "Manifest"submits the generated YAML viawingetcreate submit <manifest>, while"Update"callswingetcreate update <PackageIdentifier> --urls ... --version ... --submitfor existing Winget packagesWinget.Submission.TokenEnvNamedefaults toWINGET_CREATE_GITHUB_TOKEN;TokenFilePathis also supported, and inlineTokenexists only for temporary/manual use because command-line token arguments can be logged by external toolingWinget.Submission.NoOpendefaults totrueso CI runs do not try to open a browser; pass--winget-open-browserfor interactive desktop runs- command-line overrides include
--submit-winget,--skip-winget-submit,--winget-submit-mode Manifest|Update,--winget-tool-path,--winget-token-env,--winget-token-file,--winget-pr-title,--winget-replace [version],--winget-open-browser, and--winget-allow-interactive-auth
Example unified GitHub release publishing from staged assets:
"GitHub": {
"Publish": true,
"TagTemplate": "v{Version}",
"ReleaseNameTemplate": "{Repository} {Version}"
}-
use
--publish-project-github(or setGitHub.Publish: true) to upload the unified staged release as one repo release -
when top-level
GitHubis active, package-host GitHub publishing is suppressed and the staged release assets are uploaded instead -
uploaded assets include staged
NuGet,Portable,Installer,Tool, metadata files, top-levelrelease-manifest.json/SHA256SUMS.txt, and any generated Winget manifests -
Plan or build preview executables only:
.\Build\Build-ToolsPreview.ps1 -Plan
.\Build\Build-ToolsPreview.ps1 -Runtime win-x64
.\Build\Build-ToolsPreview.ps1 -Runtime win-x64,linux-x64- Publish preview executable releases to GitHub:
.\Build\Build-ToolsPreview.ps1 -PublishGitHub- Preview publish reruns are intentionally idempotent for the same tool version:
- tags use the
-previewsuffix instead of a timestamp - GitHub release publish reuses the existing tag/release and skips assets that were already uploaded
Build-ToolsPreview.ps1 -PublishGitHubenables verbose output automatically so long multi-runtime uploads are visible
- tags use the
Overview
- The build pipeline discovers .NET projects, resolves versions, optionally updates csproj files, packs and signs NuGet packages, and can publish to NuGet and GitHub.
- A plan-only run can be produced with
PlanOnlyor-Plan, which writes the plan JSON without changing files. Invoke-ProjectBuildnow treats publish checks from the plan pass as blocking preflight. NuGet and GitHub prechecks are evaluated before any real publish starts.
Example configuration
{
"$schema": "https://raw.githubusercontent.com/EvotecIT/PSPublishModule/main/Schemas/project.build.schema.json",
"RootPath": "..",
"VersionTracks": {
"ExampleSuite": {
"ExpectedVersion": "1.0.X",
"AnchorProject": "ExampleSuite.Core",
"Projects": [
"ExampleSuite.Csv",
"ExampleSuite.Excel",
"ExampleSuite.Markdown"
]
}
},
"ExpectedVersionMapAsInclude": true,
"ExpectedVersionMapUseWildcards": false,
"ExcludeProjects": [ "ExampleSuite.Legacy", "ExampleSuite.Experimental" ],
"Configuration": "Release",
"StagingPath": "Artefacts/ProjectBuild",
"CleanStaging": true,
"PlanOutputPath": "Artefacts/ProjectBuild/project.build.plan.json",
"UpdateVersions": true,
"Build": true,
"PackStrategy": "MSBuild",
"IncludeSymbols": true,
"PublishNuget": true,
"PublishGitHub": true,
"CreateReleaseZip": true,
"CertificateThumbprint": "THUMBPRINT",
"CertificateStore": "CurrentUser",
"PublishSource": "https://api.nuget.org/v3/index.json",
"PublishApiKeyFilePath": "C:\\path\\to\\nuget-api-key.txt",
"SkipDuplicate": true,
"PublishFailFast": true,
"GitHubAccessTokenFilePath": "C:\\path\\to\\github-token.txt",
"GitHubUsername": "ExampleOrg",
"GitHubRepositoryName": "ExampleSuite",
"GitHubReleaseMode": "Single",
"GitHubPrimaryProject": "ExampleSuite.Core",
"GitHubTagTemplate": "{Repo}-v{Version}",
"GitHubGenerateReleaseNotes": true
}
Discovery and selection
IncludeProjects: only process named projects.ExcludeProjects: skip named projects even if discovered.ExcludeDirectories: skip project discovery under these directory names.- Project discovery automatically stops at nested Git repository/worktree roots (for example review clones or
.claude/worktreesentries inside the current repo). ExpectedVersionMapAsInclude: if true, only projects matching the map are included.ExpectedVersionMapUseWildcards: allows*and?in map keys.
Versioning
ExpectedVersion: global version or X-pattern (e.g.1.2.X).ExpectedVersionMap: per-project overrides (ProjectName-> version/X-pattern).VersionTracks: anchor-driven version trains. Each track resolves one version from an anchor package/project and applies it to every project in the track.VersionTracks.<Name>.AnchorProject: project whose package identity is used as the default version source for the track.VersionTracks.<Name>.AnchorPackageId: optional explicit package identity when it differs from the project name.VersionTracks.<Name>.Projects: sibling projects that should be stamped to the same resolved version.AnchorProjectis included automatically.- When
AnchorPackageIdis used, also setAnchorProjectso the anchor project itself is stamped automatically. AlignPackageVersions: defaults to false. When true, ordinary projects using the same X-pattern are stepped from the highest current package version found for any project in that group. For example, if one2.0.Xpackage is at2.0.5and another is new, both plan2.0.6. Ordinary exact-version overrides outside a version track remain exact.VersionTracksalready coordinate their members when alignment is false and remain explicit, separate group boundaries when it is true. This does not assign one universal version to every project. See Unified Module And Project Releases for complete aligned, non-aligned, and version-track behavior.- In a module pipeline with
Release.SynchronizeModuleVersion, the next available module version is also a floor forRelease.PrimaryProject. If that project belongs to an aligned X-pattern group, the whole group is raised to the floor. A higher numeric NuGet-derived package candidate still wins. At the same numeric version, a stable X-pattern candidate does not erase the configured module prerelease; explicit prerelease versions retain normal semantic-version ordering. The module and primary package patterns must describe the same version line. - When no expected version is provided for a project, the existing csproj version is used.
- When both
VersionTracksandExpectedVersionMapare present, the explicit map wins for matching projects. UpdateVersions: when false, csproj files are not updated.- Version source resolution can use
NugetSource(v3 index URL or local folder) with optional credentials.
To keep an existing package family and newly added packages on one release version, enable alignment alongside the shared X-pattern:
{
"ExpectedVersion": "2.0.X",
"ExpectedVersionMap": {
"ExampleSuite.Core": "2.0.X",
"ExampleSuite.Excel": "2.0.X",
"ExampleSuite.NewPackage": "2.0.X"
},
"ExpectedVersionMapAsInclude": true,
"AlignPackageVersions": true
}PackStrategy: optional packing strategy.PerProjectruns a non-incremental build followed bydotnet pack --no-buildfor each project.MSBuild(aliasBatch) requiresOutputPathorStagingPath, generates a temporary traversal project, and runsRestore;Rebuild;Packfor selected projects in parallel. If no package output path is available, it logs a warning and falls back toPerProject. Both strategies verify primary managed assemblies underlib/<tfm>,runtimes/<rid>/lib/<tfm>, andtools/<tfm>/anyagainst exact fresh build target directories before package signing or publishing. Native runtime assets and metadata-only packages are not assembly-hash targets. Private feed credentials must already be available todotnet/MSBuild restore. Batch mode stops on the first project failure and treats the whole failed batch as failed, so already-produced packages from that batch are not signed or published. Projects without a resolved version are reported as failed skipped projects.
Staging and outputs
StagingPath: root directory for pipeline outputs (recommended).- Packages go to
<StagingPath>\packageswhenOutputPathis not set. - Release zips go to
<StagingPath>\releaseswhenReleaseZipOutputPathis not set.
- Packages go to
- When a project defines
<PackageId>, project-build uses that package identity for NuGet version lookup, planned.nupkgnames, and release zip names. Otherwise it falls back to the csproj file name. CleanStaging: if true, deletes the staging directory before a run. It does not clean projectbin/objdirectories; package correctness comes from the release rebuild and package-payload provenance check.PlanOutputPath: optional file path for a JSON plan output.IncludeSymbols: when true, produces portable.snupkgfiles for every packable project. Plan output includes their expected paths, and build output discovery rejects stale symbol packages just as it does stale primary packages.
NuGet publishing
PublishNuget: enabledotnet nuget push.- Portable
.snupkgfiles produced by the same build are published immediately after their primary.nupkgand are included in unified staged release assets and manifests. PublishApiKey/PublishApiKeyFilePath/PublishApiKeyEnvName: API key sources.PublishFailFast: stop on first publish/signing error.SkipDuplicate: pass--skip-duplicatetodotnet nuget push.
GitHub releases
PublishGitHub: enable GitHub release publishing.GitHubReleaseMode:Single: one release with all project zips attached.PerProject: one release per project.
GitHubPrimaryProject: in single mode, chooses the version used in the tag/name. If multiple project versions exist and no primary is available, the tag uses the current date.GitHubTagTemplateandGitHubReleaseNamesupport tokens:{Project},{Version},{PrimaryProject},{PrimaryVersion},{Repo},{Repository},{Date},{UtcDate},{DateTime},{UtcDateTime},{Timestamp},{UtcTimestamp}{Date}and{UtcDate}are formattedyyyy.MM.dd.
GitHubTagConflictPolicy:Reuse(default): idempotent, reuse existing release/tag when it already exists. InSinglemode, project-build now performs a GitHub precheck before any real publish. If the computed tag already exists and the planned asset set differs for a mixed-version package group, the run stops with an advisory instead of attaching new assets to the old release.Fail: fail if tag already exists.AppendUtcTimestamp: append-yyyyMMddHHmmssUTC suffix to computed tags.
- For mixed-version repositories, prefer one of these patterns:
GitHubReleaseMode: "PerProject"for one release per package version.GitHubTagConflictPolicy: "Fail"orAppendUtcTimestampto avoid silent reuse.GitHubTagTemplate: "{Repo}-v{UtcTimestamp}"for unique per-run tags.
Signing
CertificateThumbprint,CertificateStore,TimeStampServercontrol package signing.- If a certificate cannot be found, the run fails before publishing.
- DotNetPublish-based unified release also emits a best-effort signing interaction note. It can often detect likely hardware-token providers such as SafeNet/eToken versus local software-backed certs, but it remains heuristic because final prompt behavior still depends on middleware and machine policy.
Plan mode
PlanOnly(config) or-Plan(cmdlet): compute a plan without modifying files or publishing.- The plan JSON includes resolved versions, packages that would be created, and publish decisions.
- In plan mode, publish preflight does not require package files to already exist on disk.