Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
- Checkly caches installed dependencies between runs, keyed off the workspace's lock file, every workspace member's `package.json` and `.npmrc` (whether or not the member is in the bundle), bundled pnpmfile contents, and the resolved `bundle.packages.embed` tarball set (filtered to what the pruned lockfile still references when pruning applied) — plus, as additional inputs, any synthesized placeholder manifests shipped in the bundle, any bundled manifests rewritten by `bundle.packages.prune` (hashed as shipped), and the pruned lockfile when pruning applied. Because the bundle-specific inputs follow the bundle, the key can change without a file edit — e.g. when a different set of workspace members ends up in the bundle. To force a reinstall declaratively, set `caching.dependencyCache.version` (a string or a safe integer) at the top level of `checkly.config.ts` (not per check — one code bundle serves all Playwright Check Suites) and change its value whenever the cache should be invalidated; scheduled checks pick up the change on the next `checkly deploy`. Unset or empty-string values leave the cache key unchanged, so a dynamic value such as `version: process.env.DEPENDENCY_CACHE_VERSION` is safe when the variable is not always set. For a one-off reinstall during an ad-hoc run, use the `--refresh-cache` flag available on the run/test commands (`checkly test`, `checkly pw-test`, `checkly trigger`, `checkly checks run`) instead; the config value is the persistent knob that also applies to deployed, scheduled checks.
- 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"]`.
- If dependencies come from a private registry that Checkly's infrastructure cannot reach (for example an intranet-only Nexus mirror), list them in `bundle.packages.embed` in `checkly.config.ts` — a top-level section: `bundle: { packages: { embed: ['@acme/private-utils', 'legacy-private-pkg@2.1.0'] } }`. Each entry is a package name (embeds every version found in the lockfile) or an exact `name@version` pin; names may contain `*` wildcards (`@acme/*`, `acme-*`, `@acme/*-utils`) where each `*` matches any run of characters except `/` (never crossing the scope separator); a `!` prefix (`!@acme/legacy`, `!@acme/*`, `!legacy@2.1.0`) turns an entry into an exclusion that removes the packages it matches from what the entries *before* it selected, so entries apply in order — `['@acme/*', '!@acme/legacy']` embeds the whole scope except `@acme/legacy`, while the reverse order embeds the whole scope because the exclusion runs before anything has been selected; as long as a spec matches at least one registry package, matches that cannot be embedded are skipped (workspace members silently, git/file/URL dependencies and integrity-less entries with a warning since the runner must fetch those itself), while a spec whose only matches cannot be embedded — or that matches nothing at all — is an error, except that exclusions never error (one that removes nothing is a no-op) and removing every package an earlier entry selected also silences that entry — no error, and no skip warning even for packages it matched but did not exclude, so use `DEBUG='checkly:cli:services:embedded-packages'` to see what such an entry reached; because exclusions only subtract, a list of nothing but `!` entries selects nothing, and a configuration whose entries select no packages at all is reported as a warning (packages dropped later by lockfile pruning are covered by the pruning note above); a pattern embeds every lockfile version of every package it matches, so scope it to the packages the runner genuinely cannot fetch. List every unreachable package by name, including private packages that only appear as transitive dependencies of other private packages — dependencies of listed packages are not embedded automatically. The CLI resolves entries against the workspace-root lockfile (`pnpm-lock.yaml`, `package-lock.json`, the text `bun.lock` or a Yarn Berry `yarn.lock` — Yarn Classic v1 lockfiles are not supported), reuses tarballs from local caches (its own, then npm's) or downloads them from the registry configured in `.npmrc` (see the credentials bullet below), verifies each against the lockfile's integrity hash, and ships them inside the code bundle at `.checkly/embedded-packages/*.tgz`, where the runner serves them through a local registry during install. Yarn Berry lockfiles record no npm tarball integrity (Berry checksums cover yarn's own cache format), so the CLI resolves the tarball integrity from the registry's package metadata instead — one small metadata request per embedded package on every deploy (the per-version route, falling back to the full packument), even when the tarballs themselves come from a warm cache, so a yarn embed needs registry reachability at deploy time even on a warm cache. When the bundled lockfile is pruned to the code bundle's contents (see the pruning bullet above), the embedded set follows it: packages the pruned lockfile no longer references — dependencies of workspace members that are not part of the bundle — are neither embedded nor downloaded, even if an entry matches them. If a package unexpectedly stops being embedded, the usual cause is that only a workspace member outside the bundle depends on it, in which case the runner never installs it and nothing is wrong; if the checks genuinely need it, make the depending member part of the bundle (import it from check code) rather than disabling pruning — `CHECKLY_LOCKFILE_PRUNE=0` restores the unfiltered set but reintroduces the over-describing lockfile that pruning exists to prevent, so treat it as a last resort. Downloads are cached under the workspace root's `node_modules/.cache/checkly` (in a monorepo that is the repo root, not the member package; override with `CHECKLY_CACHE_DIR`; a per-user cache dir is the fallback when the project location isn't writable), so nothing lands in the project outside `node_modules`. CI setups that cache `node_modules` — or platforms that preserve `node_modules/.cache` — persist the tarballs automatically; otherwise persist `CHECKLY_CACHE_DIR` in CI to avoid re-downloading (note `npm ci` deletes `node_modules` wholesale, unlike incremental pnpm installs). The machine running `checkly deploy`/`test` needs registry access on a cold cache — but only for the tarballs actually shipped, not for pruned-away ones. Changing the resolved set of embedded packages invalidates the runner's dependency cache, so the next run reinstalls with the new tarballs. Applies to Playwright Check Suites only, not browser or multistep checks.
- Credentials for a private registry (`bundle.packages.embed`) must be scoped to a registry — the `//host/path/:_authToken` form, not a bare `_authToken` at the top of the file, which npm itself rejects with `ERR_INVALID_AUTH` and `npm config fix`. They come from `npm_config_*` environment variables, the project, workspace-root and user `.npmrc` files, and pnpm's global `auth.ini` — where `pnpm login` writes tokens on pnpm 11+, outranking the user `.npmrc` for pnpm projects and acting as a fallback for others. Scope-qualified keys (`//registry.example.com/:@acme:_authToken`, the spelling `pnpm login --scope=@acme` writes) are honored for packages in that scope in any project, whichever package manager it uses, and every scoped key is tried before any unscoped one regardless of path depth; pnpm's alternative spelling (`//registry.example.com/@acme/:_authToken`) is honored after the unscoped keys. A key that cannot be used is never quietly swapped for a different one: an unset `${VAR}` fails the download and names the variable, and a key left blank counts as absent for its own credential but still masks that same key elsewhere, matching npm. Keys that could not have applied to the request at all are skipped, visible only under `DEBUG='checkly:cli:services:embedded-packages'`. A `registry` that is blank or otherwise unusable is a broken setting rather than an absent one, so the download fails and names the key instead of falling back to the public registry and disclosing a private package name to it, though a blank `@scope:registry` does fall back to a usable global `registry`. bun or yarn users whose registry credentials live solely in `bunfig.toml` or `.yarnrc.yml` must duplicate them into `.npmrc` or set `npm_config_*`, or downloads fail with an auth error. A download that fails to authenticate names where the credentials came from — a config key and its file, an environment variable, the lockfile that recorded the URL, or the registry that issued it — so tell the user to read the error rather than guessing which source to edit.
- Credentials for a private registry (`bundle.packages.embed`) must be scoped to a registry — the `//host/path/:_authToken` form, not a bare `_authToken` at the top of the file, which npm itself rejects with `ERR_INVALID_AUTH` and `npm config fix`. Both spellings of a registry key are accepted — with and without the trailing slash (`//host/path:_authToken`) — with the slash-terminated one winning at the same depth, matching npm's probe order and pnpm's load-time normalisation. They come from `npm_config_*` environment variables, the project, workspace-root and user `.npmrc` files, and pnpm's global `auth.ini` — where `pnpm login` writes tokens on pnpm 11+, outranking the user `.npmrc` for pnpm projects and acting as a fallback for others. Scope-qualified keys (`//registry.example.com/:@acme:_authToken`, the spelling `pnpm login --scope=@acme` writes) are honored for packages in that scope in any project, whichever package manager it uses, and every scoped key is tried before any unscoped one regardless of path depth; pnpm's alternative spelling (`//registry.example.com/@acme/:_authToken`) is honored after the unscoped keys. A key that cannot be used is never quietly swapped for a different one: an unset `${VAR}` fails the download and names the variable, and a key left blank counts as absent for its own credential but still masks that same key and spelling elsewhere, matching npm — the other slash-spelling of the key is still consulted. Keys that could not have applied to the request at all are skipped, visible only under `DEBUG='checkly:cli:services:embedded-packages'`. A `registry` that is blank or otherwise unusable is a broken setting rather than an absent one, so the download fails and names the key instead of falling back to the public registry and disclosing a private package name to it, though a blank `@scope:registry` does fall back to a usable global `registry`. bun or yarn users whose registry credentials live solely in `bunfig.toml` or `.yarnrc.yml` must duplicate them into `.npmrc` or set `npm_config_*`, or downloads fail with an auth error. A download that fails to authenticate names where the credentials came from — a config key and its file, an environment variable, the lockfile that recorded the URL, or the registry that issued it — so tell the user to read the error rather than guessing which source to edit.
- To remove dependencies from the `package.json` files shipped in the code bundle — the workspace root's and every bundled member's; files on disk are never modified — set `bundle.packages.prune` in `checkly.config.ts`: a top-level section like `bundle: { packages: { prune: ['@acme/*'] } }`. It accepts either an array of package name patterns, removed from every dependency class (`dependencies`, `devDependencies`, `peerDependencies` and `optionalDependencies`), or an object keyed by dependency class whose values are `true` (remove the whole class) or a pattern array scoped to that class — e.g. `prune: { peerDependencies: true }` or `prune: { peerDependencies: ['@acme/*'], devDependencies: true }`. Names may contain `*` wildcards with embed's semantics (each `*` matches any run of characters except `/`, never crossing the scope separator — so a bare `*` matches only unscoped names; to remove a whole class use `true`, not `['*']`), and a `!` prefix turns an entry into an exclusion with embed's order-sensitive semantics — `['@acme/*', '!@acme/keep']` removes the scope except `@acme/keep`, while the reverse order removes the whole scope because the exclusion runs before anything has been selected; to remove a whole class *except* some packages, select everything first (`['*', '@*/*', '!@acme/keep']` — `true` cannot be combined with exclusions); unlike `bundle.packages.embed` there are no `name@version` pins — they are rejected at config load. Removed `peerDependencies` take their `peerDependenciesMeta` entries with them, and `peerDependencies: true` clears `peerDependenciesMeta` entirely. The bundled manifests are rewritten before the bundled lockfile is pruned, so the removed dependencies fall out of the regenerated lockfile too (and tarballs they pulled in are dropped from the embedded set); this is the escape hatch for dependencies lockfile pruning alone cannot drop — with pnpm's `auto-install-peers` (default since pnpm 8), a bundled member's unused mandatory `peerDependencies` are re-promoted into the regenerated lockfile even though the bundled code never imports them, so pruning them from the bundled manifest is the way to keep unreachable/private peers out of the remote install. Because rewritten manifests only make sense next to a matching lockfile, the option requires lockfile pruning to succeed in the same run: when a lockfile is bundled but cannot be pruned — an unsupported package manager, a failed regeneration, or `CHECKLY_LOCKFILE_PRUNE=0` — the original manifests ship unchanged with a warning (a bundle that ships no lockfile keeps the pruned manifests, since nothing can fall out of sync). A pattern that matches nothing is not an error, and pruning is not validated against the code: the user is responsible for not removing anything the bundled code needs at runtime. Changing the pruned output invalidates the runner's dependency cache. Applies to Playwright Check Suites only.

