Bun 13 ⚡ CI/CD Integration with Immutable and Offline Installs
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
| Command | Purpose |
|---|---|
bun install --frozen-lockfile | Fail if lockfile would change |
bun ci | Alias for frozen install |
bun install --ignore-scripts | Skip all lifecycle scripts |
bun pm cache | Print cache directory |
GitHub Actions
| Action | Purpose |
|---|---|
oven-sh/setup-bun@v2 | Install Bun |
bun-version input | latest, canary, or specific |
actions/cache@v4 | Restore cache across runs |
Lifecycle Script Modes
package.json | Behavior |
|---|---|
| Omitted | Default allow list (npm only) |
["pkg-a"] | Only listed packages |
[] | No packages |
Cache Path
| Platform | Default 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| CI fails with frozen lockfile | Lockfile out of sync | Run bun install locally and commit |
| Native module fails | Lifecycle script blocked | Add to trustedDependencies |
| Cache not restored | Wrong cache path | Use bun pm cache output |
| Lockfile ignored in Docker | Missing --frozen-lockfile | Add the flag to the Dockerfile |
| Trust list ignored | Workspace mismatch | Lockfile 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
| Item | Value |
|---|---|
| CI install | bun install --frozen-lockfile |
| Alias | bun ci |
| Config setting | frozenLockfile = true in bunfig.toml |
| GitHub Action | oven-sh/setup-bun@v2 |
| Cache command | bun pm cache |
| Lifecycle default | Curated allow list (npm only) |
| Trust override | trustedDependencies in package.json |
| Ignore scripts | --ignore-scripts |
| Offline strategy | Pre-populate cache |
Key takeaways:
bun install --frozen-lockfileis the CI contract. It fails if the lockfile would change, forcing the developer to commit the resolution before the pipeline can proceed. Thebun cialias and thefrozenLockfilesetting provide the same behavior .- The official action is
oven-sh/setup-bun@v2. Thebun-versioninput acceptslatest,canary, or a specific version. The action does not configure caching; the cache is restored withactions/cacheusing the path frombun pm cache. - Lifecycle scripts are blocked by default. Bun’s “default-secure” model only runs scripts for packages on the allow list. Defining
trustedDependenciesreplaces 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-lockfilerespects the lockfile’s record, not the default list . - Offline installs require cache preparation. Bun does not have a
--offlineflag. 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.
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!