Azure 50: Azure Artifacts npm feed — `@driveway/react` (Lib)
easy⏱ 5 mincourseazure
Scoped registry routing
An .npmrc line like @driveway:registry=https://pkgs.dev.azure.com/LithiaMotors/.../_packaging/npm-feed/npm/registry/ says: any package starting with @driveway/ goes to this registry. Other packages still hit the public npm registry. This lets Lib ship privately without forcing all of node_modules through a slow proxy.
# .npmrc
@driveway:registry=https://pkgs.dev.azure.com/LithiaMotors/.../_packaging/npm-feed/npm/registry/
always-auth=true
; auth section appended at install time
//pkgs.dev.azure.com/LithiaMotors/.../_packaging/npm-feed/npm/registry/:username=...
//pkgs.dev.azure.com/LithiaMotors/.../_packaging/npm-feed/npm/registry/:_password=<base64-pat>
Local dev vs CI authentication
Locally: vsts-npm-auth (a small CLI from Microsoft) generates a refresh token and writes the auth section for you. In CI: the npmAuthenticate@0 task uses the System.AccessToken of the pipeline run, which is automatically scoped to the feed. Devs never see a long-lived PAT in CI; everything is per-run.
# Azure Pipelines
- task: npmAuthenticate@0
inputs:
workingFile: ./.npmrc
- script: npm ci
workingDirectory: $(servicePath)
Publishing the Lib
Lib/'s own pipeline runs GitVersion → npm run build → npm publish. The publish step requires a publisher PAT scoped to the feed (stored in Variable Group lib-publish). Each merge to main bumps the patch and publishes a new version; commits with +semver: minor/major mint feature releases.
Build the resolver
Write buildNpmrc({ feedUrl, scopes, mode }) where mode is 'ci' | 'local'. Returns a string .npmrc: one <scope>:registry=... line per scope, always-auth=true, and either a pipeline placeholder (# auth injected by npmAuthenticate@0) or a local pointer (# run: vsts-npm-auth -config .npmrc). Then write validate(npmrc, scopes) that confirms every requested scope is mapped. Log both modes.
Pin the feed URL in .npmrc, not in package.json
If you put the registry URL in package.json's publishConfig, every fork or example app inherits it. Pinning in .npmrc (and committing it) keeps the URL with the project root and lets you swap feeds (e.g., during an org migration) by editing one file.
Quiz: a build fails with 401 in CI but works locally
Diagnosis: the local install used the cached vsts-npm-auth token but the CI agent's System.AccessToken doesn't have Packaging (read) permission on the feed. Fix: open the feed → Settings → Permissions → grant Project Collection Build Service (LithiaMotors) reader, or scope by repo if you prefer least-privilege.