diff --git a/eng/emitters/pipelines/templates/stages/emitter-stages.yml b/eng/emitters/pipelines/templates/stages/emitter-stages.yml index 336c4441685..f2d28266c1b 100644 --- a/eng/emitters/pipelines/templates/stages/emitter-stages.yml +++ b/eng/emitters/pipelines/templates/stages/emitter-stages.yml @@ -329,14 +329,9 @@ stages: - template: /eng/common/pipelines/templates/steps/create-authenticated-npmrc.yml # publish to devops feed - - pwsh: | - $packageFiles = Get-ChildItem -Path . -Filter '*.tgz' - foreach ($file in $packageFiles.Name) { - Write-Host "npm publish $file --verbose --access public" - npm publish $file --verbose --access public - } - displayName: Publish to DevOps feed - workingDirectory: $(buildArtifactsPath)/packages + - template: /eng/emitters/pipelines/templates/steps/publish-to-devops-feed.yml + parameters: + PackagesPath: $(buildArtifactsPath)/packages # If publishing publicly, also publish to npmjs.org - ${{ if eq(parameters.Publish, 'public') }}: diff --git a/eng/emitters/pipelines/templates/steps/publish-to-devops-feed.yml b/eng/emitters/pipelines/templates/steps/publish-to-devops-feed.yml new file mode 100644 index 00000000000..26f25f09e7f --- /dev/null +++ b/eng/emitters/pipelines/templates/steps/publish-to-devops-feed.yml @@ -0,0 +1,24 @@ +parameters: + # Directory containing the packed *.tgz npm packages to publish. + - name: PackagesPath + type: string + + # Condition guarding the publish step. + - name: Condition + type: string + default: succeeded() + +# Publishes all packed *.tgz npm packages found in PackagesPath to the internal +# Azure Artifacts (DevOps) feed. Callers are responsible for authenticating the +# active npm registry beforehand (for example via +# /eng/common/pipelines/templates/steps/create-authenticated-npmrc.yml). +steps: + - pwsh: | + $packageFiles = Get-ChildItem -Path . -Filter '*.tgz' + foreach ($file in $packageFiles.Name) { + Write-Host "npm publish $file --verbose --access public" + npm publish $file --verbose --access public + } + displayName: Publish to DevOps feed + workingDirectory: ${{ parameters.PackagesPath }} + condition: ${{ parameters.Condition }} diff --git a/packages/http-client-csharp/eng/pipeline/publish.yml b/packages/http-client-csharp/eng/pipeline/publish.yml index e37e84583fb..314258ad7e7 100644 --- a/packages/http-client-csharp/eng/pipeline/publish.yml +++ b/packages/http-client-csharp/eng/pipeline/publish.yml @@ -123,6 +123,9 @@ extends: TMPDIR: $(Agent.TempDirectory) NUGET_PACKAGES: $(Agent.TempDirectory)/nuget npm_config_cache: $(Agent.TempDirectory)/npm-cache + # Shared on-disk state for the discrete Prepare / Publish generators / Regenerate steps. + AzureSdkForNetCheckout: $(Agent.TempDirectory)/azure-sdk-for-net + GeneratorPackagesFolder: $(Agent.TempDirectory)/generator-packages steps: - checkout: self @@ -232,14 +235,82 @@ extends: TokenOwners: - Azure + # The azure-sdk-for-net PR is produced by discrete, independently-failing steps that + # share a single on-disk checkout ($(AzureSdkForNetCheckout)) and generator package folder + # ($(GeneratorPackagesFolder)): + # 1. Prepare - clone azure-sdk-for-net and regenerate the unbranded emitter artifacts. + # 2. Build generators - build and pack the Azure (and mgmt) generator from the published + # unbranded generator artifact, stamping the next available feed + # version and pinning the emitter artifacts to it. Only runs when a + # regen was requested, and fails the pipeline on any build error. + # 3. Publish generators - publish the packed generator packages to the ADO feed via the shared + # publish template (/eng/emitters/pipelines/templates/steps/publish-to-devops-feed.yml). + # 4. Regenerate & PR - regenerate the SDK libraries and open the azure-sdk-for-net PR. - task: AzureCLI@2 - displayName: Generate emitter-package.json files & create PR in azure-sdk-for-net + displayName: "Prepare azure-sdk-for-net checkout & regenerate unbranded emitter" inputs: azureSubscription: "AzureSDKEngKeyVault Secrets" scriptType: pscore scriptLocation: scriptPath scriptPath: $(Build.SourcesDirectory)/packages/http-client-csharp/eng/scripts/Submit-AzureSdkForNetPr.ps1 arguments: > + -Phase 'Prepare' + -WorkingDirectory '$(AzureSdkForNetCheckout)' + -DebugFolder '$(GeneratorPackagesFolder)' + -PackageVersion '$(PackageVersion)' + -TypeSpecCommitUrl '$(TypeSpecCommitUrl)' + -AuthToken '$(GH_TOKEN)' + -TypeSpecSourcePackageJsonPath '$(Build.SourcesDirectory)/packages/http-client-csharp/package.json' + -PipelineRunUrl '$(PipelineRunUrl)' + ${{ replace(replace('True', eq(variables['Build.SourceBranchName'], 'main'), ''), 'True', '-Internal') }} + ${{ replace(replace('True', eq(parameters.RegenerateAzureLibraries, false), ''), 'True', '-RegenerateAzureLibraries') }} + ${{ replace(replace('True', eq(parameters.RegenerateMgmtLibraries, false), ''), 'True', '-RegenerateMgmtLibraries') }} + -BuildArtifactsPath '$(Pipeline.Workspace)/build_artifacts_csharp/packages' + -UseTypeSpecNext:$${{ parameters.UseTypeSpecNext }} + + - task: AzureCLI@2 + displayName: "Build Azure/mgmt generators" + condition: and(succeeded(), or(${{ parameters.RegenerateAzureLibraries }}, ${{ parameters.RegenerateMgmtLibraries }})) + inputs: + azureSubscription: "AzureSDKEngKeyVault Secrets" + scriptType: pscore + scriptLocation: scriptPath + scriptPath: $(Build.SourcesDirectory)/packages/http-client-csharp/eng/scripts/Submit-AzureSdkForNetPr.ps1 + arguments: > + -Phase 'BuildGenerators' + -WorkingDirectory '$(AzureSdkForNetCheckout)' + -DebugFolder '$(GeneratorPackagesFolder)' + -PackageVersion '$(PackageVersion)' + -TypeSpecCommitUrl '$(TypeSpecCommitUrl)' + -AuthToken '$(GH_TOKEN)' + -TypeSpecSourcePackageJsonPath '$(Build.SourcesDirectory)/packages/http-client-csharp/package.json' + -PipelineRunUrl '$(PipelineRunUrl)' + ${{ replace(replace('True', eq(variables['Build.SourceBranchName'], 'main'), ''), 'True', '-Internal') }} + ${{ replace(replace('True', eq(parameters.RegenerateAzureLibraries, false), ''), 'True', '-RegenerateAzureLibraries') }} + ${{ replace(replace('True', eq(parameters.RegenerateMgmtLibraries, false), ''), 'True', '-RegenerateMgmtLibraries') }} + -BuildArtifactsPath '$(Pipeline.Workspace)/build_artifacts_csharp/packages' + -UseTypeSpecNext:$${{ parameters.UseTypeSpecNext }} + + # Publish the packed Azure/mgmt generator packages to the ADO feed using the shared publish + # template. The active npm registry was already authenticated for the ADO feed by the + # create-authenticated-npmrc step above, so CI on the resulting azure-sdk-for-net PR can + # restore the emitter dependencies from the feed. + - template: /eng/emitters/pipelines/templates/steps/publish-to-devops-feed.yml + parameters: + PackagesPath: $(GeneratorPackagesFolder) + Condition: and(succeeded(), or(${{ parameters.RegenerateAzureLibraries }}, ${{ parameters.RegenerateMgmtLibraries }})) + + - task: AzureCLI@2 + displayName: "Regenerate SDK libraries & create PR in azure-sdk-for-net" + inputs: + azureSubscription: "AzureSDKEngKeyVault Secrets" + scriptType: pscore + scriptLocation: scriptPath + scriptPath: $(Build.SourcesDirectory)/packages/http-client-csharp/eng/scripts/Submit-AzureSdkForNetPr.ps1 + arguments: > + -Phase 'Regenerate' + -WorkingDirectory '$(AzureSdkForNetCheckout)' + -DebugFolder '$(GeneratorPackagesFolder)' -PackageVersion '$(PackageVersion)' -TypeSpecCommitUrl '$(TypeSpecCommitUrl)' -AuthToken '$(GH_TOKEN)' diff --git a/packages/http-client-csharp/eng/scripts/RegenPreview.psm1 b/packages/http-client-csharp/eng/scripts/RegenPreview.psm1 index d23204e8fe9..ba13a34e447 100644 --- a/packages/http-client-csharp/eng/scripts/RegenPreview.psm1 +++ b/packages/http-client-csharp/eng/scripts/RegenPreview.psm1 @@ -1,5 +1,188 @@ Import-Module "$PSScriptRoot\Generation.psm1" -DisableNameChecking -Force -Global +# Default npm registry used for resolving and publishing generator preview packages. +# This is the public azure-sdk-for-js Azure Artifacts feed. +$script:DefaultGeneratorRegistry = "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/" + +function Get-NextGeneratorVersion { + <# + .SYNOPSIS + Computes the next available prerelease version for a generator package by querying the ADO feed. + + .DESCRIPTION + The branded (@azure-typespec/http-client-csharp) and mgmt (@azure-typespec/http-client-csharp-mgmt) + emitters are published to the Azure DevOps npm feed with a date-stamped alpha version following the + existing format "-alpha..". To avoid publish conflicts (which npm feeds reject + because versions are immutable), this queries the feed for the versions already published today and + returns the next available counter. When nothing is published yet for the package (or today), the + counter starts at 1. + + .PARAMETER PackageName + The npm package name to query (e.g. "@azure-typespec/http-client-csharp"). + + .PARAMETER BaseVersion + The base "X.Y.Z" version (without prerelease suffix) taken from the package's package.json. + + .PARAMETER Registry + The ADO npm feed to query. + + .PARAMETER NpmrcPath + Optional path to an authenticated .npmrc file used to query the feed. + #> + param( + [Parameter(Mandatory=$true)] + [string]$PackageName, + + [Parameter(Mandatory=$true)] + [string]$BaseVersion, + + [Parameter(Mandatory=$true)] + [string]$Registry, + + [Parameter(Mandatory=$false)] + [string]$NpmrcPath + ) + + $viewArgs = @("view", $PackageName, "versions", "--json", "--registry", $Registry) + if ($NpmrcPath -and (Test-Path $NpmrcPath)) { + $viewArgs += @("--userconfig", $NpmrcPath) + } + + Write-Host "Querying $Registry for published versions of $PackageName..." -ForegroundColor Gray + $viewOutput = & npm @viewArgs 2>&1 + $publishedVersions = @() + if ($LASTEXITCODE -eq 0) { + try { + $parsed = ($viewOutput | Out-String).Trim() | ConvertFrom-Json + if ($null -ne $parsed) { + # `npm view` returns a bare string when only a single version exists, otherwise an array. + $publishedVersions = @($parsed) + } + } + catch { + $publishedVersions = @() + } + } + elseif (($viewOutput | Out-String) -notmatch "E404") { + # A 404 simply means the package has never been published; any other failure is unexpected. + Write-Host ($viewOutput | Out-String) -ForegroundColor Yellow + throw "Failed to query published versions of $PackageName from $Registry" + } + + $dateStamp = Get-Date -Format 'yyyyMMdd' + $prefix = "$BaseVersion-alpha.$dateStamp" + $counterPattern = "^" + [regex]::Escape($prefix) + "\.(\d+)$" + $maxCounter = 0 + foreach ($version in $publishedVersions) { + if ($version -match $counterPattern) { + $counter = [int]$Matches[1] + if ($counter -gt $maxCounter) { + $maxCounter = $counter + } + } + } + + $nextVersion = "$prefix.$($maxCounter + 1)" + Write-Host " Next available version for ${PackageName}: $nextVersion" -ForegroundColor Green + return $nextVersion +} + +function Update-EmitterPackageArtifact { + <# + .SYNOPSIS + Updates an eng-folder emitter-package.json (and its lock file) to reference a generator package. + + .DESCRIPTION + By default the emitter dependency is pinned to the locally built .tgz using a "file:" path (used + for local validation loops that clean up afterwards). When a publish registry and version are + provided, the dependency is instead pinned to the published version resolved from the registry, so + the committed artifacts can be restored by CI in the resulting azure-sdk-for-net PR. + + .PARAMETER EmitterJsonPath + Path to the emitter-package.json file to update. + + .PARAMETER LockJsonPath + Path to the emitter-package-lock.json file to update. + + .PARAMETER PackagePath + Path to the locally built .tgz package (used for the default "file:" mode). + + .PARAMETER PackageName + The npm package name to reference (required for published mode). + + .PARAMETER PublishVersion + The published version to pin to (when publishing). + + .PARAMETER Registry + The npm registry to resolve the published package from. + #> + param( + [Parameter(Mandatory=$true)] + [string]$EmitterJsonPath, + + [Parameter(Mandatory=$true)] + [string]$LockJsonPath, + + [Parameter(Mandatory=$false)] + [string]$PackagePath, + + [Parameter(Mandatory=$false)] + [string]$PackageName, + + [Parameter(Mandatory=$false)] + [string]$PublishVersion, + + [Parameter(Mandatory=$false)] + [string]$Registry = $script:DefaultGeneratorRegistry + ) + + $ErrorActionPreference = 'Stop' + + $usePublishedVersion = [bool]$PublishVersion + if ($usePublishedVersion -and -not $PackageName) { + throw "PackageName is required when PublishVersion is specified." + } + if (-not $usePublishedVersion -and -not $PackagePath) { + throw "PackagePath is required when PublishVersion is not specified." + } + + $tempDir = Join-Path ([System.IO.Path]::GetDirectoryName($EmitterJsonPath)) "temp-emitter-package-update-$([System.Guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + + # Point the default registry at the public feed so dependency resolution does not fall back to + # the authenticated machine-global proxy, which fails with E401 for @typespec/@azure-tools packages. + Set-Content (Join-Path $tempDir ".npmrc") "registry=$Registry`n" -Encoding utf8 + + try { + $tempPackageJson = Join-Path $tempDir "package.json" + Copy-Item $EmitterJsonPath $tempPackageJson -Force + + Push-Location $tempDir + try { + if ($usePublishedVersion) { + Invoke "npm install $PackageName@$PublishVersion --save-exact --package-lock-only --registry $Registry" $tempDir + } else { + Invoke "npm install `"`"file:$PackagePath`"`" --package-lock-only" $tempDir + } + if ($LASTEXITCODE -ne 0) { + throw "Failed to update emitter package artifacts for $EmitterJsonPath" + } + + Copy-Item $tempPackageJson $EmitterJsonPath -Force + $lockFile = Join-Path $tempDir "package-lock.json" + if (Test-Path $lockFile) { + Copy-Item $lockFile $LockJsonPath -Force + } + } + finally { + Pop-Location + } + } + finally { + Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue + } +} + function Update-GeneratorPackage { <# .SYNOPSIS @@ -33,6 +216,19 @@ function Update-GeneratorPackage { .PARAMETER UseNpmCi If true, runs 'npm install --package-lock-only && npm ci' instead of 'npm install'. + + .PARAMETER PublishRegistry + When provided, dependencies are pinned to their published versions (see PublishedDependencyVersions) + instead of a local "file:" path, and the packed .tgz is published to this registry so the resulting + emitter artifacts reference a version that CI can restore. + + .PARAMETER PublishedDependencyVersions + Hashtable of dependency name -> published version string. Used in publish mode to pin each dependency + to the exact version already published to the feed. Dependencies not listed here fall back to + $LocalVersion for backward compatibility. + + .PARAMETER NpmrcPath + Optional path to an authenticated .npmrc file used when publishing to $PublishRegistry. #> param( [Parameter(Mandatory=$true)] @@ -51,11 +247,34 @@ function Update-GeneratorPackage { [string]$DebugFolder, [Parameter(Mandatory=$false)] - [bool]$UseNpmCi = $false + [bool]$UseNpmCi = $false, + + [Parameter(Mandatory=$false)] + [string]$PublishRegistry, + + [Parameter(Mandatory=$false)] + [hashtable]$PublishedDependencyVersions = @{}, + + [Parameter(Mandatory=$false)] + [string]$NpmrcPath ) $ErrorActionPreference = 'Stop' + # When publishing, dependencies must reference the published version so the packed (and published) + # package does not carry host-only "file:" references that consumers cannot restore. + $usePublishedDependencies = [bool]$PublishRegistry + + # Resolves the published version to pin a dependency to: the explicit per-dependency version when + # provided, otherwise the package's own version (backward-compatible default). + $resolvePublishedVersion = { + param($DependencyName) + if ($PublishedDependencyVersions.ContainsKey($DependencyName)) { + return $PublishedDependencyVersions[$DependencyName] + } + return $LocalVersion + } + $packageJsonPath = Join-Path $GeneratorPath "package.json" $originalPackageJson = Get-Content $packageJsonPath -Raw @@ -67,18 +286,30 @@ function Update-GeneratorPackage { foreach ($dep in $Dependencies.GetEnumerator()) { if ($packageJson.dependencies -and $packageJson.dependencies.PSObject.Properties[$dep.Key]) { - $packageJson.dependencies.($dep.Key) = "file:$($dep.Value)" + if ($usePublishedDependencies) { + $packageJson.dependencies.($dep.Key) = (& $resolvePublishedVersion $dep.Key) + } else { + $packageJson.dependencies.($dep.Key) = "file:$($dep.Value)" + } } } foreach ($dep in $DevDependencies.GetEnumerator()) { if ($packageJson.devDependencies -and $packageJson.devDependencies.PSObject.Properties[$dep.Key]) { - $packageJson.devDependencies.($dep.Key) = "file:$($dep.Value)" + if ($usePublishedDependencies) { + $packageJson.devDependencies.($dep.Key) = (& $resolvePublishedVersion $dep.Key) + } else { + $packageJson.devDependencies.($dep.Key) = "file:$($dep.Value)" + } } } $packageJson | ConvertTo-Json -Depth 100 | Set-Content $packageJsonPath -Encoding UTF8 - Write-Host " Updated dependencies to local packages" -ForegroundColor Green + if ($usePublishedDependencies) { + Write-Host " Updated dependencies to published versions" -ForegroundColor Green + } else { + Write-Host " Updated dependencies to local packages" -ForegroundColor Green + } } # Step 2: Install dependencies, clean, and build. @@ -156,7 +387,12 @@ function Update-GeneratorPackage { Move-Item $sourcePath $destPath -Force Write-Host " Package created: $packageFile" -ForegroundColor Green - + + # NOTE: publishing the packed .tgz to the feed is handled by the pipeline via the shared + # publish template (/eng/emitters/pipelines/templates/steps/publish-to-devops-feed.yml), not + # here. This function only stamps the next available feed version and packs the .tgz so the + # downstream emitter artifacts can reference a restorable published version. + return $destPath } finally { @@ -198,6 +434,26 @@ function Update-MgmtGenerator { .PARAMETER LocalVersion The version string to use for the local package (e.g., "1.0.0-alpha.20250127.abc123"). + + .PARAMETER PublishRegistry + When provided, the mgmt (and its local dependencies) are pinned to the published version and the + packed mgmt package is published to this registry. The emitter-package.json artifact then + references the published version instead of a host-only "file:" path so CI can restore it. + + .PARAMETER PublishVersion + In publish mode, the version to stamp and publish the mgmt npm emitter package with (typically the + next available version queried from the ADO feed). Defaults to $LocalVersion when not provided. + + .PARAMETER AzureVersion + The published version of the Azure emitter dependency (@azure-typespec/http-client-csharp). Used to + locate the packed Azure .tgz and to pin the mgmt dependency in publish mode. Defaults to $LocalVersion. + + .PARAMETER UnbrandedVersion + The published version of the unbranded emitter dependency (@typespec/http-client-csharp). Used to + locate the packed unbranded .tgz and to pin the mgmt dependency in publish mode. Defaults to $LocalVersion. + + .PARAMETER NpmrcPath + Optional path to an authenticated .npmrc file used when publishing to $PublishRegistry. #> param( [Parameter(Mandatory=$true)] @@ -207,17 +463,39 @@ function Update-MgmtGenerator { [string]$DebugFolder, [Parameter(Mandatory=$true)] - [string]$LocalVersion + [string]$LocalVersion, + + [Parameter(Mandatory=$false)] + [string]$PublishRegistry, + + [Parameter(Mandatory=$false)] + [string]$PublishVersion, + + [Parameter(Mandatory=$false)] + [string]$AzureVersion, + + [Parameter(Mandatory=$false)] + [string]$UnbrandedVersion, + + [Parameter(Mandatory=$false)] + [string]$NpmrcPath ) $ErrorActionPreference = 'Stop' + # In publish mode each package carries its own (feed-queried) version, so the Azure and unbranded + # dependencies must be located and pinned by their respective published versions. When not publishing, + # all three share $LocalVersion for the host-only "file:" validation loop. + $mgmtNpmVersion = if ($PublishVersion) { $PublishVersion } else { $LocalVersion } + $azureVer = if ($AzureVersion) { $AzureVersion } else { $LocalVersion } + $unbrandedVer = if ($UnbrandedVersion) { $UnbrandedVersion } else { $LocalVersion } + # Derive all paths from EngFolder $mgmtGeneratorPath = Join-Path $EngFolder "packages" "http-client-csharp-mgmt" # Package paths come from debug folder - $azurePackageName = "azure-typespec-http-client-csharp-$LocalVersion.tgz" - $unbrandedPackageName = "typespec-http-client-csharp-$LocalVersion.tgz" + $azurePackageName = "azure-typespec-http-client-csharp-$azureVer.tgz" + $unbrandedPackageName = "typespec-http-client-csharp-$unbrandedVer.tgz" $azurePackagePath = Join-Path $DebugFolder $azurePackageName $unbrandedPackagePath = Join-Path $DebugFolder $unbrandedPackageName @@ -232,6 +510,9 @@ function Update-MgmtGenerator { Write-Host "Azure package: $azurePackagePath" -ForegroundColor Gray Write-Host "Unbranded package: $unbrandedPackagePath" -ForegroundColor Gray Write-Host "Local version: $LocalVersion" -ForegroundColor Gray + if ($PublishRegistry) { + Write-Host "Mgmt emitter publish version: $mgmtNpmVersion" -ForegroundColor Gray + } Write-Host "" # Use shared helper to build and package the mgmt generator. @@ -245,54 +526,38 @@ function Update-MgmtGenerator { '@azure-typespec/http-client-csharp' = $azurePackagePath '@typespec/http-client-csharp' = $unbrandedPackagePath } ` - -LocalVersion $LocalVersion ` + -LocalVersion $mgmtNpmVersion ` -DebugFolder $DebugFolder ` - -UseNpmCi $false + -UseNpmCi $false ` + -PublishRegistry $PublishRegistry ` + -PublishedDependencyVersions @{ + '@azure-typespec/http-client-csharp' = $azureVer + '@typespec/http-client-csharp' = $unbrandedVer + } ` + -NpmrcPath $NpmrcPath # Update eng folder mgmt emitter package artifacts. # The emitter package only declares @azure-typespec/http-client-csharp-mgmt directly; - # the Azure + unbranded packages are pulled in transitively from the mgmt tgz, which - # now points at the local file: packages. Installing the local mgmt tgz regenerates - # the lock file with the full local dependency graph. + # the Azure + unbranded packages are pulled in transitively from the mgmt package. When + # publishing, the emitter references the published mgmt version so CI can restore it; + # otherwise it points at the local mgmt tgz for host-only validation loops. Write-Host "Updating mgmt emitter package artifacts..." -ForegroundColor Gray - + $mgmtEmitterJson = Join-Path $EngFolder "azure-typespec-http-client-csharp-mgmt-emitter-package.json" - - # Regenerate the package-lock.json with the full (local) dependency graph - $tempDir = Join-Path $EngFolder "temp-mgmt-package-update" - New-Item -ItemType Directory -Path $tempDir -Force | Out-Null - - # Point the default registry at the public azure-sdk-for-js feed so dependency - # resolution doesn't fall back to the authenticated machine-global proxy - # (packagefeedproxy), which fails with E401 for @typespec/@azure-tools packages. - Set-Content (Join-Path $tempDir ".npmrc") "registry=https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/`n" -Encoding utf8 - - try { - $tempPackageJson = Join-Path $tempDir "package.json" - - Copy-Item $mgmtEmitterJson $tempPackageJson -Force - - Push-Location $tempDir - try { - # Install the mgmt package and regenerate lock file with both dependencies - Invoke "npm install `"`"file:$mgmtPackagePath`"`" --package-lock-only" $tempDir - - Copy-Item $tempPackageJson $mgmtEmitterJson -Force - $lockFile = Join-Path $tempDir "package-lock.json" - if (Test-Path $lockFile) { - $mgmtLockJson = Join-Path $EngFolder "azure-typespec-http-client-csharp-mgmt-emitter-package-lock.json" - Copy-Item $lockFile $mgmtLockJson -Force - } - - Write-Host " Mgmt emitter package artifacts updated" -ForegroundColor Green - } - finally { - Pop-Location - } + $mgmtLockJson = Join-Path $EngFolder "azure-typespec-http-client-csharp-mgmt-emitter-package-lock.json" + + $updateEmitterArgs = @{ + EmitterJsonPath = $mgmtEmitterJson + LockJsonPath = $mgmtLockJson + PackagePath = $mgmtPackagePath } - finally { - Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue + if ($PublishRegistry) { + $updateEmitterArgs.PackageName = '@azure-typespec/http-client-csharp-mgmt' + $updateEmitterArgs.PublishVersion = $mgmtNpmVersion + $updateEmitterArgs.Registry = $PublishRegistry } + Update-EmitterPackageArtifact @updateEmitterArgs + Write-Host " Mgmt emitter package artifacts updated" -ForegroundColor Green Write-Host "" @@ -329,6 +594,18 @@ function Update-AzureGenerator { .PARAMETER LocalVersion The version string to use for the local package (e.g., "1.0.0-alpha.20250127.abc123"). + + .PARAMETER PublishRegistry + When provided, the Azure generator is pinned to the published unbranded version and the packed + Azure package is published to this registry so downstream emitter artifacts can reference a + restorable published version instead of a host-only "file:" path. + + .PARAMETER PublishVersion + In publish mode, the version to stamp and publish the Azure npm emitter package with (typically the + next available version queried from the ADO feed). Defaults to $LocalVersion when not provided. + + .PARAMETER NpmrcPath + Optional path to an authenticated .npmrc file used when publishing to $PublishRegistry. #> param( [Parameter(Mandatory=$true)] @@ -344,23 +621,43 @@ function Update-AzureGenerator { [string]$PackagesDataPropsPath, [Parameter(Mandatory=$true)] - [string]$LocalVersion + [string]$LocalVersion, + + [Parameter(Mandatory=$false)] + [string]$PublishRegistry, + + [Parameter(Mandatory=$false)] + [string]$PublishVersion, + + [Parameter(Mandatory=$false)] + [string]$NpmrcPath ) $ErrorActionPreference = 'Stop' + # In publish mode the Azure npm emitter is stamped with its own (feed-queried) version, while its + # unbranded dependency is pinned to the already-published unbranded version ($LocalVersion). When not + # publishing, the package keeps $LocalVersion for the host-only "file:" validation loop. + $azureNpmVersion = if ($PublishVersion) { $PublishVersion } else { $LocalVersion } + Write-Host "Azure generator path: $AzureGeneratorPath" -ForegroundColor Gray Write-Host "Unbranded package: $UnbrandedPackagePath" -ForegroundColor Gray Write-Host "Local version: $LocalVersion" -ForegroundColor Gray + if ($PublishRegistry) { + Write-Host "Azure emitter publish version: $azureNpmVersion" -ForegroundColor Gray + } Write-Host "" # Use shared helper to build and package the Azure generator $azurePackagePath = Update-GeneratorPackage ` -GeneratorPath $AzureGeneratorPath ` -Dependencies @{ '@typespec/http-client-csharp' = $UnbrandedPackagePath } ` - -LocalVersion $LocalVersion ` + -LocalVersion $azureNpmVersion ` -DebugFolder $DebugFolder ` - -UseNpmCi $true + -UseNpmCi $true ` + -PublishRegistry $PublishRegistry ` + -PublishedDependencyVersions @{ '@typespec/http-client-csharp' = $LocalVersion } ` + -NpmrcPath $NpmrcPath # Build and package Azure.Generator NuGet package Write-Host "Packing Azure.Generator NuGet package..." -ForegroundColor Gray @@ -978,4 +1275,4 @@ function Update-AzureSpectorScenarios { return $generationOutput } -Export-ModuleMember -Function "Update-MgmtGenerator", "Update-AzureGenerator", "Filter-LibrariesByGenerator", "Update-OpenAIGenerator", "Add-LocalNuGetSource", "Update-AzureSpectorScenarios" +Export-ModuleMember -Function "Update-MgmtGenerator", "Update-AzureGenerator", "Filter-LibrariesByGenerator", "Update-OpenAIGenerator", "Add-LocalNuGetSource", "Update-AzureSpectorScenarios", "Update-EmitterPackageArtifact", "Get-NextGeneratorVersion" diff --git a/packages/http-client-csharp/eng/scripts/Submit-AzureSdkForNetPr.ps1 b/packages/http-client-csharp/eng/scripts/Submit-AzureSdkForNetPr.ps1 index 0ed2f4152e4..1a3f6bc94d6 100755 --- a/packages/http-client-csharp/eng/scripts/Submit-AzureSdkForNetPr.ps1 +++ b/packages/http-client-csharp/eng/scripts/Submit-AzureSdkForNetPr.ps1 @@ -21,6 +21,16 @@ When specified, builds the management plane emitter locally and regenerates mgmt Path to the build artifacts directory containing the published .tgz and .nupkg files. Required when RegenerateAzureLibraries or RegenerateMgmtLibraries is specified. .PARAMETER PipelineRunUrl The URL of the pipeline run that triggered this PR. When provided, it is included in the PR description for traceability. +.PARAMETER Phase +Which phase of the flow to run. The publish pipeline drives these as discrete, independently-failing steps that share a single on-disk checkout: + - Prepare: Clone azure-sdk-for-net, update the generator version, regenerate the unbranded test projects and emitter-package.json artifacts. + - BuildGenerators: Build and pack the Azure (and mgmt) generator from the published unbranded generator artifact, stamping the next available feed version and pinning the emitter artifacts to it. The packed packages are published to the ADO feed by the pipeline via the shared publish template (/eng/emitters/pipelines/templates/steps/publish-to-devops-feed.yml). + - Regenerate: Regenerate the SDK libraries and open the azure-sdk-for-net PR. +Defaults to 'All', which runs every phase in a single invocation (used for local, non-pipeline runs). +.PARAMETER WorkingDirectory +The azure-sdk-for-net checkout directory shared across phases. When omitted (typically for the 'All' phase), a unique temp directory is created and cleaned up automatically. +.PARAMETER DebugFolder +The directory holding the packed generator packages and NuGet packages shared between the BuildGenerators and Regenerate phases. When omitted, a unique temp directory is derived from WorkingDirectory. #> [CmdletBinding(SupportsShouldProcess = $true)] param( @@ -55,7 +65,17 @@ param( [string]$PipelineRunUrl, [Parameter(Mandatory = $false)] - [switch]$UseTypeSpecNext + [switch]$UseTypeSpecNext, + + [Parameter(Mandatory = $false)] + [ValidateSet('All', 'Prepare', 'BuildGenerators', 'Regenerate')] + [string]$Phase = 'All', + + [Parameter(Mandatory = $false)] + [string]$WorkingDirectory, + + [Parameter(Mandatory = $false)] + [string]$DebugFolder ) # Import the Generation module to use the Invoke helper function @@ -63,6 +83,30 @@ Import-Module (Join-Path $PSScriptRoot "Generation.psm1") -DisableNameChecking - # Import RegenPreview module for Update-AzureGenerator and Update-MgmtGenerator Import-Module (Join-Path $PSScriptRoot "RegenPreview.psm1") -DisableNameChecking -Force +# The locally built Azure/mgmt generator packages are published to the ADO feed by the pipeline via +# the shared publish template (see publish.yml). This script only stamps the next available feed +# version onto the packed packages and pins the regenerated emitter-package.json artifacts to it, so +# CI can restore the emitter dependencies in the resulting azure-sdk-for-net PR. Resolve the registry +# and authenticated .npmrc used to query the feed for the next available version. +$PublishRegistry = $null +$PublishNpmrcPath = $null +if ($RegenerateAzureLibraries -or $RegenerateMgmtLibraries) { + $PublishRegistry = "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-js/npm/registry/" + $resolvedPublishNpmrc = Join-Path $PSScriptRoot "../../.npmrc" + if (Test-Path $resolvedPublishNpmrc) { + $PublishNpmrcPath = (Resolve-Path $resolvedPublishNpmrc).Path + Write-Host "Generator packages will be published to $PublishRegistry using .npmrc at $PublishNpmrcPath" + } else { + Write-Host "Generator packages will be published to $PublishRegistry using ambient npm configuration" + } +} + +# The publish pipeline drives this script as three discrete, independently-failing steps that share a +# single on-disk azure-sdk-for-net checkout (see publish.yml). Resolve which phases this invocation runs. +$runPrepare = $Phase -in @('All', 'Prepare') +$runBuildGenerators = $Phase -in @('All', 'BuildGenerators') +$runRegenerate = $Phase -in @('All', 'Regenerate') + # Set up variables for the PR $RepoOwner = "Azure" $RepoName = "azure-sdk-for-net" @@ -126,22 +170,49 @@ Write-Host "Creating PR in $RepoOwner/$RepoName" Write-Host "Branch: $PRBranch" Write-Host "Title: $PRTitle" -# Create temp folder for repo -$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "azure-sdk-for-net-$(Get-Date -Format 'yyyyMMdd-HHmmss')" -New-Item -ItemType Directory -Path $tempDir -Force | Out-Null -Write-Host "Created temp directory: $tempDir" +# Resolve the shared checkout directory. The publish pipeline passes an explicit, deterministic +# -WorkingDirectory so the Prepare / BuildGenerators / Regenerate steps operate on the same on-disk +# checkout. For a single 'All' invocation (e.g. local runs) fall back to a unique temp directory. +if ($WorkingDirectory) { + $tempDir = $WorkingDirectory +} else { + $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "azure-sdk-for-net-$(Get-Date -Format 'yyyyMMdd-HHmmss')" +} -try { - # Use sparse checkout to clone only the necessary files - # This significantly reduces disk space usage as azure-sdk-for-net is a very large repository - Write-Host "Setting up sparse checkout for azure-sdk-for-net repository..." - - # Initialize empty git repository - git init $tempDir - if ($LASTEXITCODE -ne 0) { - throw "Failed to initialize repository" +# The debug folder holds the packed generator + NuGet packages shared between the BuildGenerators and +# Regenerate phases. Derive a deterministic default from the checkout so both phases agree. +if (-not $DebugFolder) { + $DebugFolder = Join-Path $tempDir ".generator-packages" +} + +if ($runPrepare) { + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + Write-Host "Created temp directory: $tempDir" +} elseif (-not (Test-Path $tempDir)) { + throw "Working directory '$tempDir' does not exist. The Prepare phase must run before the '$Phase' phase." +} + +# Derived paths shared across phases (each phase may run in a separate process, so compute these from +# $tempDir rather than relying on state set by an earlier phase). +$propsFilePath = Join-Path $tempDir "eng/centralpackagemanagement/Directory.Generation.Packages.props" +$packageJsonPath = Join-Path $tempDir "eng/packages/http-client-csharp/package.json" +$httpClientDir = Join-Path $tempDir "eng/packages/http-client-csharp" + +# Whether the unbranded npm install/build succeeded during Prepare. In a split pipeline run the +# Regenerate phase only runs when the Prepare step succeeded (the pipeline fails otherwise), so default +# to true; the 'All' path updates this in the Prepare section below. The Prepare phase persists the +# resolved value to a sibling state file so the later phase processes can honor the same gating. +$installSucceeded = $true +$prepareStateFile = "$tempDir.prepare-state.json" +if (-not $runPrepare -and (Test-Path $prepareStateFile)) { + try { + $installSucceeded = [bool]((Get-Content $prepareStateFile -Raw | ConvertFrom-Json).InstallSucceeded) + } catch { + Write-Warning "Failed to read Prepare state from ${prepareStateFile}: $($_.Exception.Message). Assuming install succeeded." } - +} + +try { Push-Location $tempDir # Set the authentication token for gh CLI early so that scripts invoked @@ -152,51 +223,64 @@ try { # Configure git user for commits in this repository git config user.name "azure-sdk" git config user.email "azuresdk@microsoft.com" - - # Add the remote - git remote add origin "https://github.com/$RepoOwner/$RepoName.git" - if ($LASTEXITCODE -ne 0) { - throw "Failed to add remote" - } - - # Enable sparse checkout with cone mode for better performance - git sparse-checkout init --cone - if ($LASTEXITCODE -ne 0) { - throw "Failed to initialize sparse checkout" - } - - # Set the sparse checkout patterns - only the directories we need - # Note: 'eng' covers eng/packages/http-client-csharp, eng/packages/http-client-csharp-mgmt, and all eng/ artifacts - # Note: 'doc/GeneratorVersions' is needed for regenerating the emitter version dashboard - git sparse-checkout set eng sdk/core/Azure.Core/src/Shared sdk/core/Azure.Core.TestFramework/src doc/GeneratorVersions - if ($LASTEXITCODE -ne 0) { - throw "Failed to set sparse checkout patterns" - } - - # Fetch only the main branch with depth 1 - Write-Host "Fetching $BaseBranch branch with sparse checkout..." - git fetch --depth 1 origin $BaseBranch - if ($LASTEXITCODE -ne 0) { - throw "Failed to fetch repository" - } - - # Checkout the fetched branch - git checkout $BaseBranch - if ($LASTEXITCODE -ne 0) { - throw "Failed to checkout $BaseBranch" - } - # Create a new branch - Write-Host "Creating branch $PRBranch..." - git checkout -b $PRBranch - if ($LASTEXITCODE -ne 0) { - throw "Failed to create branch" + if ($runPrepare) { + # Use sparse checkout to clone only the necessary files + # This significantly reduces disk space usage as azure-sdk-for-net is a very large repository + Write-Host "Setting up sparse checkout for azure-sdk-for-net repository..." + + # Initialize empty git repository + git init $tempDir + if ($LASTEXITCODE -ne 0) { + throw "Failed to initialize repository" + } + + # Add the remote + git remote add origin "https://github.com/$RepoOwner/$RepoName.git" + if ($LASTEXITCODE -ne 0) { + throw "Failed to add remote" + } + + # Enable sparse checkout with cone mode for better performance + git sparse-checkout init --cone + if ($LASTEXITCODE -ne 0) { + throw "Failed to initialize sparse checkout" + } + + # Set the sparse checkout patterns - only the directories we need + # Note: 'eng' covers eng/packages/http-client-csharp, eng/packages/http-client-csharp-mgmt, and all eng/ artifacts + # Note: 'doc/GeneratorVersions' is needed for regenerating the emitter version dashboard + git sparse-checkout set eng sdk/core/Azure.Core/src/Shared sdk/core/Azure.Core.TestFramework/src doc/GeneratorVersions + if ($LASTEXITCODE -ne 0) { + throw "Failed to set sparse checkout patterns" + } + + # Fetch only the main branch with depth 1 + Write-Host "Fetching $BaseBranch branch with sparse checkout..." + git fetch --depth 1 origin $BaseBranch + if ($LASTEXITCODE -ne 0) { + throw "Failed to fetch repository" + } + + # Checkout the fetched branch + git checkout $BaseBranch + if ($LASTEXITCODE -ne 0) { + throw "Failed to checkout $BaseBranch" + } + + # Create a new branch + Write-Host "Creating branch $PRBranch..." + git checkout -b $PRBranch + if ($LASTEXITCODE -ne 0) { + throw "Failed to create branch" + } } + if ($runPrepare) { # Update the dependency in eng/centralpackagemanagement/Directory.Generation.Packages.props + # ($propsFilePath, $packageJsonPath and $httpClientDir are computed once above and shared across phases.) Write-Host "Updating dependency version in eng/centralpackagemanagement/Directory.Generation.Packages.props..." - $propsFilePath = Join-Path $tempDir "eng/centralpackagemanagement/Directory.Generation.Packages.props" - + if (-not (Test-Path $propsFilePath)) { throw "eng/centralpackagemanagement/Directory.Generation.Packages.props not found in the repository" } @@ -222,8 +306,7 @@ try { # Update the dependency in eng/packages/http-client-csharp/package.json Write-Host "Updating dependency version in eng/packages/http-client-csharp/package.json..." - $packageJsonPath = Join-Path $tempDir "eng/packages/http-client-csharp/package.json" - + if (-not (Test-Path $packageJsonPath)) { throw "eng/packages/http-client-csharp/package.json not found in the repository" } @@ -251,9 +334,8 @@ try { # Only run expensive operations if we actually made updates $installSucceeded = $true if ($packageJsonUpdated) { - # Run npm install in the http-client-csharp directory + # Run npm install in the http-client-csharp directory ($httpClientDir is shared, computed above) Write-Host "##[section]Running npm install in eng/packages/http-client-csharp..." - $httpClientDir = Join-Path $tempDir "eng/packages/http-client-csharp" # Update TypeSpec dependencies to @next versions in the Azure emitter package.json if ($UseTypeSpecNext) { @@ -429,136 +511,147 @@ try { } else { Write-Warning "TypeSpecSourcePackageJsonPath not provided or file doesn't exist. Skipping emitter-package.json generation." } - - # Regenerate all SDK libraries that use the unbranded emitter - if ($installSucceeded) { + + # Persist the resolved install state so the later BuildGenerators / Regenerate phases (which may + # run as separate processes) honor the same gating as a single 'All' invocation. + @{ InstallSucceeded = $installSucceeded } | ConvertTo-Json | Set-Content $prepareStateFile -Encoding utf8 + } # end if ($runPrepare) + + # BuildGenerators phase: build and pack the Azure (and mgmt) generator from the published + # unbranded generator artifact, stamping the next available feed version and pinning the emitter + # artifacts to it. This runs as a discrete, fail-hard pipeline step (see publish.yml) so any build + # failure fails the pipeline instead of silently producing an unrestorable PR. The packed packages + # are published to the ADO feed by the pipeline via the shared publish template. + if ($runBuildGenerators -and $installSucceeded -and ($RegenerateAzureLibraries -or $RegenerateMgmtLibraries)) { + $regenScope = @() + if ($RegenerateAzureLibraries) { $regenScope += "Azure data plane" } + if ($RegenerateMgmtLibraries) { $regenScope += "mgmt" } + Write-Host "##[section]Building generators for: $($regenScope -join ', ')..." + + # Locate the unbranded .tgz and .nupkg files from build artifacts + if (-not $BuildArtifactsPath -or -not (Test-Path $BuildArtifactsPath)) { + throw "BuildArtifactsPath is required when RegenerateAzureLibraries or RegenerateMgmtLibraries is specified. Path: $BuildArtifactsPath" + } + + New-Item -ItemType Directory -Path $DebugFolder -Force | Out-Null + + # Find unbranded .tgz from build artifacts + $unbrandedTgz = Get-ChildItem -Path $BuildArtifactsPath -Filter "typespec-http-client-csharp-*.tgz" -Recurse | Select-Object -First 1 + if (-not $unbrandedTgz) { + throw "Could not find unbranded emitter .tgz in build artifacts at: $BuildArtifactsPath" + } + $unbrandedPackagePath = $unbrandedTgz.FullName + Write-Host "Using unbranded package from build artifacts: $unbrandedPackagePath" + + # Copy .nupkg files from build artifacts to debug folder + $nupkgFiles = Get-ChildItem -Path $BuildArtifactsPath -Filter "*.nupkg" -Recurse + foreach ($nupkg in $nupkgFiles) { + Copy-Item $nupkg.FullName -Destination $DebugFolder -Force + Write-Host "Copied NuGet package: $($nupkg.Name)" + } + + # Build and package Azure generator (needed for both Azure data plane and mgmt). + # The Azure generator lives in the same eng/packages/http-client-csharp folder as $httpClientDir, + # and the generation props file is the shared $propsFilePath. + $azureGeneratorPath = $httpClientDir + $packagesDataPropsPath = $propsFilePath + $engFolder = Join-Path $tempDir "eng" + + Write-Host "##[section]Building Azure generator..." + # When publishing, query the ADO feed for the next available version to stamp the Azure + # emitter package with, following the existing "-alpha.." format. + $azurePublishVersion = $null + if ($PublishRegistry) { + $azureBaseVersion = ((Get-Content (Join-Path $azureGeneratorPath "package.json") -Raw | ConvertFrom-Json).version -split '-')[0] + $azurePublishVersion = Get-NextGeneratorVersion ` + -PackageName '@azure-typespec/http-client-csharp' ` + -BaseVersion $azureBaseVersion ` + -Registry $PublishRegistry ` + -NpmrcPath $PublishNpmrcPath + } + $azurePackagePath = Update-AzureGenerator ` + -AzureGeneratorPath $azureGeneratorPath ` + -UnbrandedPackagePath $unbrandedPackagePath ` + -DebugFolder $DebugFolder ` + -PackagesDataPropsPath $packagesDataPropsPath ` + -LocalVersion $PackageVersion ` + -PublishRegistry $PublishRegistry ` + -PublishVersion $azurePublishVersion ` + -NpmrcPath $PublishNpmrcPath + Write-Host "Azure generator built successfully" + + # Update Azure emitter package artifacts. When publishing, reference the published + # version so CI can restore it; otherwise pin to the local tgz via a "file:" path. + Write-Host "Updating Azure emitter package artifacts..." + $azureEmitterJson = Join-Path $engFolder "azure-typespec-http-client-csharp-emitter-package.json" + $azureLockJson = Join-Path $engFolder "azure-typespec-http-client-csharp-emitter-package-lock.json" + + $updateAzureEmitterArgs = @{ + EmitterJsonPath = $azureEmitterJson + LockJsonPath = $azureLockJson + PackagePath = $azurePackagePath + } + if ($PublishRegistry) { + $updateAzureEmitterArgs.PackageName = '@azure-typespec/http-client-csharp' + $updateAzureEmitterArgs.PublishVersion = $azurePublishVersion + $updateAzureEmitterArgs.Registry = $PublishRegistry + } + Update-EmitterPackageArtifact @updateAzureEmitterArgs + + # Add NuGet source for local packages + $nugetConfigPath = Join-Path $tempDir "NuGet.Config" + if (Test-Path $nugetConfigPath) { + Add-LocalNuGetSource -NuGetConfigPath $nugetConfigPath -SourcePath $DebugFolder + } + + # Build and package management plane generator (only when mgmt is requested) + if ($RegenerateMgmtLibraries) { + $mgmtGeneratorPath = Join-Path $tempDir "eng" "packages" "http-client-csharp-mgmt" + if (-not (Test-Path $mgmtGeneratorPath)) { + throw "Management plane generator not found at $mgmtGeneratorPath" + } + Write-Host "##[section]Building management plane generator..." + # When publishing, query the ADO feed for the next available mgmt emitter version. + $mgmtPublishVersion = $null + if ($PublishRegistry) { + $mgmtBaseVersion = ((Get-Content (Join-Path $mgmtGeneratorPath "package.json") -Raw | ConvertFrom-Json).version -split '-')[0] + $mgmtPublishVersion = Get-NextGeneratorVersion ` + -PackageName '@azure-typespec/http-client-csharp-mgmt' ` + -BaseVersion $mgmtBaseVersion ` + -Registry $PublishRegistry ` + -NpmrcPath $PublishNpmrcPath + } + Update-MgmtGenerator ` + -EngFolder $engFolder ` + -DebugFolder $DebugFolder ` + -LocalVersion $PackageVersion ` + -PublishRegistry $PublishRegistry ` + -PublishVersion $mgmtPublishVersion ` + -AzureVersion $azurePublishVersion ` + -UnbrandedVersion $PackageVersion ` + -NpmrcPath $PublishNpmrcPath + Write-Host "Management plane generator built successfully" + } + } + + # Regenerate phase: regenerate all SDK libraries that consume the (now published) emitters. + if ($runRegenerate -and $installSucceeded) { Write-Host "Expanding sparse checkout to include sdk directory for SDK regeneration..." git sparse-checkout add sdk if ($LASTEXITCODE -ne 0) { Write-Warning "Failed to expand sparse checkout. Skipping SDK regeneration." Write-Host "##vso[task.complete result=SucceededWithIssues;]" } else { - # Build the emitter patterns to match in tsp-location.yaml + # Build the emitter patterns to match in tsp-location.yaml. The Azure/mgmt emitter + # artifacts are produced by the BuildGenerators phase and persisted on disk. $emitterPatterns = @("eng/http-client-csharp-emitter-package.json") - - if ($RegenerateAzureLibraries -or $RegenerateMgmtLibraries) { - $regenScope = @() - if ($RegenerateAzureLibraries) { $regenScope += "Azure data plane" } - if ($RegenerateMgmtLibraries) { $regenScope += "mgmt" } - Write-Host "##[section]Building emitters locally for: $($regenScope -join ', ')..." - - # Locate the unbranded .tgz and .nupkg files from build artifacts - if (-not $BuildArtifactsPath -or -not (Test-Path $BuildArtifactsPath)) { - throw "BuildArtifactsPath is required when RegenerateAzureLibraries or RegenerateMgmtLibraries is specified. Path: $BuildArtifactsPath" - } - - $debugFolder = Join-Path ([System.IO.Path]::GetTempPath()) "csharp-debug-$(Get-Date -Format 'yyyyMMdd-HHmmss')" - New-Item -ItemType Directory -Path $debugFolder -Force | Out-Null - - # Find unbranded .tgz from build artifacts - $unbrandedTgz = Get-ChildItem -Path $BuildArtifactsPath -Filter "typespec-http-client-csharp-*.tgz" -Recurse | Select-Object -First 1 - if (-not $unbrandedTgz) { - throw "Could not find unbranded emitter .tgz in build artifacts at: $BuildArtifactsPath" - } - $unbrandedPackagePath = $unbrandedTgz.FullName - Write-Host "Using unbranded package from build artifacts: $unbrandedPackagePath" - - # Copy .nupkg files from build artifacts to debug folder - $nupkgFiles = Get-ChildItem -Path $BuildArtifactsPath -Filter "*.nupkg" -Recurse - foreach ($nupkg in $nupkgFiles) { - Copy-Item $nupkg.FullName -Destination $debugFolder -Force - Write-Host "Copied NuGet package: $($nupkg.Name)" - } - - # Build and package Azure generator (needed for both Azure data plane and mgmt) - $azureGeneratorPath = Join-Path $tempDir "eng" "packages" "http-client-csharp" - $packagesDataPropsPath = Join-Path $tempDir "eng" "centralpackagemanagement" "Directory.Generation.Packages.props" - - Write-Host "##[section]Building Azure generator..." - $previousErrorAction = $ErrorActionPreference - $ErrorActionPreference = "Continue" - try { - $azurePackagePath = Update-AzureGenerator ` - -AzureGeneratorPath $azureGeneratorPath ` - -UnbrandedPackagePath $unbrandedPackagePath ` - -DebugFolder $debugFolder ` - -PackagesDataPropsPath $packagesDataPropsPath ` - -LocalVersion $PackageVersion - Write-Host "Azure generator built successfully" - - # Update Azure emitter package artifacts - Write-Host "Updating Azure emitter package artifacts..." - $engFolder = Join-Path $tempDir "eng" - $azureTempDir = Join-Path $engFolder "temp-azure-package-update" - New-Item -ItemType Directory -Path $azureTempDir -Force | Out-Null - - try { - $azureEmitterJson = Join-Path $engFolder "azure-typespec-http-client-csharp-emitter-package.json" - $tempPackageJson = Join-Path $azureTempDir "package.json" - - Copy-Item $azureEmitterJson $tempPackageJson -Force - - Push-Location $azureTempDir - try { - Invoke "npm install `"`"file:$azurePackagePath`"`" --package-lock-only" $azureTempDir - - Copy-Item $tempPackageJson $azureEmitterJson -Force - $lockFile = Join-Path $azureTempDir "package-lock.json" - if (Test-Path $lockFile) { - $azureLockJson = Join-Path $engFolder "azure-typespec-http-client-csharp-emitter-package-lock.json" - Copy-Item $lockFile $azureLockJson -Force - } - } finally { - Pop-Location - } - } finally { - Remove-Item $azureTempDir -Recurse -Force -ErrorAction SilentlyContinue - } - - if ($RegenerateAzureLibraries) { - $emitterPatterns += "eng/azure-typespec-http-client-csharp-emitter-package.json" - } - - # Add NuGet source for local packages - $nugetConfigPath = Join-Path $tempDir "NuGet.Config" - if (Test-Path $nugetConfigPath) { - Add-LocalNuGetSource -NuGetConfigPath $nugetConfigPath -SourcePath $debugFolder - } - } catch { - Write-Warning "Failed to build Azure generator: $($_.Exception.Message). Continuing without Azure library regeneration." - Write-Host "##vso[task.complete result=SucceededWithIssues;]" - } finally { - $ErrorActionPreference = $previousErrorAction - } - - # Build and package management plane generator (only when mgmt is requested) - if ($RegenerateMgmtLibraries) { - $mgmtGeneratorPath = Join-Path $tempDir "eng" "packages" "http-client-csharp-mgmt" - if (Test-Path $mgmtGeneratorPath) { - Write-Host "##[section]Building management plane generator..." - $previousErrorAction = $ErrorActionPreference - $ErrorActionPreference = "Continue" - try { - $engFolder = Join-Path $tempDir "eng" - Update-MgmtGenerator ` - -EngFolder $engFolder ` - -DebugFolder $debugFolder ` - -LocalVersion $PackageVersion - Write-Host "Management plane generator built successfully" - - $emitterPatterns += "eng/azure-typespec-http-client-csharp-mgmt-emitter-package.json" - } catch { - Write-Warning "Failed to build management plane generator: $($_.Exception.Message). Continuing without mgmt library regeneration." - Write-Host "##vso[task.complete result=SucceededWithIssues;]" - } finally { - $ErrorActionPreference = $previousErrorAction - } - } else { - Write-Host "Management plane generator not found at $mgmtGeneratorPath, skipping..." - } - } + if ($RegenerateAzureLibraries) { + $emitterPatterns += "eng/azure-typespec-http-client-csharp-emitter-package.json" } - + if ($RegenerateMgmtLibraries) { + $emitterPatterns += "eng/azure-typespec-http-client-csharp-mgmt-emitter-package.json" + } + # Discover service directories with tsp-location.yaml referencing any of the matched emitter patterns $tspLocations = Get-ChildItem -Path (Join-Path $tempDir "sdk") -Filter "tsp-location.yaml" -Recurse $serviceDirectories = @() @@ -616,6 +709,7 @@ try { } # Regenerate the emitter version dashboard + if ($runRegenerate) { Write-Host "Regenerating emitter version dashboard..." $dashboardScript = Join-Path $tempDir "doc/GeneratorVersions/Emitter_Version_Dashboard.ps1" & $dashboardScript -RepoRoot $tempDir @@ -753,14 +847,18 @@ try { # Extract PR URL from gh output $prUrl = $ghOutput.Trim() Write-Host "Successfully created PR: $prUrl" + } # end if ($runRegenerate) } catch { Write-Error "Error creating PR: $_" exit 1 } finally { Pop-Location - # Clean up temp directory - if (Test-Path $tempDir) { + # Clean up the shared checkout only when this invocation owns its whole lifecycle. In a split + # pipeline run the Regenerate phase is the last step, so it performs the cleanup; earlier phases + # (Prepare/BuildGenerators) leave the checkout in place for the next step. + if ($Phase -in @('All', 'Regenerate') -and (Test-Path $tempDir)) { Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item $prepareStateFile -Force -ErrorAction SilentlyContinue } }