## Install troubleshooting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -858,4 +858,101 @@ describe('resolveAuthHeader()', () => {
expect(() => resolveAuthHeader(config, 'https://nexus.local/foo', 'foo', {}))
.toThrow(NpmrcEnvVarError)
})

it('matches a slashless _authToken at the host root', () => {
// The hand-written spelling without the trailing slash, which npm and
// pnpm both accept: npm probes both forms at every depth, and pnpm
// normalises this one to the canonical form at config load.
const config = parseNpmrc('//nexus.local:_authToken=slashless-secret')
const auth = resolveAuthHeader(config, 'https://nexus.local/foo/-/foo-1.0.0.tgz', 'foo', {})
expect(auth?.header).toBe('Bearer slashless-secret')
// The matched spelling is reported verbatim: it is the literal config
// key, which is what `origins` is keyed by.
expect(auth?.keys).toEqual(['//nexus.local:_authToken'])
})

it('matches a slashless key deeper in the path', () => {
const config = parseNpmrc('//nexus.local/repository/npm:_authToken=deep-slashless-secret')
const url = 'https://nexus.local/repository/npm/foo/-/foo-1.0.0.tgz'
expect(resolveAuthHeader(config, url, 'foo', {})?.header).toBe('Bearer deep-slashless-secret')
})

