CI & automation
Store your token as a secret named VLT_TOKEN and export it. Every
config file in these guides reads the token from the environment, so
nothing else changes between your laptop and CI.
GitHub Actions
name: cion: [push, pull_request]
jobs: build: runs-on: ubuntu-latest env: VLT_TOKEN: ${{ secrets.VLT_TOKEN }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - run: npm ci - run: npm testCommit the .npmrc / bunfig.toml / .yarnrc.yml from the relevant
guide. Because they reference ${VLT_TOKEN} rather than a literal
token, they are safe in version control and need no rewriting step in
CI.
Per package manager
# npm — commit .npmrc with ${VLT_TOKEN}npm ci
# pnpm — pin to 11.26.0, and pass the token as a config env var (see below)env "pnpm_config_//registry.vlt.io/acme/npm/:_authToken=$VLT_TOKEN" \ pnpm install --frozen-lockfile
# yarn — commit .yarnrc.yml with ${VLT_TOKEN}yarn install --immutable
# bun — commit bunfig.toml with $VLT_TOKENbun install --frozen-lockfile
# deno — commit .npmrc with ${VLT_TOKEN}deno ci
# vltVLT_REGISTRY=https://registry.vlt.io/acme/npm/ vlt install --frozen-lockfileGenerating .npmrc at runtime instead
If you would rather not commit the file, write it in a step:
- run: | cat > .npmrc <<'EOF' registry=https://registry.vlt.io/acme/npm/ @acme:registry=https://registry.vlt.io/acme/main/ //registry.vlt.io/acme/npm/:_authToken=${VLT_TOKEN} //registry.vlt.io/acme/main/:_authToken=${VLT_TOKEN} EOFKeep the quotes on <<'EOF' so the shell leaves ${VLT_TOKEN} intact
for the package manager to expand.
pnpm needs a different approach
pnpm ignores ${VLT_TOKEN} in a project .npmrc on purpose, so the
committed-file pattern above does not carry credentials for it. Commit
only the routing, and supply the token as a config environment
variable:
- run: env "pnpm_config_//registry.vlt.io/acme/npm/:_authToken=$VLT_TOKEN" pnpm install --frozen-lockfileUse env rather than export: the variable name contains /, :
and ., which the shell will not accept in an assignment. This needs
pnpm 11.6.0 or newer; see
the pnpm guide for the full version
matrix.
Pin pnpm by exact version in packageManager, not by range. Corepack
resolves a range against its local cache first, so pnpm@11 can
produce different versions on different runners.
Two notes on the other tools
- deno —
deno ci(Deno 2.8+) is the equivalent ofnpm ci: it installs strictly fromdeno.lockand fails rather than updating it. - deno — Deno’s default 24-hour dependency-age gate will block a
package you published minutes earlier. If CI installs your own fresh
releases, configure
minimumDependencyAgeas shown in the deno guide.
Publishing from CI
Use a service token. When you create the token at
vlt.io → Tokens, choose This is a service
token rather than This token is for myself, and give it
package:read and package:write.
That choice is what makes unattended publishing work. A personal token answers every publish with a one-time-password challenge and fails in CI with:
npm error code EOTPnpm error This operation requires a one-time password.A service token has no such challenge — verified with npm, pnpm,
yarn, bun and vlt, all publishing to main with no prompt.
- run: npm publish env: VLT_TOKEN: ${{ secrets.VLT_TOKEN_CI }}Nothing else about the config changes; a service token is a drop-in replacement in every snippet in these guides. Store it as a separate secret from your personal token so it is obvious which one CI uses.
Three per-tool notes:
- yarn needs
yarn installbeforeyarn npm publish, or it aborts withThis package doesn't seem to be present in your lockfile. - pnpm needs
--no-git-checksin CI, since the checkout is often not on the publish branch. - deno publishes in two steps —
deno packthennpm publish <tarball>. Setpublish.includeindeno.jsonfirst, ordeno packmay bundle your.npmrcinto the tarball. See the deno guide.
If you are stuck with a personal token, vlt publish can take a
pre-obtained code:
VLT_OTP=<code> vlt publishRead-only installs of public packages
A job that installs only public packages from main needs no secret
at all — just the registry URL:
@acme:registry=https://registry.vlt.io/acme/main/The npm mirror always requires a token, so this only helps if every
one of your dependencies is a public package from main.
Checklist
VLT_TOKENis set as a masked secret, not a plaintext variable.- The committed config uses
${VLT_TOKEN}(or$VLT_TOKENforbunfig.toml) — never a literal token. - Lockfiles were regenerated after switching registries, so resolved
URLs point at
registry.vlt.io. - Installs use the frozen-lockfile flag for the package manager.