| | |

Bun 13 ⚡ CI/CD Integration with Immutable and Offline Installs

⏱️ Estimated read: 10 min | |

Bun’s CI/CD story is built on a single command: bun install --frozen-lockfile. The flag ensures the lockfile is respected and never rewritten during the install. Unlike pnpm, which has a separate --offline mode that requires pre-populated caches, Bun’s approach to reproducibility is simpler: the lockfile is the contract, and the global cache is the optimization. The exam tests the flag, the setup-bun action, and the security model that governs lifecycle scripts in CI. The critical distinction is between bun install (which may update the lockfile) and bun install --frozen-lockfile (which fails if the lockfile is out of sync). The bun ci command is an alias for the frozen variant .

Key point: bun install --frozen-lockfile is the CI install command. It installs exactly what the lockfile specifies and fails if package.json and bun.lock disagree. The official GitHub Action is oven-sh/setup-bun@v2. Lifecycle scripts are blocked by default unless the package is listed in trustedDependencies or is on Bun’s curated default allow list.


Why frozen lockfiles and CI-specific behavior matter

The silent drift problem. In a local environment, bun install after adding a dependency without updating the lockfile resolves the new dependency and rewrites the lockfile. This is convenient for development but dangerous in CI: the build succeeds with a dependency tree that was never reviewed, committed, or tested. The --frozen-lockfile flag closes this gap by making the install a verification step rather than a resolution step .

The lifecycle script security problem. Bun is “default-secure”: it only runs preinstall and postinstall scripts for packages on an allow list. The default list covers popular npm packages like esbuild and sharp. Packages from file:, link:, git:, or github: sources are never trusted implicitly, even if their name matches an entry on the default list. This prevents malicious packages from spoofing trusted names through local paths or Git repositories . In CI, the lockfile records the trusted dependencies list. A bun install --frozen-lockfile in a checkout without the declaring workspace still respects the lockfile’s record, not the default list .

The cache problem. Bun’s global cache is the mechanism that avoids re-downloading packages across CI runs. The cache is not committed to the repository; it is restored through the CI platform’s caching mechanism. The official setup-bun action does not configure caching by itself, but the cache directory can be restored with actions/cache. The cache path is printed by bun pm cache .

The offline problem. Bun does not have a dedicated --offline flag comparable to pnpm’s. The global cache is used automatically when packages are present. For air-gapped environments, the recommended approach is to populate the cache before sealing the environment, or to use a tool like bun2nix for deterministic offline installs with Nix. A feature request for a Yarn-style Zero-Installs model (vendoring the cache into the repository) exists but is not yet implemented .

The trade-off. Frozen lockfiles make CI deterministic but require the lockfile to be committed and up to date. The lifecycle script security model prevents supply-chain attacks but requires explicit trust for packages with build steps. The cache speeds up installs but adds a cache key management step. Each mechanism is a layer of reliability.


a. The frozen install and the bun ci alias

The bun install --frozen-lockfile command is the CI install. It reads bun.lock and installs the exact versions recorded. If package.json has changed in a way that would require a lockfile update, the command fails with an error .

# CI install
bun install --frozen-lockfile

# Alias (equivalent)
bun ci

The bun ci command is functionally identical and is the recommended form for CI pipelines. The frozenLockfile = true setting in bunfig.toml makes the behavior the default for every install in the project . This is useful for projects that want to enforce the frozen behavior even for developers running bun install locally.

The failure mode is deliberate: the developer must run bun install locally, commit the updated bun.lock, and push. The CI pipeline then passes on the next run. The error message identifies the mismatch, and the fix is always the same.


b. The official GitHub Action and caching

The oven-sh/setup-bun@v2 action installs Bun in the runner. The bun-version input accepts latest, canary, or a specific version like 1.3.3 .

- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
  with:
    bun-version: latest
- run: bun install --frozen-lockfile

The setup-bun action does not configure caching automatically. The cache directory is at ~/.bun/install/cache on Linux. The actions/cache action restores it across runs, keyed by the lockfile hash.

- name: Get Bun cache directory
  id: bun-cache
  run: echo "dir=$(bun pm cache)" >> $GITHUB_OUTPUT

- uses: actions/cache@v4
  with:
    path: ${{ steps.bun-cache.outputs.dir }}
    key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
    restore-keys: |
      ${{ runner.os }}-bun-

