Skip to content

Commit b276598

Browse files
committed
initial commit
1 parent 3bca6a9 commit b276598

9 files changed

Lines changed: 427 additions & 16 deletions

File tree

.github/workflows/release.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Release devcontainer features
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches:
7+
- main
8+
paths:
9+
- "src/**"
10+
- ".github/workflows/release.yaml"
11+
12+
jobs:
13+
publish:
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: write
17+
packages: write
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Publish features to ghcr.io/${{ github.repository }}
23+
uses: devcontainers/action@v1
24+
with:
25+
publish-features: "true"
26+
base-path-to-features: "./src"
27+
generate-docs: "true"
28+
env:
29+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+
31+
- name: Open PR for generated documentation
32+
uses: peter-evans/create-pull-request@v6
33+
with:
34+
token: ${{ secrets.GITHUB_TOKEN }}
35+
title: "docs: update generated feature READMEs"
36+
commit-message: "docs: update generated feature READMEs"
37+
branch: automated-feature-docs
38+
delete-branch: true
39+
body: |
40+
Automated update of generated feature documentation.
41+
42+
Triggered by ${{ github.sha }} on ${{ github.ref }}.

.github/workflows/test.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Test devcontainer features
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "src/**"
7+
- "test/**"
8+
- ".github/workflows/test.yaml"
9+
push:
10+
branches:
11+
- main
12+
paths:
13+
- "src/**"
14+
- "test/**"
15+
- ".github/workflows/test.yaml"
16+
17+
jobs:
18+
test:
19+
runs-on: ubuntu-latest
20+
strategy:
21+
fail-fast: false
22+
matrix:
23+
feature:
24+
- mise-node
25+
baseImage:
26+
- mcr.microsoft.com/devcontainers/base:ubuntu
27+
- mcr.microsoft.com/devcontainers/base:debian
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@v4
31+
32+
- name: Install @devcontainers/cli
33+
run: npm install -g @devcontainers/cli
34+
35+
- name: Test ${{ matrix.feature }} on ${{ matrix.baseImage }}
36+
run: |
37+
devcontainer features test \
38+
--features ${{ matrix.feature }} \
39+
--base-image ${{ matrix.baseImage }} \
40+
--project-folder .

LICENSE

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,9 @@
11
MIT License
22

3-
Copyright (c) 2026 Payload
3+
Copyright (c) 2018-2026 Payload CMS, Inc. info@payloadcms.com
44

5-
Permission is hereby granted, free of charge, to any person obtaining a copy
6-
of this software and associated documentation files (the "Software"), to deal
7-
in the Software without restriction, including without limitation the rights
8-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9-
copies of the Software, and to permit persons to whom the Software is
10-
furnished to do so, subject to the following conditions:
5+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
116

12-
The above copyright notice and this permission notice shall be included in all
13-
copies or substantial portions of the Software.
7+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
148

15-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21-
SOFTWARE.
9+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Payload devcontainer features
2+
3+
A collection of [devcontainer features](https://containers.dev/implementors/features/) published to `ghcr.io/payloadcms/devcontainer-features`.
4+
5+
## Features
6+
7+
| Feature | Reference |
8+
| --- | --- |
9+
| [mise-node](src/mise-node) | `ghcr.io/payloadcms/devcontainer-features/mise-node:1` |
10+
11+
## Usage
12+
13+
Reference a feature in your `.devcontainer/devcontainer.json`:
14+
15+
```jsonc
16+
{
17+
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
18+
"features": {
19+
"ghcr.io/payloadcms/devcontainer-features/mise-node:1": {}
20+
}
21+
}
22+
```
23+
24+
## Repo layout
25+
26+
```
27+
src/<feature-id>/
28+
devcontainer-feature.json # feature manifest (id, version, options)
29+
install.sh # runs as root in the container at build time
30+
test/<feature-id>/
31+
test.sh # runs in a freshly built devcontainer
32+
.github/workflows/
33+
release.yaml # publishes features to ghcr.io on push to main
34+
test.yaml # runs feature tests on PRs
35+
```

src/mise-node/README.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# mise + Node project bootstrap (`mise-node`)
2+
3+
Installs [mise](https://mise.jdx.dev). When the container is created, runs `mise trust` + `mise install` against the workspace and then runs the project's JS package manager install.
4+
5+
The project is the source of truth for which tools (Node, pnpm, bun, etc.) get installed — declare them in `mise.toml` or `.tool-versions`. This feature does **not** pin a Node version itself.
6+
7+
## Example usage
8+
9+
```jsonc
10+
{
11+
"image": "mcr.microsoft.com/devcontainers/base:ubuntu",
12+
"features": {
13+
"ghcr.io/payloadcms/devcontainer-features/mise-node:1": {}
14+
}
15+
}
16+
```
17+
18+
With a `mise.toml` at the workspace root:
19+
20+
```toml
21+
[tools]
22+
node = "20"
23+
pnpm = "9"
24+
```
25+
26+
After container create the workspace will have Node 20 + pnpm 9 installed by mise, and `pnpm install` will have been run.
27+
28+
## Options
29+
30+
| Option | Type | Default | Description |
31+
| --- | --- | --- | --- |
32+
| `miseVersion` | string | `latest` | Version of mise to install. Use `latest` or a specific release like `v2025.1.0`. |
33+
| `skipBootstrap` | boolean | `false` | If `true`, skip the `mise install` + package-manager install on container create. |
34+
| `packageManager` | enum | `auto` | One of `auto`, `pnpm`, `yarn`, `npm`, `bun`, or `none`. `none` runs `mise install` but skips the JS install. |
35+
36+
## How package manager auto-detection works
37+
38+
In order:
39+
40+
1. `pnpm-lock.yaml` → pnpm
41+
2. `bun.lock` / `bun.lockb` → bun
42+
3. `yarn.lock` → yarn
43+
4. `package-lock.json` / `npm-shrinkwrap.json` → npm
44+
5. `package.json#packageManager` field (corepack convention)
45+
6. Fallback: npm
46+
47+
Each manager runs in frozen-lockfile mode and falls back to a regular install if that fails (e.g. lockfile out of sync).
48+
49+
## What the install step does
50+
51+
- Installs `curl`, `ca-certificates`, `git`, `xz-utils` if missing.
52+
- Installs `mise` to `~/.local/bin/mise` for the remote user via `https://mise.run`.
53+
- Adds `eval "$(mise activate <shell>)"` to `~/.bashrc` and `~/.zshrc`.
54+
- Writes `/etc/profile.d/mise-node.sh` so login shells and scripts pick up `~/.local/bin` and `~/.local/share/mise/shims` on `PATH`.
55+
- Ships `/usr/local/share/mise-node/postCreate.sh`, registered as the feature's `postCreateCommand`.
56+
57+
## What postCreate does
58+
59+
In the workspace folder:
60+
61+
1. `mise trust --yes` + `mise install --yes` if there's a `mise.toml`, `.mise.toml`, `.config/mise.toml`, or `.tool-versions`.
62+
2. If a `package.json` exists, picks a package manager (see above) and runs its install.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"id": "mise-node",
3+
"version": "1.0.0",
4+
"name": "mise + Node project bootstrap",
5+
"description": "Installs mise (https://mise.jdx.dev). On container create, runs `mise trust` + `mise install` in the workspace, detects the JS package manager (pnpm / yarn / npm / bun) from the lockfile or package.json, and runs the corresponding install.",
6+
"documentationURL": "https://github.com/payloadcms/devcontainer-features/tree/main/src/mise-node",
7+
"postCreateCommand": "/usr/local/share/mise-node/postCreate.sh",
8+
"installsAfter": [
9+
"ghcr.io/devcontainers/features/common-utils"
10+
]
11+
}

src/mise-node/install.sh

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/usr/bin/env bash
2+
#
3+
# mise-node devcontainer feature — install step.
4+
#
5+
# Installs mise (https://mise.jdx.dev) for the remote user, wires up shell
6+
# activation, and ships postCreate.sh to /usr/local/share/mise-node/ where
7+
# it is invoked from devcontainer-feature.json's `postCreateCommand`.
8+
9+
set -euo pipefail
10+
11+
12+
if [ "$(id -u)" -ne 0 ]; then
13+
echo "ERROR: Script must be run as root."
14+
exit 1
15+
fi
16+
17+
# Resolve target user.
18+
USERNAME="${_REMOTE_USER:-${USERNAME:-automatic}}"
19+
if [ "${USERNAME}" = "automatic" ] || [ -z "${USERNAME}" ]; then
20+
USERNAME=""
21+
for candidate in vscode node codespace "$(awk -v val=1000 -F ':' '$3==val{print $1}' /etc/passwd)"; do
22+
if [ -n "${candidate}" ] && id -u "${candidate}" >/dev/null 2>&1; then
23+
USERNAME="${candidate}"
24+
break
25+
fi
26+
done
27+
[ -z "${USERNAME}" ] && USERNAME="root"
28+
fi
29+
30+
USER_HOME="$(getent passwd "${USERNAME}" | cut -d: -f6)"
31+
if [ -z "${USER_HOME}" ] || [ ! -d "${USER_HOME}" ]; then
32+
echo "ERROR: Could not resolve home directory for user '${USERNAME}'."
33+
exit 1
34+
fi
35+
USER_GROUP="$(id -gn "${USERNAME}")"
36+
37+
# Dependencies needed to fetch and run mise.
38+
apt_get_update() {
39+
if [ ! -d /var/lib/apt/lists ] || [ "$(find /var/lib/apt/lists -mindepth 1 2>/dev/null | wc -l)" = "0" ]; then
40+
apt-get update -y
41+
fi
42+
}
43+
check_packages() {
44+
if ! dpkg -s "$@" >/dev/null 2>&1; then
45+
apt_get_update
46+
apt-get install -y --no-install-recommends "$@"
47+
fi
48+
}
49+
50+
export DEBIAN_FRONTEND=noninteractive
51+
check_packages curl ca-certificates git xz-utils
52+
53+
su - "${USERNAME}" -c "curl -fsSL https://mise.run | sh"
54+
55+
MISE_BIN="${USER_HOME}/.local/bin/mise"
56+
if [ ! -x "${MISE_BIN}" ]; then
57+
echo "ERROR: mise was not installed at ${MISE_BIN}."
58+
exit 1
59+
fi
60+
61+
# Activate mise in interactive bash and zsh.
62+
ensure_line() {
63+
local file="$1" line="$2"
64+
[ -e "${file}" ] || su - "${USERNAME}" -c "touch '${file}'"
65+
if ! grep -Fxq "${line}" "${file}"; then
66+
printf '%s\n' "${line}" >> "${file}"
67+
chown "${USERNAME}:${USER_GROUP}" "${file}"
68+
fi
69+
}
70+
ensure_line "${USER_HOME}/.bashrc" 'eval "$(~/.local/bin/mise activate bash)"'
71+
ensure_line "${USER_HOME}/.zshrc" 'eval "$(~/.local/bin/mise activate zsh)"'
72+
73+
# Expose mise + shims for non-interactive / login shells.
74+
cat >/etc/profile.d/mise-node.sh <<EOF
75+
# Added by the mise-node devcontainer feature.
76+
export PATH="${USER_HOME}/.local/bin:${USER_HOME}/.local/share/mise/shims:\${PATH}"
77+
EOF
78+
chmod 0644 /etc/profile.d/mise-node.sh
79+
80+
# Ship postCreate.sh and a small env file so it knows which user/options to use.
81+
SHARE_DIR="/usr/local/share/mise-node"
82+
mkdir -p "${SHARE_DIR}"
83+
84+
FEATURE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
85+
install -m 0755 "${FEATURE_DIR}/postCreate.sh" "${SHARE_DIR}/postCreate.sh"
86+
87+
cat >"${SHARE_DIR}/env" <<EOF
88+
# Generated by mise-node install.sh — consumed by postCreate.sh.
89+
MISE_NODE_USER="${USERNAME}"
90+
MISE_NODE_USER_HOME="${USER_HOME}"
91+
92+
EOF
93+
chmod 0644 "${SHARE_DIR}/env"
94+
95+
echo "mise-node feature installed."
96+
echo " mise: ${MISE_BIN}"
97+
echo " postCreate: ${SHARE_DIR}/postCreate.sh"
98+
echo " user: ${USERNAME}"

0 commit comments

Comments
 (0)