it('prefers the slash-terminated spelling at the same depth', () => {
// npm's probe order: `//host/a/` before `//host/a`.
const config = parseNpmrc([
'//nexus.local/repository:_authToken=slashless-secret',
'//nexus.local/repository/:_authToken=canonical-secret',
].join('\n'))
expect(resolveAuthHeader(config, 'https://nexus.local/repository/foo', 'foo', {})?.header)
.toBe('Bearer canonical-secret')
})

it('lets a deeper slashless key win over a shallower canonical one', () => {
// The spellings are probed per depth, not the canonical walk first.
const config = parseNpmrc([
'//nexus.local/:_authToken=shallow-canonical-secret',
'//nexus.local/repository/npm:_authToken=deep-slashless-secret',
].join('\n'))
expect(resolveAuthHeader(config, 'https://nexus.local/repository/npm/foo', 'foo', {})?.header)
.toBe('Bearer deep-slashless-secret')
})

it('falls through a blank canonical spelling to the slashless one', () => {
// npm's walk tests each probe for truthiness, so a blank entry under
// the slash-terminated spelling does not stop it reading the slashless
// one right behind it.
const config = parseNpmrc([
'//nexus.local/repository/:_authToken=',
'//nexus.local/repository:_authToken=works',
].join('\n'))
expect(resolveAuthHeader(config, 'https://nexus.local/repository/foo', 'foo', {})?.header)
.toBe('Bearer works')
})

