Skip to content

Commit e50287a

Browse files
sorccuclaude
andauthored
feat(check-parser): bundle .npmrc in Playwright Check Suite bundles by default [RED-691] [show] (#1394)
* feat(check-parser): bundle .npmrc in Playwright Check Suite bundles by default [RED-691] Playwright Check Suites run the user's own package-manager install in the Checkly cloud. When a project uses an alternate/private npm registry configured in .npmrc, that file was only bundled if the user manually listed it in the `include` prop; otherwise the install resolved against the default registry and private packages failed to resolve. Route .npmrc through the same workspace/package-files machinery that already bundles the lockfile and pnpm-workspace.yaml: emit the .npmrc at the workspace root and every workspace-member package root as a `workspace-npmrc` local dependency, gated on non-restricted parsing so only Playwright Check Suites are affected (browser/multistep/API checks are unchanged). This is exactly the set a package manager reads (project .npmrc is consulted from the install directory upward, never from subdirectories below it). Feed the same root + member .npmrc set into the workspace cache hash so a registry change invalidates the bundle cache. The bundled set and the hashed set are identical by construction, so a bundled .npmrc is always reflected in the cache key. Projects without an .npmrc hash byte-identically as before. Note: adding `npmrc:` records to the workspace cache hash diverges from terraform-provider-checkly's composeBundleChecksum for projects that have an .npmrc, until the provider mirrors the record type (flagged in a code comment). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(ai-context): note .npmrc is bundled automatically for Playwright Check Suites [RED-691] .npmrc no longer needs a manual `include` entry — the workspace-root and package-root .npmrc are bundled by default. Warn to reference credentials via ${NPM_TOKEN} rather than embedding plaintext tokens (since the file is now uploaded automatically), and clarify that a .npmrc below a package root is not bundled. Replace the now-obsolete `include: ["../.npmrc"]` example with a non-code-asset example. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 522049f commit e50287a

13 files changed

Lines changed: 295 additions & 9 deletions

File tree

packages/cli/src/ai-context/references/configure-playwright-checks.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
## Dependencies
1010

1111
- Use the user's `package.json` and lock file for dependencies. Do not add dependency declarations to check code.
12-
- For private packages or custom registries, include the registry config file, usually `.npmrc`, with `include`.
13-
- The `.npmrc` should reference a Checkly environment variable such as `${NPM_TOKEN}`. Tell the user that the token must exist in Checkly before `deploy` or `trigger`.
12+
- For private packages or custom registries, `.npmrc` is bundled automatically — the workspace-root `.npmrc` and any `.npmrc` beside a package's `package.json` are included by default. You do not need to add `.npmrc` to `include`.
13+
- The `.npmrc` should reference a Checkly environment variable such as `${NPM_TOKEN}`. Tell the user that the token must exist in Checkly before `deploy` or `trigger`. Because `.npmrc` is uploaded automatically, warn users to reference credentials via environment variables (`${NPM_TOKEN}`) rather than embedding plaintext tokens.
1414
- Use `installCommand` only when the default package-manager install command is not enough.
15-
- In Checkly CLI v8.0.0 and later, `include` patterns resolve relative to the Playwright config directory, not the project root. If `playwrightConfigPath` points to a subdirectory, adjust `include` globs. Example: `playwrightConfigPath: "./e2e/playwright.config.ts"` with root `.npmrc` needs `include: ["../.npmrc"]`.
15+
- In Checkly CLI v8.0.0 and later, `include` patterns resolve relative to the Playwright config directory, not the project root. If `playwrightConfigPath` points to a subdirectory, adjust `include` globs. Example: `playwrightConfigPath: "./e2e/playwright.config.ts"` with a root fixture at `fixtures/data.json` needs `include: ["../fixtures/data.json"]`.
1616

1717
## Install troubleshooting
1818

@@ -40,8 +40,8 @@ import { PlaywrightCheck } from "checkly/constructs"
4040
new PlaywrightCheck("checkout-suite", {
4141
name: "Checkout suite",
4242
playwrightConfigPath: "./e2e/playwright.config.ts",
43-
// Include root .npmrc for private package or registry auth.
44-
include: ["../.npmrc"],
43+
// .npmrc is bundled automatically for private packages or registry auth
44+
// no include entry needed.
4545
})
4646
```
4747
- `playwrightConfigPath` is resolved **relative to the `.check.ts` file that declares the construct**, not the project root. If your check lives in `monitoring/suites.check.ts` and your config in `playwright.config.ts` at the repo root, use `'../playwright.config.ts'`.

packages/cli/src/services/check-parser/__tests__/cache-hash.spec.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,95 @@ describe('composeCacheHash', () => {
149149
expect(without).not.toBe(withMember)
150150
})
151151

152+
test('omitting npmrcs matches passing an empty array (no-op)', () => {
153+
const root = buf('{"name":"root"}')
154+
const lockfile = { name: 'package-lock.json', hash: sha256('lock') }
155+
expect(composeCacheHash({
156+
lockfile,
157+
packageJsons: [{ path: 'package.json', raw: root }],
158+
excludedFields: ['version'],
159+
})).toBe(composeCacheHash({
160+
lockfile,
161+
packageJsons: [{ path: 'package.json', raw: root }],
162+
npmrcs: [],
163+
excludedFields: ['version'],
164+
}))
165+
})
166+
167+
test('adding a root .npmrc changes the hash', () => {
168+
const root = buf('{"name":"root"}')
169+
const lockfile = { name: 'package-lock.json', hash: sha256('lock') }
170+
const without = composeCacheHash({
171+
lockfile,
172+
packageJsons: [{ path: 'package.json', raw: root }],
173+
excludedFields: ['version'],
174+
})
175+
const withNpmrc = composeCacheHash({
176+
lockfile,
177+
packageJsons: [{ path: 'package.json', raw: root }],
178+
npmrcs: [{ path: '.npmrc', hash: sha256('registry=https://example.com/') }],
179+
excludedFields: ['version'],
180+
})
181+
expect(without).not.toBe(withNpmrc)
182+
})
183+
184+
test('changing .npmrc content changes the hash', () => {
185+
const root = buf('{"name":"root"}')
186+
const lockfile = { name: 'package-lock.json', hash: sha256('lock') }
187+
const a = composeCacheHash({
188+
lockfile,
189+
packageJsons: [{ path: 'package.json', raw: root }],
190+
npmrcs: [{ path: '.npmrc', hash: sha256('registry=https://a.example.com/') }],
191+
excludedFields: ['version'],
192+
})
193+
const b = composeCacheHash({
194+
lockfile,
195+
packageJsons: [{ path: 'package.json', raw: root }],
196+
npmrcs: [{ path: '.npmrc', hash: sha256('registry=https://b.example.com/') }],
197+
excludedFields: ['version'],
198+
})
199+
expect(a).not.toBe(b)
200+
})
201+
202+
test('adding a member .npmrc changes the hash', () => {
203+
const root = buf('{"name":"root"}')
204+
const lockfile = { name: 'package-lock.json', hash: sha256('lock') }
205+
const rootOnly = composeCacheHash({
206+
lockfile,
207+
packageJsons: [{ path: 'package.json', raw: root }],
208+
npmrcs: [{ path: '.npmrc', hash: sha256('root-npmrc') }],
209+
excludedFields: ['version'],
210+
})
211+
const withMember = composeCacheHash({
212+
lockfile,
213+
packageJsons: [{ path: 'package.json', raw: root }],
214+
npmrcs: [
215+
{ path: '.npmrc', hash: sha256('root-npmrc') },
216+
{ path: 'apps/main/.npmrc', hash: sha256('member-npmrc') },
217+
],
218+
excludedFields: ['version'],
219+
})
220+
expect(rootOnly).not.toBe(withMember)
221+
})
222+
223+
test('input order of npmrcs does not affect the hash', () => {
224+
const root = buf('{"name":"root"}')
225+
const lockfile = { name: 'package-lock.json', hash: sha256('lock') }
226+
const a = { path: '.npmrc', hash: sha256('root-npmrc') }
227+
const b = { path: 'apps/main/.npmrc', hash: sha256('member-npmrc') }
228+
expect(composeCacheHash({
229+
lockfile,
230+
packageJsons: [{ path: 'package.json', raw: root }],
231+
npmrcs: [b, a],
232+
excludedFields: ['version'],
233+
})).toBe(composeCacheHash({
234+
lockfile,
235+
packageJsons: [{ path: 'package.json', raw: root }],
236+
npmrcs: [a, b],
237+
excludedFields: ['version'],
238+
}))
239+
})
240+
152241
test('lockfile content change changes the hash', () => {
153242
const root = buf('{"name":"root"}')
154243
const a = composeCacheHash({
@@ -192,6 +281,11 @@ describe('composeCacheHash', () => {
192281
// and the same excluded fields must produce this exact digest in
193282
// terraform-provider-checkly's composeBundleChecksum. If you change this
194283
// fixture, mirror the change in the TF provider's test suite.
284+
//
285+
// NOTE: composeCacheHash also hashes `npmrc:` records (added for .npmrc
286+
// bundling). This fixture has no .npmrc so the digest is unchanged, but
287+
// projects that DO have an .npmrc will hash differently until the TF
288+
// provider mirrors the npmrc record type.
195289
test('matches the cross-language parity fixture digest', () => {
196290
const lockfileBytes = buf('{"lockfileVersion":3}\n')
197291
const rootPackageJson = buf([
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
registry=https://registry.example.com/
2+
//registry.example.com/:_authToken=${NPM_TOKEN}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@scope:registry=https://npm.example.com/
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "main",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"license": "ISC"
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const { defineConfig } = require('@playwright/test')
2+
3+
module.exports = defineConfig({
4+
testDir: './tests'
5+
})
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
; A .npmrc in a subdirectory that is not a workspace package root. Package
2+
; managers never read .npmrc from below the install directory, so this must NOT
3+
; be bundled.
4+
registry=https://should-not-be-bundled.example.com/
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// A test that pulls in no dependencies; the point of this fixture is to
2+
// exercise .npmrc bundling, not the import graph.

packages/cli/src/services/check-parser/__tests__/check-parser-fixtures/npmrc-example/package-lock.json

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"name": "npmrc-example",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"license": "ISC",
7+
"workspaces": [
8+
"apps/*"
9+
]
10+
}

0 commit comments

Comments
 (0)