Skip to content

Latest commit

 

History

History
374 lines (336 loc) · 20.4 KB

File metadata and controls

374 lines (336 loc) · 20.4 KB

Project Build (Repository Pipeline)

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 Docs/PSPublishModule.UnifiedModuleProjectRelease.md. 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-ConfigurationProjectRelease
    • New-ConfigurationProjectTarget
    • New-ConfigurationProjectSigning
    • New-ConfigurationProjectWorkspace
    • New-ConfigurationProjectOutput
    • New-ConfigurationProjectInstaller
    • New-ConfigurationProject
    • New-ProjectReleaseConfig
    • Export-ConfigurationProject
    • Import-ConfigurationProject
    • Invoke-ProjectRelease
    • Invoke-PowerForgeRelease -Project <ConfigurationProject>
  • This stays on the same unified release engine used by powerforge release and Invoke-PowerForgeRelease -ConfigPath ....
  • Relative target and installer paths are resolved from ConfigurationProject.ProjectRoot when provided.
  • New-ConfigurationProjectRelease can 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 '.' -PassThru
    • New-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 -Plan

For 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.Targets or the richer Tools.DotNetPublish / Tools.DotNetPublishConfigPath path backed by Schemas/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 publish PowerForge / PowerForgeWeb executables 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 -preview tags per tool/version so reruns can reuse the same release and resume missing asset uploads
    • keeps the existing local TokenFilePath pattern from Build/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.json through the top-level Module section. In this repo that section shells out to Module/Build/Build-Module.ps1 and stages the declared artefact folders.
  • Build/Build-Release.ps1 still supports bridge mode for repos that have not adopted a native Module section yet, but it automatically defers to release.json when Module is present.
  • When native Module mode is active, the same day-to-day overrides from Build/Build-Release.ps1 can still flow into the module script:
    • -NoDotnetBuild
    • -ModuleVersion <version>
    • -PreReleaseTag <tag>
    • -NoSign
    • -SignModule
  • Unified release can also declare a reusable workspace preflight via WorkspaceValidation backed by workspace.validation.json and powerforge 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-restore and --skip-build for 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 write release-manifest.json / SHA256SUMS.txt there by default
    • Outputs.Staging in release.json for default folder names when you want the same categorized layout without repeating CLI switches
    • Outputs.Staging.*Path values may point multiple categories at the same folder when you want a flat UploadReady layout such as NuGet + GitHub
    • Outputs.Staging.*NameTemplate values let the staged copies use release-facing names instead of raw internal build names
  • Winget in release.json when 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 with wingetcreate after the release assets are available
  • top-level GitHub in release.json when you want the unified staged release itself uploaded as one repo release instead of using package-host or per-target tool release publishing
    • --keep-symbols for symbol-preserving tool/app outputs
    • --skip-release-checksums when you want the staged release folder but do not want a top-level SHA256SUMS.txt
    • --sign, --sign-profile, and raw overrides such as --sign-thumbprint, --sign-subject-name, --sign-timestamp-url, --sign-tool-path
    • --sign-on-missing-tool and --sign-on-failure (Warn|Fail|Skip) for shared signing policy control
    • --package-sign-thumbprint, --package-sign-store, and --package-sign-timestamp-url for package-signing overrides without editing release.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.json now includes:

    • a native Module section pointing at Module/Build/Build-Module.ps1
    • default module artefact roots for Packed, PackedWithModules, and Unpacked
    • the existing package/tool sections when matching source configs are present
  • The Module section can optionally carry module-specific defaults too:

    • NoDotnetBuild
    • ModuleVersion
    • PreReleaseTag
    • NoSign
    • SignModule
  • Plan a release with the generated config:

Invoke-PowerForgeRelease -ConfigPath .\Build\release.json -Plan

Example 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"
        }
      ]
    }
  ]
}
  • InstallerUrlTemplate tokens are URL-encoded automatically
  • {FileName} resolves to the staged filename after any *NameTemplate rewrite, not the raw internal artifact filename
  • when --stage-root (or Outputs.Staging.RootPath) is active, a relative Winget.OutputPath is resolved under that active staged release root so per-run UploadReady\<release-id>\Winget layouts work without hard-coded absolute paths
  • set NestedInstallerType explicitly 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 via wingetcreate submit <manifest>, while "Update" calls wingetcreate update <PackageIdentifier> --urls ... --version ... --submit for existing Winget packages
  • Winget.Submission.TokenEnvName defaults to WINGET_CREATE_GITHUB_TOKEN; TokenFilePath is also supported, and inline Token exists only for temporary/manual use because command-line token arguments can be logged by external tooling
  • Winget.Submission.NoOpen defaults to true so CI runs do not try to open a browser; pass --winget-open-browser for 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 set GitHub.Publish: true) to upload the unified staged release as one repo release

  • when top-level GitHub is 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-level release-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 -preview suffix instead of a timestamp
    • GitHub release publish reuses the existing tag/release and skips assets that were already uploaded
    • Build-ToolsPreview.ps1 -PublishGitHub enables verbose output automatically so long multi-runtime uploads are visible

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 PlanOnly or -Plan, which writes the plan JSON without changing files.
  • Invoke-ProjectBuild now 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",
  "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/worktrees entries 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. AnchorProject is included automatically.
  • When AnchorPackageId is used, also set AnchorProject so the anchor project itself is stamped automatically.
  • When no expected version is provided for a project, the existing csproj version is used.
  • When both VersionTracks and ExpectedVersionMap are 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.
  • PackStrategy: optional packing strategy. PerProject runs the legacy dotnet pack loop. MSBuild (alias Batch) requires OutputPath or StagingPath, generates a temporary traversal project, and packs selected projects in one parallel MSBuild invocation. If no package output path is available, it logs a warning and falls back to PerProject. The batch target runs Restore;Pack, so private feed credentials must already be available to dotnet/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>\packages when OutputPath is not set.
    • Release zips go to <StagingPath>\releases when ReleaseZipOutputPath is not set.
  • When a project defines <PackageId>, project-build uses that package identity for NuGet version lookup, planned .nupkg names, and release zip names. Otherwise it falls back to the csproj file name.
  • CleanStaging: if true, deletes the staging directory before a run.
  • PlanOutputPath: optional file path for a JSON plan output.

NuGet publishing

  • PublishNuget: enable dotnet nuget push.
  • PublishApiKey / PublishApiKeyFilePath / PublishApiKeyEnvName: API key sources.
  • PublishFailFast: stop on first publish/signing error.
  • SkipDuplicate: pass --skip-duplicate to dotnet 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.
  • GitHubTagTemplate and GitHubReleaseName support tokens:
    • {Project}, {Version}, {PrimaryProject}, {PrimaryVersion}, {Repo}, {Repository}, {Date}, {UtcDate}, {DateTime}, {UtcDateTime}, {Timestamp}, {UtcTimestamp}
    • {Date} and {UtcDate} are formatted yyyy.MM.dd.
  • GitHubTagConflictPolicy:
    • Reuse (default): idempotent, reuse existing release/tag when it already exists. In Single mode, 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 -yyyyMMddHHmmss UTC suffix to computed tags.
  • For mixed-version repositories, prefer one of these patterns:
    • GitHubReleaseMode: "PerProject" for one release per package version.
    • GitHubTagConflictPolicy: "Fail" or AppendUtcTimestamp to avoid silent reuse.
    • GitHubTagTemplate: "{Repo}-v{UtcTimestamp}" for unique per-run tags.

Signing

  • CertificateThumbprint, CertificateStore, TimeStampServer control 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.