The bun pm cache command prints the cache directory path. The cache key includes the lockfile hash. When the lockfile changes, the cache is invalidated and a fresh cache is populated .

For Docker builds, the oven/bun image is the official base. The install is a layer in the Dockerfile, and the --frozen-lockfile flag ensures the lockfile is respected inside the container .


c. Lifecycle scripts and the trust model

Bun’s lifecycle script behavior is the most consequential CI-specific detail. The trustedDependencies field in package.json controls which packages can run preinstall and postinstall scripts.

{
  "trustedDependencies": ["esbuild", "sharp"]
}

The behavior has three modes. If trustedDependencies is omitted, the default list applies (npm sources only). If it is defined with a list, only the listed packages are trusted, and the default list is ignored entirely. If it is defined as an empty array, no packages are trusted, including the ones on the default list .

This is a security feature. A package installed from a git: URL that happens to be named esbuild will not run its scripts just because esbuild is on the default list. The package must be explicitly listed in trustedDependencies . In CI, the lockfile records the trusted list, so bun install --frozen-lockfile in a checkout without the declaring workspace still respects the lockfile’s record .

The --ignore-scripts flag is the escape hatch for CI environments that want to skip all lifecycle scripts regardless of the trust configuration. The trustedDependencies: [] setting is the per-project equivalent.


Complete Example Session

# ============================================
# PART 1: CONFIGURE THE FROZEN LOCKFILE
# ============================================

# bunfig.toml
[install]
frozenLockfile = true
# ============================================
# PART 2: LOCAL INSTALL AND COMMIT
# ============================================

bun install
git add bun.lock
git commit -m "Update lockfile"
git push
# ============================================
# PART 3: CREATE THE CI WORKFLOW
# ============================================

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
        with:
          bun-version: latest
      - run: bun install --frozen-lockfile
      - run: bun test
# ============================================
# PART 4: ADD CACHING
# ============================================

- name: Get Bun cache
  id: bun-cache
  run: echo "dir=$(bun pm cache)" >> $GITHUB_OUTPUT

- uses: actions/cache@v4
  with:
    path: ${{ steps.bun-cache.outputs.dir }}
    key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }}
# ============================================
# PART 5: CONFIGURE TRUSTED DEPENDENCIES
# ============================================

# package.json
{
  "trustedDependencies": ["esbuild"]
}
# ============================================
# PART 6: OBSERVE FROZEN LOCKFILE FAILURE
# ============================================

# Add a dependency without updating the lockfile:
bun add lodash
# Do not commit bun.lock.
# CI runs:
bun install --frozen-lockfile
# Error: lockfile would be modified.
# ============================================
# PART 7: FIX THE FAILURE
# ============================================

git add bun.lock
git commit -m "Update lockfile for lodash"
git push
# CI passes on the next run.
# ============================================
# PART 8: DOCKER BUILD
# ============================================

# Dockerfile
FROM oven/bun:latest
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
CMD ["bun", "run", "index.ts"]
# ============================================
# PART 9: OFFLINE PREPARATION (AIR-GAPPED)
# ============================================

# Populate the cache before sealing the environment:
bun install
# The cache is at ~/.bun/install/cache.
# Copy the cache to the target environment.
# Run:
bun install --frozen-lockfile

The nine parts trace the CI workflow: frozen lockfile configuration, local install, workflow creation, caching, trust configuration, failure observation, failure fix, Docker build, and offline preparation.


Quick Reference

CI Commands

CommandPurpose
bun install --frozen-lockfileFail if lockfile would change
bun ciAlias for frozen install
bun install --ignore-scriptsSkip all lifecycle scripts
bun pm cachePrint cache directory

GitHub Actions

ActionPurpose
oven-sh/setup-bun@v2Install Bun
bun-version inputlatest, canary, or specific
actions/cache@v4Restore cache across runs

Lifecycle Script Modes

package.jsonBehavior
OmittedDefault allow list (npm only)
["pkg-a"]Only listed packages
[]No packages

Cache Path

PlatformDefault Cache Directory
Linux~/.bun/install/cache
macOS~/.bun/install/cache
Windows%USERPROFILE%\.bun\install\cache

Best Practices

✅ Do This:

bun install --frozen-lockfile
<!-- Explicit frozen flag in CI -->                   # ✅
- uses: oven-sh/setup-bun@v2
<!-- Use the official action -->                      # ✅
{ "trustedDependencies": ["esbuild"] }
<!-- Explicitly trust packages with build scripts --> # ✅

❌ Don’t Do This:

# Run bun install without frozen flag in CI
<!-- Lockfile may be rewritten silently -->          # ❌
# Assume lifecycle scripts run by default
<!-- Bun blocks them unless trusted -->              # ❌
# Hardcode the cache path
<!-- Use bun pm cache to discover it -->             # ❌

Common Pitfalls

PitfallWhy It HappensFix
CI fails with frozen lockfileLockfile out of syncRun bun install locally and commit
Native module failsLifecycle script blockedAdd to trustedDependencies
Cache not restoredWrong cache pathUse bun pm cache output
Lockfile ignored in DockerMissing --frozen-lockfileAdd the flag to the Dockerfile
Trust list ignoredWorkspace mismatchLockfile records the list; commit it

Real-World Examples

1. Basic CI Install

bun install --frozen-lockfile

2. bun ci Alias

bun ci

3. GitHub Action

- uses: oven-sh/setup-bun@v2

4. Version Pin

- uses: oven-sh/setup-bun@v2
  with:
    bun-version: 1.3.3

5. Cache Setup

- run: echo "dir=$(bun pm cache)" >> $GITHUB_OUTPUT

6. Trust Dependencies

{ "trustedDependencies": ["sharp"] }

7. Ignore Scripts

bun install --ignore-scripts

8. Frozen in Docker

RUN bun install --frozen-lockfile

9. Offline Preparation

bun install
# Copy ~/.bun/install/cache to target.

10. Frozen Config

[install]
frozenLockfile = true

Visual

CI Install Decision

┌──────────────────────────────────────────────┐
│  bun install --frozen-lockfile               │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Lockfile matches package.json?              │
│  ├─ YES → Install exact versions             │
│  └─ NO  → FAIL                               │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Trusted dependencies?                       │
│  ├─ YES → Run lifecycle scripts              │
│  └─ NO  → Skip scripts                       │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  Cache warm?                                 │
│  ├─ YES → Link from cache                    │
│  └─ NO  → Download and cache                 │
└──────────────────────────────────────────────┘

Lifecycle Script Modes

┌──────────────────────────────────────────────┐
│  trustedDependencies omitted                 │
│  → Default list (npm only)                   │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  trustedDependencies: ["esbuild"]            │
│  → Only esbuild (default list ignored)       │
└──────────────────┬───────────────────────────┘
                   │
                   ▼
┌──────────────────────────────────────────────┐
│  trustedDependencies: []                     │
│  → No packages (including default list)      │
└──────────────────────────────────────────────┘

Summary

ItemValue
CI installbun install --frozen-lockfile
Aliasbun ci
Config settingfrozenLockfile = true in bunfig.toml
GitHub Actionoven-sh/setup-bun@v2
Cache commandbun pm cache
Lifecycle defaultCurated allow list (npm only)
Trust overridetrustedDependencies in package.json
Ignore scripts--ignore-scripts
Offline strategyPre-populate cache

Key takeaways:

  • bun install --frozen-lockfile is the CI contract. It fails if the lockfile would change, forcing the developer to commit the resolution before the pipeline can proceed. The bun ci alias and the frozenLockfile setting provide the same behavior .
  • The official action is oven-sh/setup-bun@v2. The bun-version input accepts latest, canary, or a specific version. The action does not configure caching; the cache is restored with actions/cache using the path from bun pm cache .
  • Lifecycle scripts are blocked by default. Bun’s “default-secure” model only runs scripts for packages on the allow list. Defining trustedDependencies replaces the default list entirely; an empty array trusts no packages .
  • The lockfile records the trust list. In a CI checkout without the declaring workspace, bun install --frozen-lockfile respects the lockfile’s record, not the default list .
  • Offline installs require cache preparation. Bun does not have a --offline flag. The cache is populated before the environment is sealed, and the install uses it automatically .

Remember: Bun’s CI/CD model is built on the lockfile. The --frozen-lockfile flag is the gate. The setup-bun action is the installer. The trustedDependencies field is the security boundary. The cache is the optimization. The lockfile is the source of truth.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!