it('matches a slashless scope-qualified key', () => {
const config = parseNpmrc('//nexus.local:@acme:_authToken=scoped-slashless-secret')
expect(resolveAuthHeader(config, 'https://nexus.local/@acme/foo', '@acme/foo', {})?.header)
.toBe('Bearer scoped-slashless-secret')
})

it('matches a slashless path-form key', () => {
const config = parseNpmrc('//npm.pkg.github.com/@acme:_authToken=path-form-slashless-secret')
const url = 'https://npm.pkg.github.com/download/@acme/foo/1.0.0/abcdef'
expect(resolveAuthHeader(config, url, '@acme/foo', {})?.header)
.toBe('Bearer path-form-slashless-secret')
})

it('never pairs a username with a _password under the other spelling', () => {
// Both halves must share one exact prefix — the spellings are distinct
// prefixes like any other. npm behaves the same, since its walk checks
// one probe at a time.
const config = parseNpmrc([
'//nexus.local/:username=user',
`//nexus.local:_password=${Buffer.from('pass').toString('base64')}`,
].join('\n'))
expect(resolveAuthHeader(config, 'https://nexus.local/foo', 'foo', {})).toBeUndefined()
})

it('reports an unset ${VAR} in a slashless key instead of a shallower credential', () => {
// A slashless key used to be invisible, so the shallower credential
// went out. Now the key plainly applies to the request, which makes
// its missing variable fatal like any other applicable key's — quietly
// sending a different credential would authenticate as an identity the
// config asked to replace.
const config = parseNpmrc([
'//nexus.local/repository:_authToken=${UNSET_TOKEN}',
'//nexus.local/:_authToken=working-secret',
].join('\n'))
expect(() => resolveAuthHeader(config, 'https://nexus.local/repository/foo', 'foo', {}))
.toThrow(NpmrcEnvVarError)
})

it('skips a slashless path-form key whose ${VAR} is unset when the URL never touches that path', () => {
// The slashless spelling gets the same tolerance as the canonical one:
// the key names a location this request never touches, so a variable
// missing from it must not abort the download.
const config = parseNpmrc('//nexus.local/@acme:_authToken=${UNSET_TOKEN}')
const url = 'https://nexus.local/repository/npm/@acme/foo/-/foo-1.0.0.tgz'
expect(resolveAuthHeader(config, url, '@acme/foo', {})).toBeUndefined()
})
})
Loading
Loading