devops(pipeline): resolve NuGet and npm packages from DevDiv_PublicPackages feed (#3352)

This commit is contained in:
Yury Semikhatsky
2026-09-04 08:37:10 -07:00
committed by GitHub
parent ad8e09c62d
commit bda416ee05
5 changed files with 116 additions and 10 deletions
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
NuGet configuration used only by the Azure Pipelines release build (publish.yml).
Routes all package restores through the DevDiv_PublicPackages Azure Artifacts
feed instead of nuget.org, as required by SFI-ES4.2.4. The pipeline copies it to
the repository root before any dotnet command so every restore discovers it, and
NuGetAuthenticate@1 provides the credentials for the feed.
-->
<configuration>
<packageSources>
<clear />
<add key="DevDiv_PublicPackages" value="https://pkgs.dev.azure.com/devdiv/DevDiv/_packaging/DevDiv_PublicPackages/nuget/v3/index.json" />
</packageSources>
</configuration>
+35 -1
View File
@@ -56,6 +56,17 @@ extends:
displayName: 'Publish Artifact'
targetPath: '$(Build.ArtifactStagingDirectory)'
steps:
- powershell: |
if ($env:CURRENT_BRANCH -notmatch '^v1\.') {
Write-Host "Can only publish from a release tag (v1.*)."
Write-Host "Unexpected ref name: $env:CURRENT_BRANCH"
exit 1
}
env:
CURRENT_BRANCH: ${{ variables['Build.SourceBranchName'] }}
displayName: 'Check the ref is a release tag'
# Allow manual runs on any branch to exercise the build without publishing.
condition: ne(variables['Build.Reason'], 'Manual')
- task: UseDotNet@2
displayName: 'Use .NET 8 SDK'
inputs:
@@ -66,6 +77,29 @@ extends:
inputs:
packageType: sdk
version: 10.x
# All NuGet restores go through the DevDiv_PublicPackages feed instead of
# nuget.org (SFI-ES4.2.4): the copied nuget.config is discovered from every
# project directory and its <clear /> drops the nuget.org source inherited
# from the user-level config. NuGetAuthenticate@1 installs the credential
# provider that authenticates to the feed.
- powershell: Copy-Item .azure-pipelines/nuget.config nuget.config
displayName: 'Use DevDiv_PublicPackages as the only NuGet source'
- task: NuGetAuthenticate@1
displayName: 'Authenticate NuGet to DevDiv_PublicPackages feed'
# Playwright.Tooling fetches playwright-core with `npm pack`, which picks up
# the registry and credentials from this .npmrc.
- task: UseNode@1
displayName: 'Install Node.js'
inputs:
version: '24.x'
# Explicit ASCII encoding: Windows PowerShell redirection would write UTF-16,
# which npm cannot read.
- powershell: Add-Content -Path .npmrc -Encoding Ascii -Value "registry=https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/DevDiv_PublicPackages/npm/registry/"
displayName: 'Point npm registry at DevDiv_PublicPackages feed'
- task: npmAuthenticate@0
displayName: 'Authenticate npm to DevDiv_PublicPackages feed'
inputs:
workingFile: .npmrc
# We need to download the driver first, so we can build
- task: DotNetCoreCLI@2
displayName: Download the driver
@@ -98,7 +132,7 @@ extends:
displayName: Publish Microsoft.Playwright{NUnit,MSTest,Xunit,TestAdapter}
condition: or(
eq('${{ parameters.doRelease }}', true),
startsWith(variables['Build.SourceBranch'], 'refs/tags/')
startsWith(variables['Build.SourceBranch'], 'refs/tags/v1.')
)
inputs:
useDotNetTask: false
+5 -1
View File
@@ -359,4 +359,8 @@ testCert.cer
coverage-report/
src/Playwright.Tests/coverage.*.xml
# Written by the release pipeline: npm credentials (npmAuthenticate@0) and the
# root copy of .azure-pipelines/nuget.config.
.npmrc
/nuget.config
+1 -1
View File
@@ -3,7 +3,7 @@
## Building
```bash
./build.sh --download-driver # download the Playwright driver
./build.sh --download-driver # download the Playwright driver (needs Node.js/npm on PATH)
dotnet build ./src # build the entire solution
```
@@ -24,6 +24,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Formats.Tar;
using System.IO;
using System.IO.Compression;
@@ -168,7 +169,7 @@ internal class DriverDownloader
var tasks = new List<Task>
{
DownloadPlaywrightPackageAsync(client, driversDirectory.FullName),
DownloadPlaywrightPackageAsync(driversDirectory.FullName),
};
foreach (var (platformId, nodeSuffix) in _platforms)
{
@@ -185,12 +186,60 @@ internal class DriverDownloader
return true;
}
private async Task DownloadPlaywrightPackageAsync(HttpClient client, string driversDirectory)
private static string ResolveNpmExecutable()
{
string url = $"https://registry.npmjs.org/playwright-core/-/playwright-core-{DriverVersion}.tgz";
await WithRetriesAsync(url, async () =>
string name = OperatingSystem.IsWindows() ? "npm.cmd" : "npm";
// On Windows npm.cmd must be started by its full path: launched by bare name,
// the script's %~dp0 expands to the working directory instead of its own
// directory and it fails to find node_modules/npm/bin/npm-cli.js.
string path = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
foreach (string directory in path.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
{
using var stream = await GetStreamAsync(client, url).ConfigureAwait(false);
string candidate = Path.Combine(directory.Trim('"'), name);
if (File.Exists(candidate))
{
return candidate;
}
}
return name;
}
private async Task DownloadPlaywrightPackageAsync(string driversDirectory)
{
// Fetched with `npm pack` rather than a hard-coded registry URL so that the
// registry configured for npm (and its credentials) are used, e.g. the
// root-level .npmrc the release pipeline writes to point at the internal
// Azure Artifacts feed. npm is run from the repository root for that reason.
string package = $"playwright-core@{DriverVersion}";
string tempDirectory = Directory.CreateTempSubdirectory("playwright-core-").FullName;
try
{
Console.WriteLine($"Downloading {package} with npm pack");
var startInfo = new ProcessStartInfo(ResolveNpmExecutable())
{
WorkingDirectory = Path.GetFullPath(BasePath),
UseShellExecute = false,
};
startInfo.ArgumentList.Add("pack");
startInfo.ArgumentList.Add(package);
startInfo.ArgumentList.Add("--pack-destination");
startInfo.ArgumentList.Add(tempDirectory);
using (var process = Process.Start(startInfo) ?? throw new Exception("Failed to start npm"))
{
await process.WaitForExitAsync().ConfigureAwait(false);
if (process.ExitCode != 0)
{
throw new Exception($"npm pack {package} failed with exit code {process.ExitCode}");
}
}
string tgz = Path.Combine(tempDirectory, $"playwright-core-{DriverVersion}.tgz");
if (!File.Exists(tgz))
{
throw new Exception($"npm pack did not produce {tgz}");
}
using var stream = File.OpenRead(tgz);
using var gzip = new GZipStream(stream, CompressionMode.Decompress);
using var tar = new TarReader(gzip);
bool extractedAnything = false;
@@ -207,9 +256,13 @@ internal class DriverDownloader
}
if (!extractedAnything)
{
throw new Exception($"No files were extracted from {url}");
throw new Exception($"No files were extracted from {tgz}");
}
}).ConfigureAwait(false);
}
finally
{
Directory.Delete(tempDirectory, true);
}
Console.WriteLine($"Extracted playwright-core {DriverVersion}");
}