Skip to content

fix(pnpm-store): create store dir at build time to unblock later features#33

Merged
baxyz merged 2 commits into
mainfrom
july-fix
Jun 1, 2026
Merged

fix(pnpm-store): create store dir at build time to unblock later features#33
baxyz merged 2 commits into
mainfrom
july-fix

Conversation

@baxyz

@baxyz baxyz commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Problem

install.sh writes store-dir=/workspaces/.pnpm-store into ~/.npmrc during the Docker image build. But /workspaces does not exist at build time (it's mounted later by the container runtime). Any subsequent pnpm invocation by a later feature — notably vite-plus — fails with:

error: Failed to install dependencies. See log for details: /home/node/.vite-plus/0.1.24/install.log

because pnpm tries to create /workspaces/.pnpm-store as a non-root user and cannot.

Fix

Add mkdir -p "${STORE_DIR}" (as root) in install.sh, right after writing to ~/.npmrc. This ensures the directory exists during the Docker build so pnpm can use the configured path immediately.

The named volume declared in devcontainer-feature.json shadows this empty directory at container start — that's intentional. The persistent volume is used at runtime; the image-layer directory is just a placeholder to keep the build working.

Changes

  • src/pnpm-store/install.sh: create STORE_DIR during build, chown to remote user
  • src/pnpm-store/devcontainer-feature.json: bump to v1.0.2

Related

Supersedes / resolves the same root cause as #32.

…atures

mkdir STORE_DIR during the Docker image build (as root) so any pnpm invocation
by a later feature (e.g. vite-plus) can use the configured store-dir path.
Without this, /workspaces/.pnpm-store is written to ~/.npmrc but /workspaces
doesn't exist yet at build time, causing pnpm to fail with 'Failed to install
dependencies'.

The named volume declared in devcontainer-feature.json shadows this empty
directory at container start, so pnpm uses the persistent volume at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown

✅ PR Validation Passed

All checks passed!


📋 Pipeline Status

Job Status
🧾 Conventional Commits passing
🧪 Feature Tests passing
🐚 ShellCheck passing

🤖 Generated by @helpers4 CI • 2026-06-01

Comment thread src/pnpm-store/install.sh Outdated
# pnpm cannot create the store directory as a non-root user.
# The named volume declared in devcontainer-feature.json shadows this directory
# at container start — that's intentional.
mkdir -p "${STORE_DIR}"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Altitude: this mkdir is a bandaid on a build-time/runtime mismatch

The root cause is that store-dir=/workspaces/.pnpm-store is written to ~/.npmrc at image-build time, but the path only exists at container start (after the named volume mounts). This mkdir patches the symptom so later build-time pnpm invocations (e.g. vite-plus) do not crash.

A deeper fix would defer the ~/.npmrc write to postCreateCommand — pnpm is not invoked at build time by this feature itself, so there is no need to configure it during the build. The named volume would then be the source of truth from the start.

That is a more invasive change; keeping the mkdir is a valid pragmatic call. If kept, it should at minimum add || true to prevent an image build abort on filesystems where root cannot create /workspaces (read-only overlay layers, unusual base images):

Suggested change
mkdir -p "${STORE_DIR}"
mkdir -p "${STORE_DIR}" || true
if [ "${USERNAME}" != "root" ]; then
chown "${USERNAME}:${USER_GROUP}" "${STORE_DIR}" 2>/dev/null || true
fi
echo " ✅ Created store directory at ${STORE_DIR}"

@baxyz

baxyz commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Code review findings — guard script (pre-existing lines)

These bugs exist in the guard script body (embedded in install.sh via heredoc). They are not in the diff of this PR but are part of the code being shipped.


🐛 Bug 1 — No-sudo path: store stays root-owned with no warning (CONFIRMED)

# src/pnpm-store/install.sh, guard body ~line 125
if [ -n "${store_owner}" ] && [ "${store_owner}" != "$(id -u)" ] && command -v sudo >/dev/null 2>&1; then
    sudo chown -R "$(id -u):$(id -g)" "${STORE_DIR}" 2>/dev/null || true
fi

When sudo is absent the entire if is skipped — no else, no warning. Named volumes start root-owned; the store stays root-owned; pnpm gets EACCES on first write with nothing in the logs.

Fix:

if [ -n "${store_owner}" ] && [ "${store_owner}" != "$(id -u)" ]; then
    if command -v sudo >/dev/null 2>&1; then
        sudo chown -R "$(id -u):$(id -g)" "${STORE_DIR}" 2>/dev/null ||             echo "⚠️  pnpm-store: chown failed — pnpm may not be able to write to the store"
    else
        echo "⚠️  pnpm-store: store is root-owned and sudo is unavailable; pnpm writes will fail (EACCES)"
    fi
fi

🐛 Bug 2 — 2>/dev/null || true on chown swallows failure → false ✅ (CONFIRMED)

sudo chown -R "$(id -u):$(id -g)" "${STORE_DIR}" 2>/dev/null || true

If chown fails (sudoers policy, read-only volume), the error is silenced and || true forces exit 0. The guard then prints ✅ pnpm-store: pnpm store-dir = /workspaces/.pnpm-store — a false success. pnpm will fail later with EACCES.

Remove 2>/dev/null at minimum, or check the result and emit a warning (covered by the fix above).


🐛 Bug 3 — stat failure → store_owner empty → chown silently skipped (CONFIRMED)

store_owner="$(stat -c '%u' "${STORE_DIR}" 2>/dev/null || echo '')"
if [ -n "${store_owner}" ] && ...

stat can fail on NFS (root-squash), overlay FS, or restricted bind-mounts. The || echo '' fallback sets store_owner to empty; the [ -n ] guard is false; chown is silently skipped. Same silent EACCES result.

Fix: treat an empty store_owner as "unknown, attempt chown anyway":

store_owner="$(stat -c '%u' "${STORE_DIR}" 2>/dev/null || echo 'unknown')"
if [ "${store_owner}" != "$(id -u)" ]; then
    ...
fi

⚠️ Risk — Hardcoded volume name helpers4-pnpm-store shared across all projects on the host

{ "source": "helpers4-pnpm-store", "target": "/workspaces/.pnpm-store", "type": "volume" }

Any two devcontainers on the same Docker host that use this feature (regardless of project) share one volume. pnpm's content-addressable store format changed between major versions — pnpm 8 and pnpm 9 stores are not fully compatible. If project A uses pnpm 9 and project B uses pnpm 8, one will corrupt the other's store.

Consider namespacing the volume name (e.g. helpers4-pnpm-store-v9) or documenting this as a known limitation.

…ling

Three bugs in the postCreateCommand guard:
- stat failure (NFS, overlay FS) returned empty store_owner, silently skipping
  chown and leaving the store root-owned with no diagnostic
- sudo absent: the chown if-block was skipped entirely with no warning, causing
  pnpm EACCES on first write with nothing in the logs
- sudo chown used 2>/dev/null || true, swallowing failures and printing a false
  success checkmark regardless of outcome

Fix: treat stat failure as "unknown" (attempt chown anyway), split the sudo
check to emit a warning when sudo is absent, and surface chown failures instead
of swallowing them.

Also add || true to the build-time mkdir -p to prevent aborting the image build
on unusual filesystems where root cannot create /workspaces.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@baxyz
baxyz merged commit 66ded93 into main Jun 1, 2026
27 checks passed
@baxyz
baxyz deleted the july-fix branch June 1, 2026 22:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant