Public/Invoke-CpmfUipsPack.ps1
|
function Invoke-CpmfUipsPack { <# .SYNOPSIS Bumps projectVersion, packs a UiPath project with uipcli, and stages the .nupkg to a local NuGet feed. .DESCRIPTION Execution order: 1. Install-CpmfUipsPackCommandLineTool (skipped with -SkipInstall; repeated per target) 2. Update-CpmfUipsPackProjectVersion (skipped with -NoBump; runs once before all targets) 3. uipcli package pack (once per entry in -Targets) 4. Copy .nupkg to -FeedPath (once per target) 5. If -MultiTfm and both net6+net8 targets: merge lib/ TFMs into one nupkg With -UseWorktree, steps 2-5 run inside a temporary git worktree created at HEAD. The working directory is never modified. The worktree is always removed on exit, even on failure. If the pack step fails after the version has been bumped (non-worktree mode), the original version is restored in project.json. Returns the full path(s) of staged .nupkg files as [string[]]. .PARAMETER ProjectJson Path to the UiPath project.json. Default: ..\project.json relative to the module root. Always pass this parameter explicitly when using the module from any location other than a Scripts\ subfolder of the UiPath project: Invoke-CpmfUipsPack -ProjectJson 'C:\repos\MyProject\project.json' .PARAMETER ShowVersion Print the module version and exit without performing any pack work. .PARAMETER FeedPath Destination directory for the staged .nupkg. Defaults to C:\Users\Public\nugetfeed. .PARAMETER OutputPath Base directory for the native uipcli pack output. The module creates a unique subdirectory beneath this root for each pack run. Defaults to C:\Users\Public\UiPath.CLI.Windows\pack-output. .PARAMETER UipcliArgs Additional arguments passed verbatim to uipcli, e.g. -UipcliArgs '--traceLevel','Verbose','--outputType','Tests' .PARAMETER NoBump Skip the version bump. Useful when repacking after a rollback. .PARAMETER SkipInstall Skip Install-CpmfUipsPackCommandLineTool. Use when .NET and uipcli are already installed. .PARAMETER UseWorktree Pack from a clean git worktree instead of the working directory. Requires the project to be inside a git repository. .PARAMETER WorktreeBase Parent directory for the temporary worktree. Defaults to the system temp directory. Ignored unless -UseWorktree is set. .PARAMETER WorktreeSibling Place the worktree as a sibling of the git repo root rather than in temp. Implies -UseWorktree. .PARAMETER CliVersionNet6 UiPath CLI version for the net6 target (23.x classic). Default: 23.10.2.6. .PARAMETER CliVersionNet8 UiPath CLI version for the net8 target (25.x dotnet tool). Default: 25.10.11. .PARAMETER UipcliPathNet6 Absolute path to the net6 uipcli.exe. Overrides version-based path inference when supplied. .PARAMETER UipcliPathNet8 Absolute path to the net8 uipcli.exe. Overrides version-based path inference when supplied. .PARAMETER Targets Which CLI versions to build with. Valid values: 'net6', 'net8'. Defaults to @('net8'). Use @('net6','net8') to build for both, which requires -Backend uipcli. .PARAMETER MultiTfm For Library projects: after building with both net6 and net8 targets, merge the lib/ TFM folders into a single nupkg. Requires -Targets @('net6','net8'). Ignored for Process/Tests projects (a warning is emitted). .PARAMETER CliVersion Deprecated. Use -CliVersionNet6 or -CliVersionNet8 instead. Versions matching '^23\.' map to -CliVersionNet6; others map to -CliVersionNet8. .PARAMETER ToolBase Tool root directory. Forwarded to Install-CpmfUipsPackCommandLineTool. Defaults to %LOCALAPPDATA%\cpmf\tools. .PARAMETER ToolBasePath Canonical tool root directory. Same as -ToolBase; kept for the shared path-var naming convention. .PARAMETER ProjectVersion Write this exact version string to project.json before packing, instead of computing an auto-bump. Useful in CI pipelines that derive the version from a git tag (e.g. GITHUB_REF_NAME). Implies -NoBump for subsequent targets. On pack failure the original version is restored. .PARAMETER Backend Which tool performs the pack. Valid values: 'workflowcompiler' (default), 'uipcli', 'uipathcli'. 'workflowcompiler' invokes UiPath.WorkflowCompiler directly — the engine the other two drive internally. It is the only backend that can control --include-sources (uipcli hardcodes it to true, so packages built through uipcli ship every .cs file in plaintext under content/) and the only one that can override the package id without editing project.json. It is net8-only and cannot be combined with -MultiTfm or -Targets net6. 'uipcli' and 'uipathcli' ignore every package identity parameter and both source-inclusion settings, because they expose no such options; a warning is emitted when they are supplied anyway. Use them for net6 builds and for -MultiTfm, which the single net8 compiler cannot serve. .PARAMETER PublisherProps Path to a publisher props file (MSBuild-style XML) supplying shared package identity and pack policy: PublisherAuthors, PublisherTags, PublisherIconUrl, PublisherProjectUrl, PublisherRepositoryType, and the PublisherPack* policy group. Explicit parameters always win over the props file, which in turn wins over env vars and config files. .PARAMETER PackageId Package id written to the nuspec, overriding project.json's "name" without editing the file. Workflowcompiler backend only. .PARAMETER PackageAuthor Package author. Defaults to PublisherAuthors from the props file. .PARAMETER PackageDescription Package description. .PARAMETER PackageTags Space-separated package tags. Defaults to PublisherTags. .PARAMETER IconUrl Package icon URL. Defaults to PublisherIconUrl. .PARAMETER ProjectUrl Package project URL. Defaults to PublisherProjectUrl. .PARAMETER ReleaseNotes Package release notes. .PARAMETER RepositoryType Repository type stamped into the package. Defaults to PublisherRepositoryType. .PARAMETER RepositoryUrl Repository URL. Derived from the project's git origin remote when omitted. .PARAMETER RepositoryBranch Repository branch. Derived from the project's git HEAD when omitted. .PARAMETER RepositoryCommit Repository commit. Derived from the project's short git HEAD SHA when omitted. .PARAMETER IncludeSources Ship project source files inside the package under content/. Defaults to false — deliberately the opposite of the compiler's own default. Workflow- compiler backend only. .PARAMETER WorkflowCompilerPath Path to UiPath.WorkflowCompiler.dll or .exe. Falls back to the props file's PublisherPackWorkflowCompilerPath, then $env:UIPATH_WORKFLOWCOMPILER_LOCATION. .PARAMETER DotnetPath Dotnet host used to run a .dll compiler. Defaults to the props file's PublisherPackDotnetPath, then 'dotnet'. .PARAMETER ConfigFile Path to a .psd1 config file that supplies default values for any parameter not explicitly passed on the command line. Explicit parameters always win. Supported keys: FeedPath, OutputPath, UipcliArgs, NoBump, SkipInstall, UseWorktree, WorktreeBase, WorktreeSibling, CliVersionNet6, CliVersionNet8, UipcliPathNet6, UipcliPathNet8, Targets, MultiTfm, ToolBase, ToolBasePath, Backend, PublisherProps, PackageId, PackageAuthor, PackageDescription, PackageTags, IconUrl, ProjectUrl, ReleaseNotes, RepositoryType, RepositoryUrl, RepositoryBranch, RepositoryCommit, IncludeSources, WorkflowCompilerPath, DotnetPath. .OUTPUTS [string[]] Full path(s) of the staged .nupkg file(s). .NOTES Set $env:UIPATH_DISABLE_TELEMETRY to any non-empty value (e.g. '1' or 'true') to suppress uipcli telemetry data transmission. The telemetry banner will still be printed — that is expected uipcli 23.x behaviour, not a bug. #> [CmdletBinding(SupportsShouldProcess)] [OutputType([string[]])] param( [string] $ProjectJson = (Join-Path $PSScriptRoot '..\project.json'), [switch] $ShowVersion, [string] $FeedPath = 'C:\Users\Public\nugetfeed', [string] $OutputPath = '', [string[]]$UipcliArgs = @(), [ValidateSet('uipcli', 'uipathcli', 'workflowcompiler')] [string] $Backend = 'workflowcompiler', [switch] $NoBump, [switch] $SkipInstall, [switch] $UseWorktree, [string] $WorktreeBase = [System.IO.Path]::GetTempPath(), [switch] $WorktreeSibling, [string] $CliVersionNet6 = '23.10.2.6', [string] $CliVersionNet8 = '25.10.11', [string] $UipcliPathNet6, [string] $UipcliPathNet8, [string[]]$Targets = @('net8'), [switch] $MultiTfm, [string] $CliVersion = '', # deprecated [string] $ProjectVersion = '', [Alias('ToolBase')] [string] $ToolBasePath = (Join-Path $env:LOCALAPPDATA 'cpmf\tools'), [string] $ConfigFile = '', # ── Publisher identity / workflowcompiler backend ──────────────────── [string] $PublisherProps = '', [string] $PackageId = '', [string] $PackageAuthor = '', [string] $PackageDescription = '', [string] $PackageTags = '', [string] $IconUrl = '', [string] $ProjectUrl = '', [string] $ReleaseNotes = '', [string] $RepositoryType = '', [string] $RepositoryUrl = '', [string] $RepositoryBranch = '', [string] $RepositoryCommit = '', [switch] $IncludeSources, [string] $WorkflowCompilerPath = '', [string] $DotnetPath = '' ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' if ($ShowVersion) { $moduleVersion = (Get-Module CpmfUipsPack).Version Write-Output "CpmfUipsPack $moduleVersion" return } # Deprecated -CliVersion shim if ($PSBoundParameters.ContainsKey('CliVersion') -and $CliVersion -ne '') { Write-Warning "[CpmfUipsPack] -CliVersion is deprecated. Use -CliVersionNet6 or -CliVersionNet8." if ($CliVersion -match '^23\.') { $CliVersionNet6 = $CliVersion } else { $CliVersionNet8 = $CliVersion } } # Apply layered config defaults (user config < env vars < project config) # Explicit command-line parameters always win over all config sources. $cfg = Get-CpmfUipsPackEffectiveConfig -ConfigFile $ConfigFile # Publisher props sit between explicit parameters and the config layers: # explicit parameter > publisher props > env var > repo/user config. # Overlaying onto $cfg gets that ordering for free, because the loops below # only apply $cfg values for parameters the caller did not bind. $publisherPropsPath = if ($PSBoundParameters.ContainsKey('PublisherProps')) { $PublisherProps } elseif ($cfg.ContainsKey('PublisherProps')) { [string]$cfg['PublisherProps'] } else { '' } $publisherPropMap = Get-CpmfUipsPublisherProps -Path $publisherPropsPath if ($publisherPropMap.Count -gt 0) { $propsToParams = [ordered]@{ 'PublisherAuthors' = 'PackageAuthor' 'PublisherTags' = 'PackageTags' 'PublisherIconUrl' = 'IconUrl' 'PublisherProjectUrl' = 'ProjectUrl' 'PublisherRepositoryType' = 'RepositoryType' 'PublisherPackWorkflowCompilerPath' = 'WorkflowCompilerPath' 'PublisherPackDotnetPath' = 'DotnetPath' } foreach ($propName in $propsToParams.Keys) { if ($publisherPropMap.ContainsKey($propName)) { $cfg[$propsToParams[$propName]] = $publisherPropMap[$propName] } } # Booleans arrive as the strings 'true'/'false'; both are truthy as # strings, so parse rather than pass through. if ($publisherPropMap.ContainsKey('PublisherPackIncludeSources')) { $parsed = $false if ([bool]::TryParse($publisherPropMap['PublisherPackIncludeSources'], [ref]$parsed)) { $cfg['IncludeSources'] = $parsed } else { Write-Warning "[PublisherProps] PublisherPackIncludeSources is not a boolean: '$($publisherPropMap['PublisherPackIncludeSources'])' — ignored." } } } foreach ($key in @('FeedPath', 'OutputPath', 'WorktreeBase', 'CliVersionNet6', 'CliVersionNet8', 'UipcliPathNet6', 'UipcliPathNet8', 'ToolBasePath', 'Backend', 'PublisherProps', 'PackageId', 'PackageAuthor', 'PackageDescription', 'PackageTags', 'IconUrl', 'ProjectUrl', 'ReleaseNotes', 'RepositoryType', 'RepositoryUrl', 'RepositoryBranch', 'RepositoryCommit', 'WorkflowCompilerPath', 'DotnetPath')) { if (-not $PSBoundParameters.ContainsKey($key) -and $cfg.ContainsKey($key)) { Set-Variable -Name $key -Value $cfg[$key] } } foreach ($key in @('UipcliArgs', 'Targets')) { if (-not $PSBoundParameters.ContainsKey($key) -and $cfg.ContainsKey($key)) { Set-Variable -Name $key -Value ([string[]]$cfg[$key]) } } foreach ($key in @('NoBump', 'SkipInstall', 'UseWorktree', 'WorktreeSibling', 'MultiTfm', 'IncludeSources')) { if (-not $PSBoundParameters.ContainsKey($key) -and $cfg.ContainsKey($key) -and $cfg[$key]) { Set-Variable -Name $key -Value ([switch]$true) } } # Deprecated CliVersion in config if (-not $PSBoundParameters.ContainsKey('CliVersionNet6') -and -not $PSBoundParameters.ContainsKey('CliVersionNet8') -and $cfg.ContainsKey('CliVersion') -and $cfg['CliVersion'] -ne '') { Write-Warning "[CpmfUipsPack] Config key 'CliVersion' is deprecated. Use 'CliVersionNet6' or 'CliVersionNet8'." $v = $cfg['CliVersion'] if ($v -match '^23\.') { $CliVersionNet6 = $v } else { $CliVersionNet8 = $v } } # Validate -Targets $validTargets = @('net6', 'net8') foreach ($t in $Targets) { if ($t -notin $validTargets) { throw "-Targets contains invalid value '$t'. Valid values: net6, net8" } } if ([string]::IsNullOrWhiteSpace($OutputPath)) { $OutputPath = if ($env:PUBLIC) { Join-Path $env:PUBLIC 'UiPath.CLI.Windows\pack-output' } else { Join-Path ([System.IO.Path]::GetTempPath()) 'UiPath.CLI.Windows\pack-output' } } $resolvedUseWorktree = $UseWorktree -or $WorktreeSibling # The workflowcompiler backend is a single net8 build: it has no net6 # counterpart and cannot feed the multi-TFM merge. if ($Backend -eq 'workflowcompiler') { if ($MultiTfm) { throw "-MultiTfm is not supported with -Backend workflowcompiler (single net8 compiler; nothing to merge). Use -Backend uipcli -Targets net6,net8 -MultiTfm." } if ($Targets -contains 'net6') { throw "-Targets net6 is not supported with -Backend workflowcompiler. Use -Targets net8, or -Backend uipcli for a net6 build." } if ($Targets.Count -gt 1) { throw "-Backend workflowcompiler builds a single target. Use -Targets net8, or -Backend uipcli to build several." } } else { # uipcli and uipathcli expose no package-identity or source-inclusion # options at all, so anything supplied here would be silently dropped and # read as applied. Sources in particular: uipcli always passes # --include-sources True down to the compiler, so the package ships every # .cs file whatever -IncludeSources or PublisherPackIncludeSources say. $ignoredSettings = [System.Collections.Generic.List[string]]::new() foreach ($name in @('PackageId', 'PackageAuthor', 'PackageDescription', 'PackageTags', 'IconUrl', 'ProjectUrl', 'ReleaseNotes', 'RepositoryType', 'RepositoryUrl', 'RepositoryBranch', 'RepositoryCommit')) { if (-not [string]::IsNullOrWhiteSpace((Get-Variable -Name $name -ValueOnly))) { $ignoredSettings.Add("-$name") } } if ($publisherPropMap.Count -gt 0) { $ignoredSettings.Add('-PublisherProps') } $sourcesRequested = $PSBoundParameters.ContainsKey('IncludeSources') -or $publisherPropMap.ContainsKey('PublisherPackIncludeSources') if ($sourcesRequested) { Write-Warning "[Publish] The '$Backend' backend cannot control source inclusion — it has no such option, and always packs project sources into content/. -IncludeSources and PublisherPackIncludeSources are ignored here. Use -Backend workflowcompiler to keep sources out of the package." } if ($ignoredSettings.Count -gt 0) { Write-Warning "[Publish] The '$Backend' backend ignores package identity settings: $(($ignoredSettings | Sort-Object -Unique) -join ', '). Use -Backend workflowcompiler for these to take effect." } } Test-CpmfUipsPackPrerequisites ` -RequireGit:$resolvedUseWorktree ` -RequireDotnetCli:($Targets -contains 'net8') ` -ToolBase $ToolBasePath $ProjectJson = (Resolve-Path $ProjectJson).Path $ProjectRoot = Split-Path $ProjectJson -Parent # ── workflowcompiler backend: resolve the binary and assemble metadata ──── $workflowCompiler = @{} $packOptions = @{} $packSettings = @{} if ($Backend -eq 'workflowcompiler') { $workflowCompiler = Resolve-CpmfUipsWorkflowCompiler ` -WorkflowCompilerPath $WorkflowCompilerPath ` -PublisherProps $publisherPropMap ` -DotnetPath $DotnetPath # Repository fields are per-repo, computed fresh from git unless supplied. $gitMeta = @{} if ([string]::IsNullOrWhiteSpace($RepositoryUrl) -or [string]::IsNullOrWhiteSpace($RepositoryBranch) -or [string]::IsNullOrWhiteSpace($RepositoryCommit)) { $gitMeta = Get-CpmfUipsGitMetadata -Path $ProjectRoot } foreach ($key in @('RepositoryUrl', 'RepositoryBranch', 'RepositoryCommit')) { if ([string]::IsNullOrWhiteSpace((Get-Variable -Name $key -ValueOnly)) -and $gitMeta.ContainsKey($key)) { Set-Variable -Name $key -Value $gitMeta[$key] } } # Copyright and licence live in the props file for the MSBuild edition # only — the compiler's pack-options model has no field for either. $packOptions = @{ id = $PackageId author = $PackageAuthor description = $PackageDescription releaseNotes = $ReleaseNotes tags = $PackageTags iconUrl = $IconUrl projectUrl = $ProjectUrl repositoryType = $RepositoryType repositoryUrl = $RepositoryUrl repositoryBranch = $RepositoryBranch repositoryCommit = $RepositoryCommit } $packSettings = @{ IncludeSources = [bool]$IncludeSources LogLevel = 'Warning' SkipAnalyze = $false SkipValidate = $false NoOptimizeDeps = $false SplitPackages = $false ExcludeConfiguredSources = $false PolicyFileType = 'Default' } # PublisherPack* policy defaults, when the props file supplies them. $policyMap = [ordered]@{ 'PublisherPackSkipAnalyze' = 'SkipAnalyze' 'PublisherPackSkipValidate' = 'SkipValidate' 'PublisherPackNoOptimizeDeps' = 'NoOptimizeDeps' 'PublisherPackSplitPackages' = 'SplitPackages' 'PublisherPackExcludeConfiguredSources' = 'ExcludeConfiguredSources' } foreach ($propName in $policyMap.Keys) { if ($publisherPropMap.ContainsKey($propName)) { $parsed = $false if ([bool]::TryParse($publisherPropMap[$propName], [ref]$parsed)) { $packSettings[$policyMap[$propName]] = $parsed } else { Write-Warning "[PublisherProps] $propName is not a boolean: '$($publisherPropMap[$propName])' — ignored." } } } foreach ($pair in @(@('PublisherPackLogLevel', 'LogLevel'), @('PublisherPackPolicyFileType', 'PolicyFileType'))) { if ($publisherPropMap.ContainsKey($pair[0])) { $packSettings[$pair[1]] = $publisherPropMap[$pair[0]] } } } # Pre-install all requested CLI versions before acquiring the lock. # The workflowcompiler backend has nothing to install — the binary is # supplied by configuration, never downloaded. if (-not $SkipInstall -and $Backend -ne 'workflowcompiler') { if ($Backend -eq 'uipathcli') { Install-UipathcliTool -ToolBasePath $ToolBasePath } else { foreach ($target in $Targets) { $cliVer = if ($target -eq 'net6') { $CliVersionNet6 } else { $CliVersionNet8 } $uipcliPath = if ($target -eq 'net6') { $UipcliPathNet6 } else { $UipcliPathNet8 } Install-CpmfUipsPackCommandLineTool -CliVersion $cliVer -UipcliPath $uipcliPath -ToolBasePath $ToolBasePath } } } $lockFile = Join-Path $ProjectRoot '.uipath-pack.lock' Invoke-WithFileLock -LockFile $lockFile -ScriptBlock { if ($resolvedUseWorktree) { $repoRoot = git -C $ProjectRoot rev-parse --show-toplevel 2>$null if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($repoRoot)) { throw "Cannot use -UseWorktree: $ProjectRoot is not inside a git repository" } $repoRoot = $repoRoot.Trim() -replace '/', '\' if ($WorktreeSibling) { $WorktreeBase = Split-Path $repoRoot -Parent } $relativeProjectJson = $ProjectJson.Substring($repoRoot.Length).TrimStart('\', '/') $worktreePath = Get-GitWorktreePath ` -ProjectJson $ProjectJson ` -RepoRoot $repoRoot ` -WorktreeBase $WorktreeBase Invoke-GitWorktree -RepoRoot $repoRoot -WorktreePath $worktreePath -ScriptBlock { param($wt) $wtProjectJson = Join-Path $wt $relativeProjectJson Invoke-MultiTargetPack ` -ProjectJson $wtProjectJson ` -FeedPath $FeedPath ` -UipcliArgs $UipcliArgs ` -Backend $Backend ` -NoBump:$NoBump ` -ProjectVersion $ProjectVersion ` -Targets $Targets ` -CliVersionNet6 $CliVersionNet6 ` -CliVersionNet8 $CliVersionNet8 ` -UipcliPathNet6 $UipcliPathNet6 ` -UipcliPathNet8 $UipcliPathNet8 ` -MultiTfm:$MultiTfm ` -ToolBasePath $ToolBasePath ` -OutputPath $OutputPath ` -WorkflowCompiler $workflowCompiler ` -PackOptions $packOptions ` -PackSettings $packSettings } } else { Write-Output (Invoke-MultiTargetPack ` -ProjectJson $ProjectJson ` -FeedPath $FeedPath ` -UipcliArgs $UipcliArgs ` -Backend $Backend ` -NoBump:$NoBump ` -ProjectVersion $ProjectVersion ` -Targets $Targets ` -CliVersionNet6 $CliVersionNet6 ` -CliVersionNet8 $CliVersionNet8 ` -UipcliPathNet6 $UipcliPathNet6 ` -UipcliPathNet8 $UipcliPathNet8 ` -MultiTfm:$MultiTfm ` -ToolBasePath $ToolBasePath ` -OutputPath $OutputPath ` -WorkflowCompiler $workflowCompiler ` -PackOptions $packOptions ` -PackSettings $packSettings) } } # end Invoke-WithFileLock } # --------------------------------------------------------------------------- # Internal helper — orchestrates version bump + one-or-more PackAndStage calls # + optional MultiTfm merge. # --------------------------------------------------------------------------- function Invoke-MultiTargetPack { [CmdletBinding(SupportsShouldProcess)] [OutputType([string[]])] param( [string] $ProjectJson, [string] $FeedPath, [string[]] $UipcliArgs, [string] $Backend = 'uipcli', [switch] $NoBump, [string] $ProjectVersion = '', [string[]] $Targets, [string] $CliVersionNet6, [string] $CliVersionNet8, [string] $UipcliPathNet6, [string] $UipcliPathNet8, [switch] $MultiTfm, [string] $ToolBasePath, [string] $OutputPath, [hashtable]$WorkflowCompiler = @{}, [hashtable]$PackOptions = @{}, [hashtable]$PackSettings = @{} ) $results = [System.Collections.Generic.List[string]]::new() $isFirstTarget = $true # Computed once — independent of uipcli version $uipathcliExe = Join-Path $ToolBasePath 'uipathcli\uipath.exe' foreach ($target in $Targets) { $cliVer = if ($target -eq 'net6') { $CliVersionNet6 } else { $CliVersionNet8 } $uipcliPath = if ($target -eq 'net6') { $UipcliPathNet6 } else { $UipcliPathNet8 } $p = Get-CpmfUipsToolPaths -CliVersion $cliVer -UipcliPath $uipcliPath -ToolBase $ToolBasePath # Use a target tag in the filename only when building multiple targets $targetTag = if ($Targets.Count -gt 1) { $target } else { '' } # Version bump runs inside the first Invoke-PackAndStage only $thisBump = if ($isFirstTarget) { $NoBump } else { [switch]$true } $thisVersion = if ($isFirstTarget) { $ProjectVersion } else { '' } $staged = Invoke-PackAndStage ` -ProjectJson $ProjectJson ` -FeedPath $FeedPath ` -UipcliArgs $UipcliArgs ` -Backend $Backend ` -NoBump:$thisBump ` -ProjectVersion $thisVersion ` -UipcliExe $p.UipcliExe ` -UipathcliExe $uipathcliExe ` -TargetTag $targetTag ` -OutputPath $OutputPath ` -WorkflowCompiler $WorkflowCompiler ` -PackOptions $PackOptions ` -PackSettings $PackSettings if ($staged) { $results.Add($staged) } $isFirstTarget = $false } # Multi-TFM merge: combine net8 + net6 builds into one nupkg if ($MultiTfm -and $Targets.Count -eq 2 -and $Targets -contains 'net6' -and $Targets -contains 'net8') { $net8Path = $results | Where-Object { $_ -like '*.net8.nupkg' } | Select-Object -First 1 $net6Path = $results | Where-Object { $_ -like '*.net6.nupkg' } | Select-Object -First 1 if ($net8Path -and $net6Path) { # Output name: strip .net8 infix → <name>.<version>.nupkg $mergedName = [System.IO.Path]::GetFileName($net8Path) -replace '\.net8\.nupkg$', '.nupkg' $mergedPath = Join-Path $FeedPath $mergedName $merged = Invoke-MultiTfmMerge ` -Net8Nupkg $net8Path ` -Net6Nupkg $net6Path ` -OutputPath $mergedPath # Remove the two per-target nupkgs; return merged path only Remove-Item $net8Path, $net6Path -Force -ErrorAction SilentlyContinue $results.Clear() if ($merged) { $results.Add($merged) } } else { Write-Warning "[Publish] -MultiTfm specified but could not locate both net6 and net8 nupkgs for merge." } } elseif ($MultiTfm) { Write-Warning "[Publish] -MultiTfm requires -Targets @('net6','net8') — ignored." } Write-Output ($results.ToArray()) } # --------------------------------------------------------------------------- # Internal helper — version bump + pack + stage, with rollback on failure. # Extracted so both worktree and non-worktree paths share identical logic. # --------------------------------------------------------------------------- function Invoke-PackAndStage { [CmdletBinding(SupportsShouldProcess)] [OutputType([string])] param( [string] $ProjectJson, [string] $FeedPath, [string[]]$UipcliArgs, [string] $Backend = 'uipcli', [switch] $NoBump, [string] $ProjectVersion = '', [string] $UipcliExe, [string] $UipathcliExe = '', [string] $TargetTag = '', [string] $OutputPath, [hashtable]$WorkflowCompiler = @{}, [hashtable]$PackOptions = @{}, [hashtable]$PackSettings = @{} ) # Version bump (capture current for rollback) $versionBumped = $false $previousVersion = Update-CpmfUipsPackProjectVersion -ProjectJson $ProjectJson -NoBump if (-not $NoBump) { if ($ProjectVersion -ne '') { $newVersion = Update-CpmfUipsPackProjectVersion -ProjectJson $ProjectJson -ProjectVersion $ProjectVersion } else { $newVersion = Update-CpmfUipsPackProjectVersion -ProjectJson $ProjectJson } Write-Verbose "[Publish] Version: $previousVersion → $newVersion" $versionBumped = $true } else { Write-Verbose "[Publish] Version bump skipped (-NoBump). Current: $previousVersion" } if ([string]::IsNullOrWhiteSpace($OutputPath)) { $OutputPath = if ($env:PUBLIC) { Join-Path $env:PUBLIC 'UiPath.CLI.Windows\pack-output' } else { [System.IO.Path]::GetTempPath() } } $TempOutputDir = Join-Path $OutputPath ([System.Guid]::NewGuid().ToString()) try { $null = New-Item -ItemType Directory -Path $TempOutputDir -Force if ($PSCmdlet.ShouldProcess($ProjectJson, "Pack with $Backend and stage to feed")) { $label = if ($TargetTag) { " [$TargetTag]" } else { '' } $projectName = [System.IO.Path]::GetFileName((Split-Path $ProjectJson -Parent)) # The package version is only known here, after the bump. $effectivePackOptions = @{} foreach ($k in $PackOptions.Keys) { $effectivePackOptions[$k] = $PackOptions[$k] } $effectivePackOptions['version'] = if ($versionBumped) { $newVersion } else { $previousVersion } Write-Progress -Activity "CpmfUipsPack$label" -Status "Packing $projectName …" -PercentComplete 10 $exitCode = Invoke-CliBackend ` -Op pack ` -Backend $Backend ` -UipcliExe $UipcliExe ` -UipathcliExe $UipathcliExe ` -ProjectJson $ProjectJson ` -OutputDir $TempOutputDir ` -ExtraArgs $UipcliArgs ` -WorkflowCompiler $WorkflowCompiler ` -PackOptions $effectivePackOptions ` -PackSettings $PackSettings Write-Progress -Activity "CpmfUipsPack$label" -Completed if ($exitCode -ne 0) { throw "$Backend pack failed (exit $exitCode)" } $nupkg = Get-ChildItem -Path $TempOutputDir -Filter '*.nupkg' | Select-Object -First 1 if (-not $nupkg) { throw "No .nupkg found in $TempOutputDir after pack" } $null = New-Item -ItemType Directory -Path $FeedPath -Force $destName = if ($TargetTag) { $nupkg.Name -replace '\.nupkg$', ".$TargetTag.nupkg" } else { $nupkg.Name } $dest = Join-Path $FeedPath $destName Write-Progress -Activity "CpmfUipsPack$label" -Status "Staging $destName …" -PercentComplete 90 Copy-Item -Path $nupkg.FullName -Destination $dest -Force Write-Progress -Activity "CpmfUipsPack$label" -Completed Write-Verbose "[Publish] Copied: $destName → $FeedPath" Write-Output $dest } } catch { if ($versionBumped) { Write-Warning "[Publish] Pack failed — restoring version to $previousVersion" $raw = Get-Content $ProjectJson -Raw $restored = $raw -replace '("projectVersion"\s*:\s*")[^"]*(")', "`${1}$previousVersion`${2}" [System.IO.File]::WriteAllText($ProjectJson, $restored, (New-Object System.Text.UTF8Encoding $false)) } throw } finally { Remove-Item -Path $TempOutputDir -Recurse -Force -ErrorAction SilentlyContinue } } |