Private preview - DevOps chapter, NPM Registry deep page.
Sidebar wiring lives in the parallel global pass. Not indexed.
Curriculum . Playwright DevOps . Registry
Private NPM Registry - JFrog Artifactory and Sonatype Nexus
Why your Playwright fixtures, page objects, and helper libs should live behind a private registry,
and how to set one up two different ways - JFrog Artifactory and Sonatype Nexus. Side-by-side topology,
.npmrc anatomy, scoped publish + consume flow, and a working CI handshake. Worked example
ships a @tta/cart-helpers package used across the TTACart suite.
2
Registries compared
JFrog Artifactory and Sonatype Nexus, side-by-side.
6
Diagrams
3 mermaid sequences and flowcharts, 3 hand-styled inline SVGs.
Public registry.npmjs.org is fast and free, but it solves a different problem than what a test
automation team needs in production. Four pressures push automation teams toward a private registry:
Air-gapped builds - the CI runner inside a bank or hospital network cannot reach the public internet. It still needs playwright, @playwright/test, and your own helper packages.
Version pinning at the org level - one team accidentally bumps chalk to a vulnerable major; every other team also pulls the bad version on next install. A proxy repo gives you a sliding window and a freeze switch.
Custom modules - @tta/cart-helpers, @tta/booking-helpers, @tta/network-fixtures. These never go to the public registry but they ship across many Playwright projects.
Audit trail - SOC 2, ISO 27001, and most regulated SDLCs need a who/what/when log of every package downloaded by the build farm. The proxy is the natural choke point.
Three repo types you will see in both products
Hosted - your own packages live here. Read + write. @tta/[email protected] uploads here.
Proxy (or "remote") - a cached mirror of registry.npmjs.org. The first install of [email protected] fetches from npm, every install after that serves the cached tarball.
Group (or "virtual") - a logical union that resolves a request in order: try hosted first, fall back to proxy. One URL for the consumer.
02Topology - the one diagram that matters mermaid 1/3
This is the picture you need on a whiteboard before you spin up either product. It is the same shape in both, just named differently.
flowchart LR
DEV[Developer laptop] -->|npm install @tta/...| GROUP[Group / Virtual repo]
CI[CI runner - GitHub Actions] -->|npm install| GROUP
GROUP -->|scoped @tta/*| HOSTED[Hosted repo - your packages]
GROUP -->|everything else| PROXY[Proxy - cache of npmjs.org]
PROXY -.->|cache miss| NPMJS[(registry.npmjs.org)]
HOSTED -.->|publish only| RELEASE[CI release job]
classDef ours fill:#dbeafe,stroke:#3b82f6
classDef cache fill:#fef3c7,stroke:#d97706
classDef pub fill:#fee2e2,stroke:#ef4444
class HOSTED ours
class PROXY cache
class NPMJS pub
Topology - one URL (the group repo) hides both the cache and your private packages.
The win: the developer and the CI runner only ever talk to one URL. The group repo dispatches
the request to the right backend. If you take down the hosted repo for maintenance, the proxy still
serves public packages. If npmjs.org has an outage, the proxy keeps serving cached versions.
03JFrog Artifactory - npm-virtual on top vendor 1/2
Artifactory ships a one-click "Create npm repository" wizard. It uses these names:
npm-local (or whatever you name it) - the local / hosted repo. Your @tta/* packages publish here.
npm-remote - the remote repo pointing at https://registry.npmjs.org. Acts as the cache.
npm-virtual - the virtual repo, ordered: npm-local first, then npm-remote. This is the URL your .npmrc points at.
Install URL pattern
artifactory - .npmrc
# Artifactory virtual repo URL pattern
registry=https://artifactory.tta-corp.example/artifactory/api/npm/npm-virtual/
# Scoped registry override for @tta/* (optional but explicit)
@tta:registry=https://artifactory.tta-corp.example/artifactory/api/npm/npm-virtual/
//artifactory.tta-corp.example/artifactory/api/npm/npm-virtual/:_authToken=${ARTIFACTORY_TOKEN}
always-auth=true
Publish URL pattern
Publish goes to the local repo, not the virtual one.
Modern SPA, "Set Me Up" generator gives you the .npmrc block
Functional but older feel, IQ for security tab is paid
Repo type names
local / remote / virtual
hosted / proxy / group
HA model
Active-active with shared DB + storage (Enterprise tier)
Active-passive primarily; clustering is Pro tier add-on
Free tier
JFrog Cloud free up to 2 GB
Nexus Repository OSS - free, self-hosted, full npm format
Pricing tier for npm
Pro / Enterprise X / Enterprise +
OSS (free) / Pro (per-user) / Sonatype Lifecycle
Scoped package URL
/artifactory/api/npm/npm-virtual/@tta/pkg
/repository/npm-group/@tta/pkg
Auth token gen
User profile -> "Generate Identity Token"
User profile -> NPM Bearer Token
Replication
Native event-based replication for HA
Smart Proxy + Blob Store replication
Vuln scan
Built-in JFrog Xray (Enterprise X+)
Sonatype Lifecycle add-on (paid)
Where Playwright lands
Cached in npm-remote on first install
Cached in npm-proxy on first install
Heuristic: if you already pay for JFrog for Maven and Docker artifacts, add the npm format - no new vendor. If you are starting from zero and want a free OSS path, Nexus Repository OSS is the path of least resistance for a Playwright team.
06.npmrc anatomy + token resolution svg 1/3
The same .npmrc file works against both registries - only the URL changes. Four lines do the
real work:
registry= - the default registry for any unscoped package (chalk, zod, etc.)
@tta:registry= - the scoped registry for everything that starts with @tta/. Lets you route only your packages to the private repo and leave unscoped packages on the public.
//registry-host/path/:_authToken=... - the host-specific auth header. The slashes must match the registry URL exactly, including trailing slash.
always-auth=true - send the auth token on GET too. Default is "only on PUT". Private registries usually require auth even for reads.
How npm picks the right registry URL and the right token, line by line.
Scoped registry routing - the win
You almost always want scoped routing, not a global private registry. That way chalk still
comes from the cache of the public registry, and only @tta/* reaches into the hosted repo. Two
benefits: (1) faster cold install because most traffic still hits a CDN-fronted cache, (2) zero risk of an
outage in the private registry taking down installs of public packages.
Scoped registries split traffic by package name prefix. Best of both worlds.
07Publish flow - @tta/cart-helpersmermaid 2/3
The publish flow is identical between Artifactory and Nexus from the developer side. Only the URL
changes. We package @tta/cart-helpers - a shared login + cart fixture used by every TTACart
Playwright project at https://app.thetestingacademy.com/playwright/ttacart/.
sequenceDiagram
autonumber
participant Dev as Developer
participant NPM as npm CLI
participant REG as Hosted repo
Dev->>NPM: 1. npm version patch
NPM-->>Dev: 1.0.0 -> 1.0.1
Dev->>NPM: 2. npm publish --access restricted
NPM->>NPM: 3. read .npmrc, find @tta scope
NPM->>REG: 4. PUT /@tta/cart-helpers/-/cart-helpers-1.0.1.tgz
REG-->>NPM: 5. 201 Created
NPM-->>Dev: 6. + @tta/[email protected]
Publish sequence - same six steps for either vendor.
publishConfig.registry pins the publish URL inside the package itself, so a typo in the user-level .npmrc cannot accidentally push your private package to registry.npmjs.org. Highly recommended.
08Consume in a Playwright project mermaid 3/3
sequenceDiagram
autonumber
participant CI as GitHub Actions
participant NPM as npm
participant GROUP as Group / Virtual repo
participant HOSTED as Hosted repo
participant PROXY as Proxy repo
CI->>NPM: 1. npm ci
NPM->>GROUP: 2. GET @tta/cart-helpers
GROUP->>HOSTED: 3. lookup (scope match)
HOSTED-->>GROUP: 4. tarball stream
GROUP-->>NPM: 5. @tta/cart-helpers-1.0.1.tgz
NPM->>GROUP: 6. GET @playwright/test
GROUP->>PROXY: 7. lookup (unscoped)
PROXY-->>GROUP: 8. cached or fetch upstream
GROUP-->>NPM: 9. @playwright/test-1.49.0.tgz
Consume sequence - one URL hides both backends. Cached on second run.
# Everything public still works, only @tta is rerouted.
@tta:registry=https://nexus.tta-corp.example/repository/npm-group/
//nexus.tta-corp.example/repository/npm-group/:_authToken=${NPM_TOKEN}
always-auth=true
First run downloads ~120 MB into node_modules/; subsequent runs hit the lock file and the group repo cache. Browsers are still fetched via npx playwright install - that is a separate binary cache, not handled by the npm registry.
09CI integration - GitHub Actions secret svg 3/3
In CI, the registry token lives in a GitHub Actions secret called NPM_TOKEN. The
actions/setup-node action consumes it via the registry-url input. The token never appears
in logs because Actions masks any value passed via secrets.*.
Token missing or empty. Check echo $NPM_TOKEN in CI.
Trailing slash mismatch between registry= and //host/path/:_authToken.
Token expired - regen and update the secret.
always-auth=true missing - install GETs go through without the header.
403 Forbidden
Token exists but the user role has no read perm on the repo.
Trying to publish to a hosted repo with "Disable redeploy" set, version already exists.
Scope claim missing - Nexus role allows @team-a/* but you are pushing @tta/*.
Cache poisoning
Cached tarball corrupted by a partial proxy fetch.
Symptom: EINTEGRITY mismatch between lock file and what proxy serves.
Fix: invalidate cache in the proxy repo settings for that package.
Prevent: enable "remote storage SHA verification" in both vendors.
Scope mismatch
Package @tta/cart-helpers shows "Not found" but exists in the hosted repo.
Cause: @tta:registry not set, npm fell back to registry.npmjs.org which 404s.
Fix: add the scoped line to .npmrc.
D1Drill 1 - Stand up a Nexus npm-group repo
Spin up Nexus Repository OSS locally with Docker. Create three npm repos: npm-hosted,
npm-proxy (pointed at https://registry.npmjs.org), and npm-group that
wraps them. Verify by hitting /repository/npm-group/chalk in your browser.
Hint
Use docker run -p 8081:8081 sonatype/nexus3. Default admin password lives in /nexus-data/admin.password.
D2Drill 2 - Stand up Artifactory npm-virtual
Do the same with JFrog Artifactory free tier (cloud or self-hosted JCR). Create npm-local,
npm-remote, npm-virtual. Confirm the wizard generated .npmrc matches the
shape you learned in section 3.
Hint
The "Set Me Up" dialog gives a copy-paste .npmrc block including a generated identity token.
D3Drill 3 - Publish @tta/cart-helpers
Build a minimal package that exports one helper: loginAsShopper(page). Tag 1.0.0,
publish to your hosted repo. Confirm it shows up in the repo browser under @tta/cart-helpers/-/cart-helpers-1.0.0.tgz.
Hint
Add publishConfig.registry in package.json so npm publish does the right thing even without CLI args.
D4Drill 4 - Consume it from a new Playwright project
Create a fresh npm init playwright@latest project. Add a .npmrc scoped to @tta.
npm install @tta/cart-helpers. Write one spec that imports loginAsShopper and asserts
the cart icon shows after login.
Hint
Use the TTACart instance at https://app.thetestingacademy.com/playwright/ttacart/. Your helper navigates there and clicks the login link.
D5Drill 5 - Audit the request
Run npm install with --loglevel=http. Verify the Authorization header
gets attached. Confirm in the Nexus / Artifactory audit log that the install was recorded against your
user.
Hint
npm prints request URLs at http log level. The header itself is redacted in the client log; check the server.
D6Drill 6 - Rotate the token
Revoke the current identity token, generate a new one, update both the local .npmrc and the
GitHub Actions secret. Re-run npm ci to confirm both still work.
Hint
The trick: nothing in the codebase or lock file references the token directly. Rotation is two paste operations.
D7Drill 7 - CI handshake on GitHub Actions
Push the workflow YAML from section 9. Confirm: (1) the install pulls @tta/cart-helpers from
your registry, (2) the run logs never print the token in plaintext, (3) playwright test passes.
Hint
Verify with npm config get registry printed in a step - it should show your group URL.
D8Drill 8 - Break it, fix it
Force-break the setup: drop the trailing slash from _authToken, re-run npm ci.
Observe the 401. Add it back, observe the install succeed. Same for: removing always-auth=true,
wrong scope name, expired token. Each break-fix takes ~30 seconds and locks the pattern in.
Hint
Run with NPM_CONFIG_LOGLEVEL=verbose to see the exact request that fails.
SSolution snippets
Sub-tabs split between shell commands and TS test snippets. Java equivalent shown where the Playwright API differs only by language binding.
Stand up Nexus locally
nexus-local.sh
# 1) Run Nexus
docker run -d --name nexus -p 8081:8081 \
-v nexus-data:/nexus-data \
sonatype/nexus3:latest
# 2) Grab admin password
docker exec nexus cat /nexus-data/admin.password
# 3) Log in at http://localhost:8081 and create:# - npm-hosted (Hosted, redeploy: Disable)# - npm-proxy (Proxy, remote: https://registry.npmjs.org)# - npm-group (Group, members: npm-hosted, npm-proxy)
Stand up Artifactory locally
artifactory-local.sh
# 1) Run JFrog Container Registry (free)
docker run -d --name jcr -p 8082:8082 -p 8081:8081 \
releases-docker.jfrog.io/jfrog/artifactory-jcr:latest
# 2) Log in at http://localhost:8082 (admin / password)# 3) Create npm-local, npm-remote, npm-virtual via the wizard
Publish @tta/cart-helpers
publish.sh
cd packages/cart-helpers
npm version patch # bumps 1.0.0 -> 1.0.1
npm publish --access restricted
# Output: + @tta/[email protected]
Consume in a Playwright project
consume.sh
# 1) Drop .npmrc at project root (do NOT commit secrets)
cat > .npmrc <<EOF
@tta:registry=https://nexus.tta-corp.example/repository/npm-group/
//nexus.tta-corp.example/repository/npm-group/:_authToken=\${NPM_TOKEN}
always-auth=true
EOF
# 2) Install
export NPM_TOKEN=ey... # identity token from registry UI
npm install @tta/cart-helpers @playwright/test
npx playwright install --with-deps
Rotate token (zero downtime)
rotate.sh
# 1) Generate new token in registry UI -> user profile# 2) Update GitHub Actions secret
gh secret set NPM_TOKEN -b "ey...new..."
# 3) Update local .npmrc env
export NPM_TOKEN=ey...new...
# 4) Revoke old token from UI ONLY AFTER step 3 confirms install works
Both Artifactory and Nexus speak Maven, npm, Docker, PyPI, and more out of the same instance. If you already host Java artifacts there, adding npm is a 10-minute config change, not a new vendor.