diff --git a/.devcontainer/devcontainer-lock.json b/.devcontainer/devcontainer-lock.json index 219acb6..8b0d4f1 100644 --- a/.devcontainer/devcontainer-lock.json +++ b/.devcontainer/devcontainer-lock.json @@ -1,3 +1,24 @@ { - "features": {} + "features": { + "ghcr.io/synergy-shock/devcontainer-features/claude:2": { + "version": "2.1.145", + "resolved": "ghcr.io/synergy-shock/devcontainer-features/claude@sha256:4ad94850485d53ec0a7ccf1fbef5e48286dde8be1811bdb926434637085e0c4e", + "integrity": "sha256:4ad94850485d53ec0a7ccf1fbef5e48286dde8be1811bdb926434637085e0c4e" + }, + "ghcr.io/synergy-shock/devcontainer-features/gitbutler:0": { + "version": "0.1.0", + "resolved": "ghcr.io/synergy-shock/devcontainer-features/gitbutler@sha256:28e79b97e473e948f1d4ff8d35690ef1191f1f0bf2bb3f0df398bfb507b16ae2", + "integrity": "sha256:28e79b97e473e948f1d4ff8d35690ef1191f1f0bf2bb3f0df398bfb507b16ae2" + }, + "ghcr.io/synergy-shock/devcontainer-features/pnpm:11": { + "version": "11.0.0", + "resolved": "ghcr.io/synergy-shock/devcontainer-features/pnpm@sha256:9ad82ac4ab3b0cf1d8422adbfd09ce61b12d99acd21d5083718ee55c87d9a2f9", + "integrity": "sha256:9ad82ac4ab3b0cf1d8422adbfd09ce61b12d99acd21d5083718ee55c87d9a2f9" + }, + "ghcr.io/synergy-shock/devcontainer-features/rtk:0": { + "version": "0.1.0", + "resolved": "ghcr.io/synergy-shock/devcontainer-features/rtk@sha256:69779045064cdc8a6d92461949d15974d0e19bc78d0f2b791969d5d719f392d4", + "integrity": "sha256:69779045064cdc8a6d92461949d15974d0e19bc78d0f2b791969d5d719f392d4" + } + } } diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 3437a17..62314b1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,18 +1,20 @@ { "name": "Devcontainer Features", - "image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie", + "image": "mcr.microsoft.com/devcontainers/base:trixie", "features": { - "../src/pnpm": {}, - "../src/gitbutler": {}, - // "../src/opencode": {}, - "../src/rtk": {}, - "../src/claude": {}, + "ghcr.io/devcontainers/features/common-utils:2": {}, + "ghcr.io/devcontainers/features/git:1": {}, + "ghcr.io/devcontainers/features/git-lfs:1": {}, + "ghcr.io/devcontainers/features/node:2": { + "version": "24", + "npmVersion": "latest", + "pnpmVersion": "latest", + }, + "ghcr.io/synergy-shock/devcontainer-features/claude:2": {}, + "ghcr.io/synergy-shock/devcontainer-features/rtk:0": {}, + "ghcr.io/synergy-shock/devcontainer-features/gitbutler:0": {}, }, "mounts": [ - "source=pnpm-store,target=/home/node/.pnpm-store,type=volume", - "source=${localEnv:HOME}/.npmrc,target=/home/node/.npmrc,type=bind,readonly", - // "source=${localEnv:HOME}/.config/opencode,target=/home/node/.config/opencode,type=bind", - // "source=${localEnv:HOME}/.local/share/opencode,target=/home/node/.local/share/opencode,type=bind", "source=${localEnv:HOME}/.claude,target=/home/node/.claude,type=bind", "source=${localEnv:HOME}/.claude.json,target=/home/node/.claude.json,type=bind", "source=/Applications/1Password.app/Contents/MacOS/op-ssh-sign,target=/op-ssh-sign,type=bind,readonly", @@ -22,8 +24,6 @@ "runArgs": [], "containerEnv": { "SSH_AUTH_SOCK": "/agent.sock", - "PNPM_HOME": "/home/node/.pnpm-store", - "PNPM_STORE_DIR": "/home/node/.pnpm-store", }, "forwardPorts": [], "remoteUser": "node", diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1c5ceef..4a1e08f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -14,14 +14,6 @@ jobs: fail-fast: false matrix: include: - # pnpm requires Node — only typescript-node:24 has it. - - feature: pnpm - base-image: mcr.microsoft.com/devcontainers/typescript-node:24-trixie - - feature: pnpm - base-image: mcr.microsoft.com/devcontainers/typescript-node:24-bookworm - - feature: pnpm - base-image: mcr.microsoft.com/devcontainers/typescript-node:24-bullseye - # claude / gitbutler / opencode are Node-less — test on the bare base # across all current Debian + Ubuntu LTS releases. - feature: claude @@ -61,6 +53,22 @@ jobs: base-image: mcr.microsoft.com/devcontainers/base:jammy - feature: opencode base-image: mcr.microsoft.com/devcontainers/base:focal + + # node uses the official Linux prebuilt tarball; Node 24 LTS requires + # glibc >= 2.28, which is satisfied by all six base images (focal and + # bullseye both ship glibc 2.31). + - feature: node + base-image: mcr.microsoft.com/devcontainers/base:trixie + - feature: node + base-image: mcr.microsoft.com/devcontainers/base:bookworm + - feature: node + base-image: mcr.microsoft.com/devcontainers/base:bullseye + - feature: node + base-image: mcr.microsoft.com/devcontainers/base:noble + - feature: node + base-image: mcr.microsoft.com/devcontainers/base:jammy + - feature: node + base-image: mcr.microsoft.com/devcontainers/base:focal steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 @@ -95,7 +103,7 @@ jobs: strategy: fail-fast: false matrix: - feature: [claude, gitbutler, opencode, pnpm] + feature: [claude, gitbutler, opencode, node] steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 diff --git a/Makefile b/Makefile index 210a4d4..ee0ad66 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -FEATURES ?= claude,gitbutler,opencode,pnpm +FEATURES ?= claude,gitbutler,opencode,rtk BASE_IMAGE ?= mcr.microsoft.com/devcontainers/typescript-node:24-trixie BASE_PATH ?= ./src OCI_REGISTRY ?= ghcr.io diff --git a/README.md b/README.md index 35af222..9c33538 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ A collection of [Dev Container Features](https://containers.dev/features) publis |----|------|-------------|-----------| | [`claude`](./src/claude) | Claude Code | Installs the Claude Code CLI (`@anthropic-ai/claude-code`). | `ghcr.io/synergy-shock/devcontainer-features/claude` | | [`gitbutler`](./src/gitbutler) | GitButler CLI | Installs the GitButler CLI (`but`). | `ghcr.io/synergy-shock/devcontainer-features/gitbutler` | +| [`node`](./src/node) | Node.js | Installs Node.js from the official nodejs.org prebuilt binaries, with optional `npm` and `pnpm` pinning. NVM-free. | `ghcr.io/synergy-shock/devcontainer-features/node` | | [`opencode`](./src/opencode) | OpenCode | Installs the OpenCode AI CLI. | `ghcr.io/synergy-shock/devcontainer-features/opencode` | -| [`pnpm`](./src/pnpm) | pnpm | Installs pnpm globally and configures the store and npm prefix. | `ghcr.io/synergy-shock/devcontainer-features/pnpm` | | [`rtk`](./src/rtk) | rtk (Rust Token Killer) | Installs the `rtk` CLI proxy. Pairs with `claude` and/or `opencode`. | `ghcr.io/synergy-shock/devcontainer-features/rtk` | ## Usage @@ -18,9 +18,8 @@ Reference a feature from any `devcontainer.json` by its GHCR URL and version tag ```jsonc { - "image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie", + "image": "mcr.microsoft.com/devcontainers/base:trixie", "features": { - "ghcr.io/synergy-shock/devcontainer-features/pnpm:11": {}, "ghcr.io/synergy-shock/devcontainer-features/claude:2": {}, "ghcr.io/synergy-shock/devcontainer-features/opencode:1": {}, "ghcr.io/synergy-shock/devcontainer-features/rtk:0": {}, @@ -31,56 +30,49 @@ Reference a feature from any `devcontainer.json` by its GHCR URL and version tag Version tags follow the `MAJOR`, `MAJOR.MINOR`, and `MAJOR.MINOR.PATCH` aliases that `devcontainers/action` publishes automatically. -## Wiring host state into the container +## Recommended pairings -The features install binaries; the `mounts` and `containerEnv` in this repo's own [`.devcontainer/devcontainer.json`](./.devcontainer/devcontainer.json) are a worked example of how to thread host credentials, caches, and services through so those binaries have something to talk to. +These features are intentionally narrow — one upstream CLI each. For everything else (a shell, language runtimes, git, GitHub auth), reach for **Microsoft's official base images** and the **`devcontainers/features`** catalog before writing your own. Both are first-party, versioned, and reviewed. -### Conventions worth copying +**Start from an official base image.** Pick the one that already ships the runtime you need so you don't reinstall it as a feature: -- **Bind host config, not container config.** Treat the container as disposable. Anything you would lose on rebuild (auth tokens, AI session state, shell history for the tools you care about) should live on the host and be mounted in. -- **Named volumes for caches.** Bind mounts share the host filesystem's permissions and inode layout, which is fine for config but slow and fragile for large package caches. Use a Docker named volume (like `pnpm-store` below) for anything write-heavy. -- **`readonly` for credentials you only need to read.** `.npmrc` and the 1Password signing helper are mounted readonly so a misbehaving tool inside the container cannot corrupt host state. -- **Pair every socket bind with an env var.** A forwarded socket with no `SSH_AUTH_SOCK` (or equivalent) pointing at it is just a file. +- [`mcr.microsoft.com/devcontainers/base:`](https://github.com/devcontainers/images/tree/main/src/base-debian) — minimal Debian/Ubuntu with `vscode` user, sudo, common utilities. The right default for our `claude` / `gitbutler` / `opencode` / `rtk` features, none of which need Node. +- [`mcr.microsoft.com/devcontainers/typescript-node:-`](https://github.com/devcontainers/images/tree/main/src/typescript-node) — base + Node.js + `pnpm` and `yarn` via corepack, ships with a `node` user. Use this when you actually need a JS toolchain; it removes the need for any pnpm feature. -### Forward PNPM configuration to all devcontainers +**Layer official features for shared tooling.** From [`ghcr.io/devcontainers/features`](https://github.com/devcontainers/features/tree/main/src): -Mount the host's `~/.npmrc` readonly so the container inherits your registry, auth tokens, and pnpm settings without copying secrets into the image. `PNPM_HOME` and `PNPM_STORE_DIR` keep the store path predictable across rebuilds. +- [`node:1`](https://github.com/devcontainers/features/tree/main/src/node) — install Node + npm, and pnpm via its `pnpmVersion` option (and yarn via `installYarnUsingApt`). NVM-based. Use it if you want NVM's multi-version management; reach for this repo's [`node`](./src/node) feature instead if you want a single, NVM-free Node install (official `nodejs.org` prebuilt binaries straight into `/usr/local`). +- [`common-utils:2`](https://github.com/devcontainers/features/tree/main/src/common-utils) — sudo, curl/wget, useful shells, the canonical non-root user setup. Add it when you start from a non-`devcontainers/base` image. **Required before** this repo's `node` feature. +- [`git:1`](https://github.com/devcontainers/features/tree/main/src/git) — newer git than what's in older distros. +- [`git-lfs:1`](https://github.com/devcontainers/features/tree/main/src/git-lfs) — Git LFS, if your repo uses it. +- [`github-cli:1`](https://github.com/devcontainers/features/tree/main/src/github-cli) — `gh` for PR/issue workflows and `gh auth` against the host. + +A typical Node-flavored stack ends up looking like: ```jsonc { "image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie", "features": { - "ghcr.io/synergy-shock/devcontainer-features/pnpm:11": {} - }, - "mounts": [ - "source=${localEnv:HOME}/.npmrc,target=/home/node/.npmrc,type=bind,readonly", - ], - "containerEnv": { - "PNPM_HOME": "/home/node/.pnpm-store", - "PNPM_STORE_DIR": "/home/node/.pnpm-store" + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/git-lfs:1": {}, + "ghcr.io/synergy-shock/devcontainer-features/claude:2": {}, + "ghcr.io/synergy-shock/devcontainer-features/gitbutler:0": {} } } ``` -### Unify PNPM cache across all devcontainers +If you don't need Node at all, drop the image down to `mcr.microsoft.com/devcontainers/base:trixie` and skip the `node` feature entirely. -Back the pnpm store with a Docker named volume so every devcontainer on the host shares one cache — first install in a fresh container is fast, and disk usage stops scaling with the number of projects. +## Wiring host state into the container -```jsonc -{ - "image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie", - "features": { - "ghcr.io/synergy-shock/devcontainer-features/pnpm:11": {} - }, - "mounts": [ - "source=pnpm-store,target=/home/node/.pnpm-store,type=volume", - ], - "containerEnv": { - "PNPM_HOME": "/home/node/.pnpm-store", - "PNPM_STORE_DIR": "/home/node/.pnpm-store" - } -} -``` +The features install binaries; the `mounts` and `containerEnv` in this repo's own [`.devcontainer/devcontainer.json`](./.devcontainer/devcontainer.json) are a worked example of how to thread host credentials, caches, and services through so those binaries have something to talk to. + +### Conventions worth copying + +- **Bind host config, not container config.** Treat the container as disposable. Anything you would lose on rebuild (auth tokens, AI session state, shell history for the tools you care about) should live on the host and be mounted in. +- **Named volumes for caches.** Bind mounts share the host filesystem's permissions and inode layout, which is fine for config but slow and fragile for large package caches. Use a Docker named volume for anything write-heavy (a shared pnpm/yarn store, build caches, Docker layer caches inside the container). +- **`readonly` for credentials you only need to read.** `.npmrc` and the 1Password signing helper are mounted readonly so a misbehaving tool inside the container cannot corrupt host state. +- **Pair every socket bind with an env var.** A forwarded socket with no `SSH_AUTH_SOCK` (or equivalent) pointing at it is just a file. ### 1Password SSH agent and commit signing (macOS) @@ -115,8 +107,10 @@ Then inside the container verify the binaries you touched: ```bash claude --version but --version +node --version opencode --version pnpm --version +rtk --version ``` ## Testing @@ -131,8 +125,8 @@ Then from the repo root: ```bash # Autogenerated checks for a single feature -devcontainer features test -f pnpm \ - -i mcr.microsoft.com/devcontainers/typescript-node:24-trixie \ +devcontainer features test -f claude \ + -i mcr.microsoft.com/devcontainers/base:trixie \ -p . # Custom scenarios for a single feature diff --git a/src/claude/devcontainer-feature.json b/src/claude/devcontainer-feature.json index 9545076..24aa5ee 100644 --- a/src/claude/devcontainer-feature.json +++ b/src/claude/devcontainer-feature.json @@ -12,10 +12,5 @@ "proposals": ["latest"] } }, - "containerEnv": { - "NPM_CONFIG_PREFIX": "/usr/local/share/npm-global", - "PNPM_HOME": "/usr/local/share/pnpm", - "PNPM_STORE_DIR": "/usr/local/share/pnpm-store", - "PATH": "/usr/local/share/npm-global/bin:/usr/local/share/pnpm:/usr/local/bin:${PATH}" - } + "containerEnv": {} } diff --git a/src/gitbutler/devcontainer-feature.json b/src/gitbutler/devcontainer-feature.json index 735b690..c5c2e66 100644 --- a/src/gitbutler/devcontainer-feature.json +++ b/src/gitbutler/devcontainer-feature.json @@ -1,6 +1,6 @@ { "id": "gitbutler", - "version": "0.1.0", + "version": "0.1.1", "name": "GitButler CLI", "description": "Installs the GitButler CLI (but).", "documentationURL": "https://github.com/Synergy-Shock/devcontainer-features/tree/main/src/gitbutler", diff --git a/src/node/NOTES.md b/src/node/NOTES.md new file mode 100644 index 0000000..e43f79c --- /dev/null +++ b/src/node/NOTES.md @@ -0,0 +1,30 @@ +## OS support + +Debian/Ubuntu-based images. The feature does **not** call `apt-get` itself — it expects `curl`, `jq`, and `tar` to already be installed. The expected provider is [`ghcr.io/devcontainers/features/common-utils:2`](https://github.com/devcontainers/features/tree/main/src/common-utils), which must be listed **before** the `node` feature in your `devcontainer.json`. If any required command is missing, the install script aborts with a clear error pointing at `common-utils:2`. + +## Implementation details + +- **Node.js** is the official Linux prebuilt tarball from `https://nodejs.org/dist/`, extracted into `/usr/local` with `tar --strip-components=1 --exclude=CHANGELOG.md --exclude=LICENSE --exclude=README.md` so that `node`, `npm`, and `npx` land directly under `/usr/local/bin` — already on every shell's `PATH`. No NVM, no NodeSource apt repo, no shell-rc edits. +- **Version resolution** is done against the official `https://nodejs.org/dist/index.json` index. Accepted forms for the `version` option: `lts`, `latest`/`current`, `lts/` (e.g. `lts/krypton`), `` (e.g. `24`), `.`, or an exact version with or without a leading `v`. +- **npm** is pinned with `npm install -g npm@` into the Node tarball's `/usr/local` prefix, overwriting `/usr/local/bin/npm`. The default `latest` always installs the newest published npm. +- **pnpm** is installed with `npm install -g pnpm@` (no `get.pnpm.io` script, no Corepack indirection). It lands in `NPM_CONFIG_PREFIX=/usr/local/share/npm-global` — a dedicated dir for npm globals so user-installed packages don't mix with the Node tarball's `/usr/local/bin` — and is symlinked at `/usr/local/bin/pnpm` so it's reachable even before `/etc/profile.d/node.sh` is sourced. The install script writes `/etc/profile.d/node.sh` exporting `NPM_CONFIG_PREFIX`, `PNPM_HOME=/usr/local/share/pnpm` (where pnpm itself stores globally-installed packages on later `pnpm add -g …`), and prepending both `${NPM_CONFIG_PREFIX}/bin` and `${PNPM_HOME}` to `PATH`. `/etc/profile` is sourced by login shells, and `common-utils:2` (a prerequisite) also wires `/etc/bash.bashrc` to source `/etc/profile.d/*.sh`, so the binaries are reachable from both login and interactive non-login shells. + +## Feature ordering + +Add `common-utils` **before** `node`, since this feature checks its dependencies up front: + +```jsonc +{ + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {}, + "ghcr.io/synergy-shock/devcontainer-features/node:0": { + "version": "lts", + "npmVersion": "latest", + "pnpmVersion": "latest" + } + } +} +``` + +If you already start from `mcr.microsoft.com/devcontainers/base:*`, `common-utils` essentials are baked in; the explicit feature line is still the safest way to guarantee `curl` / `jq` / `tar` regardless of the base image. diff --git a/src/node/README.md b/src/node/README.md new file mode 100644 index 0000000..5aebcf5 --- /dev/null +++ b/src/node/README.md @@ -0,0 +1,55 @@ + +# Node.js (node) + +Installs Node.js from the official nodejs.org prebuilt binaries, with optional npm and pnpm pinning. Assumes 'ghcr.io/devcontainers/features/common-utils:2' has run. + +## Example Usage + +```json +"features": { + "ghcr.io/synergy-shock/devcontainer-features/node:1": {} +} +``` + +## Options + +| Options Id | Description | Type | Default Value | +|-----|-----|-----|-----| +| version | Node.js version. 'lts' (default), 'latest'/'current', an lts codename ('lts/krypton'), a major ('24'), a major.minor ('24.16'), or an exact version ('24.16.0' / 'v24.16.0'). | string | lts | +| npmVersion | npm version (passed to 'npm install -g npm@'). Use 'latest' for the newest, or pin (e.g. '11.15.0'). | string | latest | +| pnpmVersion | pnpm version (passed to 'npm install -g pnpm@'). Installed into NPM_CONFIG_PREFIX=/usr/local/share/npm-global, which is added to PATH via /etc/profile.d/node.sh. Use 'latest' for the newest, or pin (e.g. '11.2.2'). | string | latest | + +## OS support + +Debian/Ubuntu-based images. The feature does **not** call `apt-get` itself — it expects `curl`, `jq`, and `tar` to already be installed. The expected provider is [`ghcr.io/devcontainers/features/common-utils:2`](https://github.com/devcontainers/features/tree/main/src/common-utils), which must be listed **before** the `node` feature in your `devcontainer.json`. If any required command is missing, the install script aborts with a clear error pointing at `common-utils:2`. + +## Implementation details + +- **Node.js** is the official Linux prebuilt tarball from `https://nodejs.org/dist/`, extracted into `/usr/local` with `tar --strip-components=1 --exclude=CHANGELOG.md --exclude=LICENSE --exclude=README.md` so that `node`, `npm`, and `npx` land directly under `/usr/local/bin` — already on every shell's `PATH`. No NVM, no NodeSource apt repo, no shell-rc edits. +- **Version resolution** is done against the official `https://nodejs.org/dist/index.json` index. Accepted forms for the `version` option: `lts`, `latest`/`current`, `lts/` (e.g. `lts/krypton`), `` (e.g. `24`), `.`, or an exact version with or without a leading `v`. +- **npm** is pinned with `npm install -g npm@` into the Node tarball's `/usr/local` prefix, overwriting `/usr/local/bin/npm`. The default `latest` always installs the newest published npm. +- **pnpm** is installed with `npm install -g pnpm@` (no `get.pnpm.io` script, no Corepack indirection). It lands in `NPM_CONFIG_PREFIX=/usr/local/share/npm-global` — a dedicated dir for npm globals so user-installed packages don't mix with the Node tarball's `/usr/local/bin` — and is symlinked at `/usr/local/bin/pnpm` so it's reachable even before `/etc/profile.d/node.sh` is sourced. The install script writes `/etc/profile.d/node.sh` exporting `NPM_CONFIG_PREFIX`, `PNPM_HOME=/usr/local/share/pnpm` (where pnpm itself stores globally-installed packages on later `pnpm add -g …`), and prepending both `${NPM_CONFIG_PREFIX}/bin` and `${PNPM_HOME}` to `PATH`. `/etc/profile` is sourced by login shells, and `common-utils:2` (a prerequisite) also wires `/etc/bash.bashrc` to source `/etc/profile.d/*.sh`, so the binaries are reachable from both login and interactive non-login shells. + +## Feature ordering + +Add `common-utils` **before** `node`, since this feature checks its dependencies up front: + +```jsonc +{ + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {}, + "ghcr.io/synergy-shock/devcontainer-features/node:0": { + "version": "lts", + "npmVersion": "latest", + "pnpmVersion": "latest" + } + } +} +``` + +If you already start from `mcr.microsoft.com/devcontainers/base:*`, `common-utils` essentials are baked in; the explicit feature line is still the safest way to guarantee `curl` / `jq` / `tar` regardless of the base image. + +--- + +_Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/Synergy-Shock/devcontainer-features/blob/main/src/node/devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/src/node/devcontainer-feature.json b/src/node/devcontainer-feature.json new file mode 100644 index 0000000..f9cdaa7 --- /dev/null +++ b/src/node/devcontainer-feature.json @@ -0,0 +1,35 @@ +{ + "id": "node", + "version": "1.0.0", + "name": "Node.js", + "description": "Installs Node.js from the official nodejs.org prebuilt binaries, with optional npm and pnpm pinning. Assumes 'ghcr.io/devcontainers/features/common-utils:2' has run.", + "documentationURL": "https://github.com/Synergy-Shock/devcontainer-features/tree/main/src/node", + "options": { + "version": { + "type": "string", + "default": "lts", + "description": "Node.js version. 'lts' (default), 'latest'/'current', an lts codename ('lts/krypton'), a major ('24'), a major.minor ('24.16'), or an exact version ('24.16.0' / 'v24.16.0').", + "proposals": [ + "lts", + "latest", + "lts/krypton", + "lts/jod", + "lts/iron", + "26", + "24" + ] + }, + "npmVersion": { + "type": "string", + "default": "latest", + "description": "npm version (passed to 'npm install -g npm@'). Use 'latest' for the newest, or pin (e.g. '11.15.0').", + "proposals": ["latest", "11.15.0", "10.9.0"] + }, + "pnpmVersion": { + "type": "string", + "default": "latest", + "description": "pnpm version (passed to 'npm install -g pnpm@'). Installed into NPM_CONFIG_PREFIX=/usr/local/share/npm-global, which is added to PATH via /etc/profile.d/node.sh. Use 'latest' for the newest, or pin (e.g. '11.2.2').", + "proposals": ["latest", "11.2.2", "10.0.0", "9.15.9"] + } + } +} diff --git a/src/node/install.sh b/src/node/install.sh new file mode 100755 index 0000000..50c820d --- /dev/null +++ b/src/node/install.sh @@ -0,0 +1,174 @@ +#!/bin/bash +set -e + +#----------------------------------------------------------------------------------------------------- +# Node.js Dev Container Feature +# Installs the official Node.js prebuilt binary from https://nodejs.org/dist/, then pins npm +# (via `npm install -g`) and pnpm (via the official install script at https://get.pnpm.io/install.sh). +# +# Intended for use on Debian/Ubuntu-based images that already have +# `ghcr.io/devcontainers/features/common-utils:2` applied (for curl, jq, tar, ca-certificates). +#----------------------------------------------------------------------------------------------------- + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +NODE_VERSION="${VERSION:-lts}" +NPM_VERSION_OPT="${NPMVERSION:-latest}" +PNPM_VERSION_OPT="${PNPMVERSION:-latest}" + +require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "(!) Required command '$1' is not installed." + echo "(!) This feature assumes 'ghcr.io/devcontainers/features/common-utils:2'" + echo " has been added to your devcontainer.json *before* the 'node' feature." + echo " See: https://github.com/devcontainers/features/tree/main/src/common-utils" + exit 1 + fi +} + +require_cmd curl +require_cmd jq +require_cmd tar + +ARCH=$(uname -m) +case "${ARCH}" in + x86_64) + NODE_ARCH="x64" + ;; + aarch64|arm64) + NODE_ARCH="arm64" + ;; + *) + echo "(!) Architecture ${ARCH} is not supported by this feature." + exit 1 + ;; +esac + +# ------------------------------------------------------------------ +# Resolve Node version against the official nodejs.org index +# ------------------------------------------------------------------ +NODE_INDEX_URL="https://nodejs.org/dist/index.json" + +resolve_node_version() { + local req="$1" + local index="$2" + + case "${req}" in + lts) + echo "${index}" | jq -r '[.[] | select(.lts != false)][0].version' + ;; + latest|current) + echo "${index}" | jq -r '.[0].version' + ;; + lts/*) + local codename="${req#lts/}" + echo "${index}" | jq -r --arg c "${codename}" ' + [.[] | select((.lts | type) == "string" and (.lts | ascii_downcase) == ($c | ascii_downcase))][0].version' + ;; + v[0-9]*.[0-9]*.[0-9]*) + echo "${req}" + ;; + [0-9]*.[0-9]*.[0-9]*) + echo "v${req}" + ;; + v[0-9]*.[0-9]*) + echo "${index}" | jq -r --arg p "${req}." '[.[] | select(.version | startswith($p))][0].version' + ;; + [0-9]*.[0-9]*) + echo "${index}" | jq -r --arg p "v${req}." '[.[] | select(.version | startswith($p))][0].version' + ;; + v[0-9]*) + echo "${index}" | jq -r --arg p "${req}." '[.[] | select(.version | startswith($p))][0].version' + ;; + [0-9]*) + echo "${index}" | jq -r --arg p "v${req}." '[.[] | select(.version | startswith($p))][0].version' + ;; + *) + echo "" + ;; + esac +} + +echo "==> Resolving Node version '${NODE_VERSION}'..." +NODE_INDEX=$(curl -fsSL "${NODE_INDEX_URL}") +RESOLVED_VERSION=$(resolve_node_version "${NODE_VERSION}" "${NODE_INDEX}") + +if [ -z "${RESOLVED_VERSION}" ] || [ "${RESOLVED_VERSION}" = "null" ]; then + echo "(!) Could not resolve Node version '${NODE_VERSION}' from ${NODE_INDEX_URL}." + echo " Accepted forms: lts | latest | lts/ | | . | " + exit 1 +fi + +echo "==> Resolved Node ${NODE_VERSION} -> ${RESOLVED_VERSION}" + +# ------------------------------------------------------------------ +# Download & extract Node into /usr/local (FHS merge) +# ------------------------------------------------------------------ +NODE_TARBALL="node-${RESOLVED_VERSION}-linux-${NODE_ARCH}.tar.gz" +NODE_URL="https://nodejs.org/dist/${RESOLVED_VERSION}/${NODE_TARBALL}" + +echo "==> Downloading ${NODE_URL}" +curl -fsSL "${NODE_URL}" \ + | tar -xz -C /usr/local --strip-components=1 --no-same-owner \ + --exclude='CHANGELOG.md' \ + --exclude='LICENSE' \ + --exclude='README.md' + +node --version +npm --version + +# ------------------------------------------------------------------ +# Pin / update npm — installs into the Node tarball's prefix (/usr/local), +# overwriting /usr/local/bin/npm. +# ------------------------------------------------------------------ +echo "==> Installing npm@${NPM_VERSION_OPT}..." +npm install -g --no-fund --no-audit "npm@${NPM_VERSION_OPT}" +npm --version + +# ------------------------------------------------------------------ +# Install pnpm via npm. From here on, npm globals live in a dedicated +# directory exposed on PATH via containerEnv, keeping user-installed +# globals separate from the Node tarball's /usr/local/bin. +# ------------------------------------------------------------------ +export NPM_CONFIG_PREFIX="/usr/local/share/npm-global" +mkdir -p "${NPM_CONFIG_PREFIX}/bin" + +echo "==> Installing pnpm@${PNPM_VERSION_OPT} via npm..." +# Accept both '10.0.0' and 'v10.0.0' — npm semver wants no leading 'v'. +PNPM_SPEC="${PNPM_VERSION_OPT#v}" +npm install -g --no-fund --no-audit "pnpm@${PNPM_SPEC}" + +# Symlink pnpm into /usr/local/bin so it is reachable from any shell even +# before /etc/profile.d/node.sh is sourced (matches npm's own /usr/local/bin/npm +# symlink that the Node tarball ships). +ln -sf "${NPM_CONFIG_PREFIX}/bin/pnpm" /usr/local/bin/pnpm + +# PNPM_HOME is where pnpm stores globally-installed packages (populated +# when users later run `pnpm add -g …`). containerEnv puts it on PATH. +export PNPM_HOME="/usr/local/share/pnpm" +mkdir -p "${PNPM_HOME}" + +# Make pnpm reachable for the verification call below; persisted to future +# shells via /etc/profile.d/node.sh, written next. +export PATH="${NPM_CONFIG_PREFIX}/bin:${PNPM_HOME}:${PATH}" +pnpm --version + +# ------------------------------------------------------------------ +# Persist env vars and PATH additions for future shells. +# /etc/profile is sourced by login shells; common-utils:2 (a prerequisite) +# also wires /etc/bash.bashrc to source /etc/profile.d/*.sh. +# ------------------------------------------------------------------ +cat > /etc/profile.d/node.sh <<'EOF' +export NPM_CONFIG_PREFIX="/usr/local/share/npm-global" +export PNPM_HOME="/usr/local/share/pnpm" +export PATH="${NPM_CONFIG_PREFIX}/bin:${PNPM_HOME}:${PATH}" +EOF +chmod 0644 /etc/profile.d/node.sh + +echo "==> Node feature installation complete!" +echo " node: $(node --version)" +echo " npm: $(npm --version)" +echo " pnpm: $(pnpm --version)" diff --git a/src/pnpm/NOTES.md b/src/pnpm/NOTES.md deleted file mode 100644 index 1e91b16..0000000 --- a/src/pnpm/NOTES.md +++ /dev/null @@ -1,32 +0,0 @@ -## OS support - -Debian/Ubuntu-based images with Node.js and `npm` pre-installed (e.g., the `mcr.microsoft.com/devcontainers/typescript-node` family). - -## Implementation details - -- `npm config set prefix /usr/local/share/npm-global` so global installs land in a system-wide location. -- `PNPM_HOME=/usr/local/share/pnpm` and `PNPM_STORE_DIR=/usr/local/share/pnpm-store`. -- `pnpm setup` is run under `SHELL=/bin/bash`. -- A `/etc/profile.d/pnpm.sh` snippet exports `NPM_CONFIG_PREFIX`, `PNPM_HOME`, `PNPM_STORE_DIR`, and prepends both bin dirs to `PATH` so login shells pick them up. -- The directories are `chown`-ed to `_REMOTE_USER` after install. - -## Unify the pnpm store across devcontainers - -Back the pnpm store with a Docker named volume and forward the host `~/.npmrc` readonly. Every devcontainer on the host then shares one cache — first install in a fresh container is fast, and registry/auth config stays on the host instead of being baked into images. - -```jsonc -{ - "image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie", - "features": { - "ghcr.io/synergy-shock/devcontainer-features/pnpm:11": {} - }, - "mounts": [ - "source=pnpm-store,target=/home/node/.pnpm-store,type=volume", - "source=${localEnv:HOME}/.npmrc,target=/home/node/.npmrc,type=bind,readonly" - ], - "containerEnv": { - "PNPM_HOME": "/home/node/.pnpm-store", - "PNPM_STORE_DIR": "/home/node/.pnpm-store" - } -} -``` diff --git a/src/pnpm/README.md b/src/pnpm/README.md deleted file mode 100644 index f38f4ad..0000000 --- a/src/pnpm/README.md +++ /dev/null @@ -1,55 +0,0 @@ - -# pnpm (pnpm) - -Installs pnpm globally and configures the pnpm store directory and npm global prefix. - -## Example Usage - -```json -"features": { - "ghcr.io/synergy-shock/devcontainer-features/pnpm:11": {} -} -``` - -## Options - -| Options Id | Description | Type | Default Value | -|-----|-----|-----|-----| -| version | Select or enter the version of pnpm to install globally. | string | 11 | - -## OS support - -Debian/Ubuntu-based images with Node.js and `npm` pre-installed (e.g., the `mcr.microsoft.com/devcontainers/typescript-node` family). - -## Implementation details - -- `npm config set prefix /usr/local/share/npm-global` so global installs land in a system-wide location. -- `PNPM_HOME=/usr/local/share/pnpm` and `PNPM_STORE_DIR=/usr/local/share/pnpm-store`. -- `pnpm setup` is run under `SHELL=/bin/bash`. -- A `/etc/profile.d/pnpm.sh` snippet exports `NPM_CONFIG_PREFIX`, `PNPM_HOME`, `PNPM_STORE_DIR`, and prepends both bin dirs to `PATH` so login shells pick them up. -- The directories are `chown`-ed to `_REMOTE_USER` after install. - -## Unify the pnpm store across devcontainers - -Back the pnpm store with a Docker named volume and forward the host `~/.npmrc` readonly. Every devcontainer on the host then shares one cache — first install in a fresh container is fast, and registry/auth config stays on the host instead of being baked into images. - -```jsonc -{ - "image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie", - "features": { - "ghcr.io/synergy-shock/devcontainer-features/pnpm:11": {} - }, - "mounts": [ - "source=pnpm-store,target=/home/node/.pnpm-store,type=volume", - "source=${localEnv:HOME}/.npmrc,target=/home/node/.npmrc,type=bind,readonly" - ], - "containerEnv": { - "PNPM_HOME": "/home/node/.pnpm-store", - "PNPM_STORE_DIR": "/home/node/.pnpm-store" - } -} -``` - ---- - -_Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/Synergy-Shock/devcontainer-features/blob/main/src/pnpm/devcontainer-feature.json). Add additional notes to a `NOTES.md`._ diff --git a/src/pnpm/devcontainer-feature.json b/src/pnpm/devcontainer-feature.json deleted file mode 100644 index bb8d874..0000000 --- a/src/pnpm/devcontainer-feature.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "id": "pnpm", - "version": "11.0.0", - "name": "pnpm", - "description": "Installs pnpm globally and configures the pnpm store directory and npm global prefix.", - "documentationURL": "https://github.com/Synergy-Shock/devcontainer-features/tree/main/src/pnpm", - "options": { - "version": { - "type": "string", - "default": "11", - "description": "Select or enter the version of pnpm to install globally.", - "proposals": ["latest", "10", "9", "8"] - } - }, - "containerEnv": { - "NPM_CONFIG_PREFIX": "/usr/local/share/npm-global", - "PNPM_HOME": "/usr/local/share/pnpm", - "PNPM_STORE_DIR": "/usr/local/share/pnpm-store", - "PATH": "/usr/local/share/npm-global/bin:/usr/local/share/pnpm:/usr/local/bin:${PATH}" - } -} diff --git a/src/pnpm/install.sh b/src/pnpm/install.sh deleted file mode 100755 index b1ba73b..0000000 --- a/src/pnpm/install.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash -set -e - -#----------------------------------------------------------------------------------------------------- -# pnpm Dev Container Feature -# Installs: pnpm, configures npm global prefix and pnpm store directory -# Intended for use on Debian/Ubuntu-based images with Node.js/npm pre-installed -#----------------------------------------------------------------------------------------------------- - -if [ "$(id -u)" -ne 0 ]; then - echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' - exit 1 -fi - -PNPM_VERSION="${VERSION:-10}" - -export DEBIAN_FRONTEND=noninteractive - -apt-get update -apt-get install -y --no-install-recommends curl -rm -rf /var/lib/apt/lists/* - -echo "==> Configuring npm global prefix..." -NPM_GLOBAL_PREFIX="/usr/local/share/npm-global" -mkdir -p "${NPM_GLOBAL_PREFIX}" -npm config set prefix "${NPM_GLOBAL_PREFIX}" - -export PNPM_HOME="/usr/local/share/pnpm" -mkdir -p "${PNPM_HOME}" -export PATH="${PNPM_HOME}:${PATH}" - -PROFILE_SCRIPT="/etc/profile.d/pnpm.sh" -cat > "${PROFILE_SCRIPT}" << EOF -export NPM_CONFIG_PREFIX=${NPM_GLOBAL_PREFIX} -export PNPM_HOME=${PNPM_HOME} -export PNPM_STORE_DIR=/usr/local/share/pnpm-store -export PATH=${NPM_GLOBAL_PREFIX}/bin:${PNPM_HOME}:/usr/local/bin:\${PATH} -EOF -chmod +x "${PROFILE_SCRIPT}" - -echo "==> Installing pnpm@${PNPM_VERSION}..." -npm install -g "pnpm@${PNPM_VERSION}" -export SHELL=/bin/bash -pnpm setup -pnpm --version - -echo "==> Configuring pnpm store directory..." -PNPM_STORE="/usr/local/share/pnpm-store" -mkdir -p "${PNPM_STORE}" -pnpm config set store-dir "${PNPM_STORE}" - -echo "==> Adjusting permissions for ${_REMOTE_USER} user..." -chown -R "${_REMOTE_USER}:${_REMOTE_USER}" /usr/local/share/pnpm /usr/local/share/npm-global /usr/local/share/pnpm-store 2>/dev/null || true - -echo "==> pnpm feature installation complete!" diff --git a/test/_global/all_features.sh b/test/_global/all_features.sh index 01d5af2..b897099 100755 --- a/test/_global/all_features.sh +++ b/test/_global/all_features.sh @@ -3,13 +3,11 @@ set -e source dev-container-features-test-lib -check "pnpm on PATH" bash -c "command -v pnpm" check "claude on PATH" bash -c "command -v claude" check "but on PATH" bash -c "command -v but" check "opencode on PATH" bash -c "command -v opencode" check "rtk on PATH" bash -c "command -v rtk" -check "pnpm --version" bash -c "pnpm --version" check "claude --version" bash -c "claude --version" check "but --version" bash -c "but --version" check "opencode --version" bash -c "opencode --version" diff --git a/test/_global/all_features_non_root.sh b/test/_global/all_features_non_root.sh new file mode 100755 index 0000000..07c460b --- /dev/null +++ b/test/_global/all_features_non_root.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +check "whoami is node" bash -c '[ "$(whoami)" = "node" ]' +check "home writable" bash -c 'touch "$HOME/.rwtest" && rm "$HOME/.rwtest"' + +check "claude on PATH" bash -c "command -v claude" +check "but on PATH" bash -c "command -v but" +check "opencode on PATH" bash -c "command -v opencode" +check "rtk on PATH" bash -c "command -v rtk" + +check "claude --version" bash -c "claude --version" +check "but --version" bash -c "but --version" +check "opencode --version" bash -c "opencode --version" +check "rtk --version" bash -c "rtk --version" + +reportResults diff --git a/test/_global/common_utils_node_claude.sh b/test/_global/common_utils_node_claude.sh new file mode 100755 index 0000000..b163e32 --- /dev/null +++ b/test/_global/common_utils_node_claude.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +source dev-container-features-test-lib + +# Proves the `node` feature (plus prerequisite `common-utils:2`) plays nicely +# alongside `claude` on a vanilla Node-less base image, without falling back +# to the `typescript-node:*` base. + +check "node on PATH" bash -c "command -v node" +check "npm on PATH" bash -c "command -v npm" +check "pnpm on PATH" bash -c "command -v pnpm" +check "claude on PATH" bash -c "command -v claude" + +check "node --version" bash -c "node --version | grep -E '^v[0-9]+\\.[0-9]+\\.[0-9]+'" +check "npm --version" bash -c "npm --version | grep -E '^[0-9]+\\.[0-9]+\\.[0-9]+'" +check "pnpm --version" bash -c "pnpm --version | grep -E '^[0-9]+\\.[0-9]+\\.[0-9]+'" +check "claude --version" bash -c "claude --version | grep -E '[0-9]+\\.[0-9]+\\.[0-9]+'" + +reportResults diff --git a/test/_global/opencode_and_gitbutler_non_root.sh b/test/_global/opencode_and_gitbutler_non_root.sh new file mode 100755 index 0000000..b98382a --- /dev/null +++ b/test/_global/opencode_and_gitbutler_non_root.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +check "whoami is node" bash -c '[ "$(whoami)" = "node" ]' +check "home writable" bash -c 'touch "$HOME/.rwtest" && rm "$HOME/.rwtest"' +check "but on PATH" bash -c 'command -v but' +check "opencode on PATH" bash -c 'command -v opencode' + +check "but setup as node with opencode installed" bash -c ' + set -e + mkdir -p "$HOME/proj" + cd "$HOME/proj" + git init -q + git config user.email t@t + git config user.name t + git commit --allow-empty -qm init + but setup +' + +reportResults diff --git a/test/_global/scenarios.json b/test/_global/scenarios.json index 05b8924..c9d78f7 100644 --- a/test/_global/scenarios.json +++ b/test/_global/scenarios.json @@ -2,11 +2,36 @@ "all_features": { "image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie", "features": { - "pnpm": {}, "gitbutler": {}, "opencode": {}, "claude": {}, "rtk": {} } + }, + "all_features_non_root": { + "image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie", + "remoteUser": "node", + "features": { + "gitbutler": {}, + "opencode": {}, + "claude": {}, + "rtk": {} + } + }, + "opencode_and_gitbutler_non_root": { + "image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie", + "remoteUser": "node", + "features": { + "opencode": {}, + "gitbutler": {} + } + }, + "common_utils_node_claude": { + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {}, + "node": {}, + "claude": {} + } } } diff --git a/test/claude/non_root_user.sh b/test/claude/non_root_user.sh new file mode 100755 index 0000000..405c7d2 --- /dev/null +++ b/test/claude/non_root_user.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +check "whoami is vscode" bash -c '[ "$(whoami)" = "vscode" ]' +check "home writable" bash -c 'touch "$HOME/.rwtest" && rm "$HOME/.rwtest"' +check "claude on PATH" bash -c 'command -v claude' +check "claude --version" bash -c "claude --version | grep -E '[0-9]+\\.[0-9]+\\.[0-9]+'" +check "claude dir readable" test -r /usr/local/share/claude + +reportResults diff --git a/test/claude/scenarios.json b/test/claude/scenarios.json index bdef3dc..a5b1c74 100644 --- a/test/claude/scenarios.json +++ b/test/claude/scenarios.json @@ -12,5 +12,12 @@ "version": "2.1.145" } } + }, + "non_root_user": { + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "remoteUser": "vscode", + "features": { + "claude": {} + } } } diff --git a/test/gitbutler/non_root_user.sh b/test/gitbutler/non_root_user.sh new file mode 100755 index 0000000..a8ca4b3 --- /dev/null +++ b/test/gitbutler/non_root_user.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +check "whoami is vscode" bash -c '[ "$(whoami)" = "vscode" ]' +check "home writable" bash -c 'touch "$HOME/.rwtest" && rm "$HOME/.rwtest"' +check "but on PATH" bash -c 'command -v but' +check "but --version" bash -c 'but --version' + +check "but setup as vscode" bash -c ' + set -e + mkdir -p "$HOME/proj" + cd "$HOME/proj" + git init -q + git config user.email t@t + git config user.name t + git commit --allow-empty -qm init + but setup +' + +reportResults diff --git a/test/gitbutler/scenarios.json b/test/gitbutler/scenarios.json index 2ea826e..3f1e6bd 100644 --- a/test/gitbutler/scenarios.json +++ b/test/gitbutler/scenarios.json @@ -1,8 +1,15 @@ { - "default": { - "image": "mcr.microsoft.com/devcontainers/base:trixie", - "features": { - "gitbutler": {} - } + "default": { + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "features": { + "gitbutler": {} } + }, + "non_root_user": { + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "remoteUser": "vscode", + "features": { + "gitbutler": {} + } + } } diff --git a/test/node/default.sh b/test/node/default.sh new file mode 100755 index 0000000..af41a8b --- /dev/null +++ b/test/node/default.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib +check "node --version" bash -c "node --version | grep -E '^v[0-9]+\\.[0-9]+\\.[0-9]+'" +check "npm --version" bash -c "npm --version | grep -E '^[0-9]+\\.[0-9]+\\.[0-9]+'" +check "pnpm --version" bash -c "pnpm --version | grep -E '^[0-9]+\\.[0-9]+\\.[0-9]+'" +reportResults diff --git a/test/node/lts_codename.sh b/test/node/lts_codename.sh new file mode 100755 index 0000000..a38f6f0 --- /dev/null +++ b/test/node/lts_codename.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib +check "lts/krypton resolves to v24.x" bash -c "node --version | grep -E '^v24\\.[0-9]+\\.[0-9]+'" +reportResults diff --git a/test/node/non_root_user.sh b/test/node/non_root_user.sh new file mode 100755 index 0000000..a65ef59 --- /dev/null +++ b/test/node/non_root_user.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib +check "node --version as non-root" bash -c "node --version | grep -E '^v[0-9]+\\.[0-9]+\\.[0-9]+'" +check "pnpm --version as non-root" bash -c "pnpm --version | grep -E '^[0-9]+\\.[0-9]+\\.[0-9]+'" +check "running as non-root" bash -c "test \"$(id -un)\" = 'vscode'" +reportResults diff --git a/test/node/pinned_version.sh b/test/node/pinned_version.sh new file mode 100755 index 0000000..6b57396 --- /dev/null +++ b/test/node/pinned_version.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib +check "node v24.16.0" bash -c "node --version | grep -F 'v24.16.0'" +check "npm 11.15.0" bash -c "npm --version | grep -F '11.15.0'" +check "pnpm 11.2.2" bash -c "pnpm --version | grep -F '11.2.2'" +reportResults diff --git a/test/node/scenarios.json b/test/node/scenarios.json new file mode 100644 index 0000000..c55e8d8 --- /dev/null +++ b/test/node/scenarios.json @@ -0,0 +1,37 @@ +{ + "default": { + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {}, + "node": {} + } + }, + "pinned_version": { + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {}, + "node": { + "version": "24.16.0", + "npmVersion": "11.15.0", + "pnpmVersion": "11.2.2" + } + } + }, + "lts_codename": { + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {}, + "node": { + "version": "lts/krypton" + } + } + }, + "non_root_user": { + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "remoteUser": "vscode", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {}, + "node": {} + } + } +} diff --git a/test/node/test.sh b/test/node/test.sh new file mode 100755 index 0000000..0deac47 --- /dev/null +++ b/test/node/test.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +source dev-container-features-test-lib + +check "node on PATH" bash -c "command -v node" +check "node --version" bash -c "node --version | grep -E '^v[0-9]+\\.[0-9]+\\.[0-9]+'" +check "npm on PATH" bash -c "command -v npm" +check "npm --version" bash -c "npm --version | grep -E '^[0-9]+\\.[0-9]+\\.[0-9]+'" +check "pnpm on PATH" bash -c "command -v pnpm" +check "pnpm --version" bash -c "pnpm --version | grep -E '^[0-9]+\\.[0-9]+\\.[0-9]+'" +check "node at /usr/local/bin/node" bash -c "test -x /usr/local/bin/node" +check "pnpm at /usr/local/bin/pnpm" bash -c "test -x /usr/local/bin/pnpm" +check "no /usr/local/CHANGELOG.md" bash -c "test ! -e /usr/local/CHANGELOG.md" + +reportResults diff --git a/test/opencode/non_root_user.sh b/test/opencode/non_root_user.sh new file mode 100755 index 0000000..9ca9025 --- /dev/null +++ b/test/opencode/non_root_user.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +check "whoami is vscode" bash -c '[ "$(whoami)" = "vscode" ]' +check "home writable" bash -c 'touch "$HOME/.rwtest" && rm "$HOME/.rwtest"' +check "opencode on PATH" bash -c 'command -v opencode' +check "opencode --version" bash -c 'opencode --version' +check ".config owned" bash -c '[ "$(stat -c %U "$HOME/.config")" = "$(whoami)" ]' +check ".local owned" bash -c '[ "$(stat -c %U "$HOME/.local")" = "$(whoami)" ]' + +reportResults diff --git a/test/opencode/scenarios.json b/test/opencode/scenarios.json index 6322fb2..ad2655c 100644 --- a/test/opencode/scenarios.json +++ b/test/opencode/scenarios.json @@ -4,5 +4,12 @@ "features": { "opencode": {} } + }, + "non_root_user": { + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "remoteUser": "vscode", + "features": { + "opencode": {} + } } } diff --git a/test/pnpm/default.sh b/test/pnpm/default.sh deleted file mode 100755 index 944de6e..0000000 --- a/test/pnpm/default.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -set -e -source dev-container-features-test-lib -check "pnpm major is 11" bash -c "pnpm --version | grep -E '^11\\.'" -reportResults diff --git a/test/pnpm/pnpm_9.sh b/test/pnpm/pnpm_9.sh deleted file mode 100755 index 1d16e14..0000000 --- a/test/pnpm/pnpm_9.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/bash -set -e -source dev-container-features-test-lib -check "pnpm major is 9" bash -c "pnpm --version | grep -E '^9\\.'" -reportResults diff --git a/test/pnpm/scenarios.json b/test/pnpm/scenarios.json deleted file mode 100644 index 20edb3a..0000000 --- a/test/pnpm/scenarios.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "default": { - "image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie", - "features": { - "pnpm": {} - } - }, - "pnpm_9": { - "image": "mcr.microsoft.com/devcontainers/typescript-node:24-trixie", - "features": { - "pnpm": { - "version": "9" - } - } - } -} diff --git a/test/pnpm/test.sh b/test/pnpm/test.sh deleted file mode 100755 index a55f463..0000000 --- a/test/pnpm/test.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash -set -e - -source dev-container-features-test-lib - -check "pnpm on PATH" bash -c "command -v pnpm" -check "pnpm --version" bash -c "pnpm --version | grep -E '[0-9]+\\.[0-9]+\\.[0-9]+'" -check "store dir configured" bash -c "test -d /usr/local/share/pnpm-store" -check "npm prefix configured" bash -c "test -d /usr/local/share/npm-global" -check "profile.d snippet" bash -c "test -x /etc/profile.d/pnpm.sh" - -reportResults diff --git a/test/rtk/non_root_user.sh b/test/rtk/non_root_user.sh new file mode 100755 index 0000000..f42c7d9 --- /dev/null +++ b/test/rtk/non_root_user.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -e +source dev-container-features-test-lib + +check "whoami is vscode" bash -c '[ "$(whoami)" = "vscode" ]' +check "home writable" bash -c 'touch "$HOME/.rwtest" && rm "$HOME/.rwtest"' +check "rtk on PATH" bash -c 'command -v rtk' +check "rtk --version" bash -c 'rtk --version' + +reportResults diff --git a/test/rtk/scenarios.json b/test/rtk/scenarios.json index e86647f..bb77cf4 100644 --- a/test/rtk/scenarios.json +++ b/test/rtk/scenarios.json @@ -4,5 +4,12 @@ "features": { "rtk": {} } + }, + "non_root_user": { + "image": "mcr.microsoft.com/devcontainers/base:trixie", + "remoteUser": "vscode", + "features": { + "rtk": {} + } } }