@@ -6,70 +6,133 @@ parameters:
66 - name : installPython
77 type : boolean
88 default : true
9+ - name : customNPMRegistry
10+ type : string
11+ default : ' '
12+ - name : nodeVersion
13+ type : string
14+ default : ' 22.17.0'
15+ - name : pythonVersion
16+ type : string
17+ default : ' 3.9'
918 - name : npmrcPath
1019 type : string
11- default : ' $(Build.SourcesDirectory )/.npmrc'
20+ default : ' $(Agent.TempDirectory )/.npmrc'
1221
1322steps :
1423 - ${{ if eq(parameters.installNode, true) }} :
15- - pwsh : |
16- $npmrcPath = "${{ parameters.npmrcPath }}"
24+ - task : NodeTool@0
25+ inputs :
26+ versionSpec : ${{ parameters.nodeVersion }}
27+ checkLatest : true
28+ displayName : 🛠 Install Node ${{ parameters.nodeVersion }}
1729
18- if (Test-Path -LiteralPath $npmrcPath -PathType Container) {
19- throw "npmrcPath points to a directory (expected a file): $npmrcPath"
20- }
30+ - ${{ if ne(parameters.customNPMRegistry, '') }} :
31+ # When using a private/custom registry, configure npm to read auth/config from a temp user config
32+ # instead of relying on a checked-in project .npmrc.
33+ - pwsh : |
34+ $path = "${{ parameters.npmrcPath }}"
2135
22- $parent = Split-Path -Parent $npmrcPath
23- if ($parent -and -not (Test-Path -LiteralPath $parent)) {
24- New-Item -ItemType Directory -Path $parent -Force | Out-Null
25- }
36+ if (Test-Path -LiteralPath $path -PathType Container) {
37+ throw "npmrcPath points to a directory (expected a file): $path"
38+ }
2639
27- if (-not (Test -Path -LiteralPath $npmrcPath -PathType Leaf)) {
28- New-Item -ItemType File -Path $npmrcPath -Force | Out-Null
29- }
30- displayName: Ensure .npmrc is a file
40+ $parent = Split -Path -Parent $path
41+ if ($parent -and -not (Test -Path -LiteralPath $parent)) {
42+ New-Item -ItemType Directory -Path $parent -Force | Out-Null
43+ }
3144
32- - task : npmAuthenticate@0
33- inputs :
34- workingFile : ${{ parameters.npmrcPath }}
45+ if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
46+ New-Item -ItemType File -Path $path -Force | Out-Null
47+ }
48+
49+ Write-Host "##vso[task.setvariable variable=NPM_CONFIG_USERCONFIG]$path"
50+ Write-Host "##vso[task.setvariable variable=NPM_CONFIG_REGISTRY]${{ parameters.customNPMRegistry }}"
51+ displayName: 📦 Setup NPM User Config
3552
36- - pwsh : |
37- $source = "${{ parameters.npmrcPath }}"
38- $dest = "$(Build.SourcesDirectory)/.npmrc"
53+ # Configure npm/yarn to use the custom registry and ensure auth headers are sent.
54+ - pwsh : |
55+ $path = "${{ parameters.npmrcPath }}"
56+ $registry = "${{ parameters.customNPMRegistry }}"
3957
40- if (-not (Test-Path -LiteralPath $source -PathType Leaf)) {
41- throw "Expected authenticated .npmrc file at: $source"
42- }
58+ $env:NPM_CONFIG_USERCONFIG = $path
59+ $env:NPM_CONFIG_REGISTRY = $registry
4360
44- if (Test-Path -LiteralPath $dest -PathType Container) {
45- throw "Expected destination .npmrc to be a file, but it is a directory: $dest"
46- }
61+ npm config set registry "$registry"
4762
48- $srcFull = [System.IO.Path]::GetFullPath($source)
49- $destFull = [System.IO.Path]::GetFullPath($dest)
63+ # npm >v7 deprecated the `always-auth` config option, refs npm/cli@72a7eeb
64+ # following is a workaround for yarn-like clients to send authorization header for GET
65+ # requests to the registry.
66+ "always-auth=true" | Out-File -FilePath $path -Append -Encoding utf8
5067
51- if ($srcFull -ieq $destFull) {
52- Write-Host "Authenticated .npmrc already in repo root: $dest"
53- return
54- }
68+ $yarn = Get-Command yarn -ErrorAction SilentlyContinue
69+ if ($null -ne $yarn) {
70+ yarn config set registry "$registry"
71+ } else {
72+ Write-Host "yarn not found; skipping yarn registry configuration"
73+ }
74+ displayName: 📦 Setup NPM & Yarn
5575
56- if (-not (Test-Path -LiteralPath $dest -PathType Leaf)) {
57- Copy-Item -LiteralPath $source -Destination $dest -Force
58- Write-Host "Copied authenticated .npmrc -> $dest"
59- } else {
60- Write-Host "Repo root .npmrc already exists; not overwriting: $dest"
61- }
62- displayName: Copy .npmrc to repo root (if missing)
76+ # Populate the temp .npmrc with auth for the configured registry.
77+ - task : npmAuthenticate@0
78+ inputs :
79+ workingFile : ${{ parameters.npmrcPath }}
80+ displayName : 📦 Setup NPM Authentication
81+
82+ # Some lockfiles contain hardcoded references to public registries. Rewrite them so installs
83+ # and `npx` resolve from the custom registry consistently.
84+ - pwsh : |
85+ $registry = "${{ parameters.customNPMRegistry }}"
86+ $scriptPath = Join-Path "$(Agent.TempDirectory)" 'setup-npm-registry.js'
87+
88+ $lines = @(
89+ "const fs = require('fs').promises;",
90+ "const path = require('path');",
91+ "",
92+ "async function* getLockFiles(dir) {",
93+ " const files = await fs.readdir(dir);",
94+ "",
95+ " for (const file of files) {",
96+ " const fullPath = path.join(dir, file);",
97+ " const stat = await fs.stat(fullPath);",
98+ "",
99+ " if (stat.isDirectory()) {",
100+ " if (file === 'node_modules' || file === '.git') {",
101+ " continue;",
102+ " }",
103+ " yield* getLockFiles(fullPath);",
104+ " } else if (file === 'yarn.lock' || file === 'package-lock.json') {",
105+ " yield fullPath;",
106+ " }",
107+ " }",
108+ "}",
109+ "",
110+ "async function rewrite(file, registry) {",
111+ " let contents = await fs.readFile(file, 'utf8');",
112+ " contents = contents.replace(/https:\\/\\/registry\\.[^.]+\\.(com|org)\\//g, registry);",
113+ " await fs.writeFile(file, contents);",
114+ "}",
115+ "",
116+ "async function main() {",
117+ " const root = process.cwd();",
118+ " const registry = process.env.NPM_CONFIG_REGISTRY || " + JSON.stringify($registry) + ";",
119+ "",
120+ " for await (const file of getLockFiles(root)) {",
121+ " await rewrite(file, registry);",
122+ " console.log('Updated node registry:', file);",
123+ " }",
124+ "}",
125+ "",
126+ "main();"
127+ )
128+
129+ Set-Content -LiteralPath $scriptPath -Value ($lines -join "`n") -Encoding utf8
130+ node $scriptPath
131+ displayName: 📦 Setup NPM Registry
63132
64133 - script : npm config get registry
65134 displayName : Verify NPM Registry
66135
67- - task : NodeTool@0
68- inputs :
69- versionSpec : ' 22.17.0'
70- checkLatest : true
71- displayName : Select Node 22 LTS
72-
73136 - ${{ if eq(parameters.installPython, true) }} :
74137 - task : PipAuthenticate@1
75138 displayName : ' Pip Authenticate'
@@ -78,7 +141,7 @@ steps:
78141
79142 - task : UsePythonVersion@0
80143 inputs :
81- versionSpec : ' 3.9 ' # note Install Python dependencies step below relies on Python 3.9
144+ versionSpec : ${{ parameters.pythonVersion }}
82145 addToPath : true
83146 architecture : ' x64'
84- displayName : Select Python version
147+ displayName : Select Python ${{ parameters.pythonVersion }}
0 commit comments