From 3248e74c8e36a8d10323b9b3a7e006ce79bc63cb Mon Sep 17 00:00:00 2001 From: Bas Steins Date: Sun, 22 Mar 2026 13:44:26 +0100 Subject: [PATCH] Add skills and instructions for copilot --- .github/copilot-instructions.md | 85 ++++ .../change-devcontainer-feature/SKILL.md | 95 ++++ .../create-devcontainer-feature/SKILL.md | 462 ++++++++++++++++++ 3 files changed, 642 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/skills/change-devcontainer-feature/SKILL.md create mode 100644 .github/skills/create-devcontainer-feature/SKILL.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..41fa684 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,85 @@ +# Copilot Instructions — devcontainer-features + +> Community-maintained collection of [Dev Container Features](https://containers.dev/implementors/features/) that install CLI tools into development containers. + +## Quick Reference + +- **Build/test a feature**: `devcontainer features test --skip-scenarios -f -i debian:latest .` +- **Release**: Manual workflow dispatch via `.github/workflows/release.yaml` (main branch only) +- **Feature count**: ~51 features across 7 install methods + +## Repository Layout + +``` +src// # One directory per feature +├── devcontainer-feature.json # Metadata, version, options (source of truth) +├── install.sh # Installation script (must be executable) +├── NOTES.md # Human-written docs: project link, description, install method +└── README.md # AUTO-GENERATED by release workflow — never edit manually +test// +└── test.sh # Feature tests using dev-container-features-test-lib +.github/ +├── skills/ # AI skill files for creating/changing features +├── workflows/test.yaml # CI: tests changed features on PR, all on push to main +└── workflows/release.yaml # Publishes features to GHCR, auto-generates README.md +README.md # Root: feature table (name, description, method, version) +``` + +**Critical invariant**: The directory name under `src/` must exactly match the `"id"` field in `devcontainer-feature.json`. + +## Feature Naming Conventions + +| Priority | Style | When | Examples | +|----------|-------|------|---------| +| 1 | Domain-style | Project has a well-known website | `starship.rs`, `chezmoi.io`, `atuin.sh` | +| 2 | Owner-project | Project name alone is ambiguous | `charmbracelet-gum`, `schpet-linear-cli` | +| 3 | Plain name | Widely recognized, unambiguous tools | `bat`, `jq`, `fzf`, `fd` | + +Use hyphens (not slashes) for owner-project names. + +## Install Methods (in priority order) + +1. **gh release** (~69%) — pre-built binaries from GitHub Releases. Canonical template: `src/fzf/install.sh` +2. **curl** (~20%) — official install script. Reference: `src/bun.sh/install.sh` +3. **apt** (~6%) — Debian/Ubuntu packages. Reference: `src/jq/install.sh` +4. **cargo / bun / npm / nix** — last resort. References: `src/jnsahaj-lumen/install.sh`, `src/critique.work/install.sh`, `src/devenv.sh/install.sh` + +## install.sh Conventions + +Every `install.sh` must have: +- **Shebang**: `#!/bin/bash` +- **Strict flags**: `set -o errexit`, `pipefail`, `noclobber`, `nounset`, `allexport` +- **Footer**: `echo_banner "devcontainer.community"` → `install "$@"` → `echo "(*) Done!"` +- **Executable bit**: `chmod +x` AND `git update-index --chmod=+x` + +Helper functions (e.g., `apt_get_update`, `github_get_latest_release`, `debian_get_target_arch`) come from [shell-snippets](https://github.com/devcontainer-community/shell-snippets) and must be copied verbatim — only customize feature-specific parts. + +## Version Management + +- New features start at `1.0.0` +- **Every change requires a version bump** — patch by default, minor for new options, major for breaking changes +- Version must be updated in **both** `src//devcontainer-feature.json` AND the root `README.md` table +- Both must match exactly + +## Testing + +CI runs on 3 base images for each changed feature: +- `debian:latest` +- `ubuntu:latest` +- `mcr.microsoft.com/devcontainers/base:ubuntu` + +Test files must source `dev-container-features-test-lib` and call `reportResults`. + +## Skills (Detailed Workflows) + +For step-by-step instructions, read these files: +- **New feature**: [.github/skills/create-devcontainer-feature/SKILL.md](.github/skills/create-devcontainer-feature/SKILL.md) +- **Change feature**: [.github/skills/change-devcontainer-feature/SKILL.md](.github/skills/change-devcontainer-feature/SKILL.md) + +## Common Pitfalls + +- **Never manually edit** `src//README.md` — it's auto-generated by the release workflow +- **Architecture mapping varies per project** — always verify asset names (Go: `amd64`/`arm64`; Rust: `x86_64-unknown-linux-musl`/`aarch64-unknown-linux-musl`) +- **Root README table must stay alphabetically sorted** when adding new features +- **`installsAfter`** is rare — only add when there's a real dependency (e.g., aws-cli depends on common-utils) +- **`options.version.default`** is always `"latest"` unless versioning isn't supported diff --git a/.github/skills/change-devcontainer-feature/SKILL.md b/.github/skills/change-devcontainer-feature/SKILL.md new file mode 100644 index 0000000..efbe81a --- /dev/null +++ b/.github/skills/change-devcontainer-feature/SKILL.md @@ -0,0 +1,95 @@ +# change-devcontainer-feature + +Modify an existing devcontainer feature: update install logic, fix bugs, change metadata — and bump the version. + +## When to Use + +Use this skill when an issue or request involves changing an existing feature (bug fix, install method update, description change, dependency update, etc.). Do **not** use this for creating a new feature from scratch — use `create-devcontainer-feature` for that. + +## Critical Rule: Always Bump the Version + +**Every change to a feature MUST include a version bump.** No exceptions. + +- **Patch bump** (default): `1.0.0` → `1.0.1` — for bug fixes, minor install script changes, documentation fixes, dependency updates +- **Minor bump**: `1.0.0` → `1.1.0` — for new options, significant behavior changes, new capabilities +- **Major bump**: `1.0.0` → `2.0.0` — for breaking changes (e.g., different binary name, removed options, changed default behavior) + +Use a **patch bump** unless the issue explicitly requests or the change clearly warrants a minor or major bump. + +## Step 1 — Identify the Feature + +Determine which feature to modify from the issue description. The feature `id` matches the directory name under `src/`. + +Read the existing files to understand the current state: +- `src//devcontainer-feature.json` — current version, options, metadata +- `src//install.sh` — current install logic +- `src//NOTES.md` — current documentation +- `test//test.sh` — current tests + +## Step 2 — Make the Requested Changes + +Apply the changes described in the issue. Common change types: + +- **Bug fix in install.sh** — fix download URL, architecture mapping, error handling, etc. +- **Update install method** — e.g., switch from curl to gh release +- **Update metadata** — description, name, options in `devcontainer-feature.json` +- **Update documentation** — `NOTES.md` content +- **Update tests** — fix or improve `test.sh` assertions +- **Update helper functions** — copy latest versions from https://github.com/devcontainer-community/shell-snippets + +When modifying `install.sh`: +- Keep helper functions verbatim from the shell-snippets repo +- Only customize the feature-specific parts (repository, binary name, URL template, architecture mappings) +- Preserve the required header (`set -o` flags) and footer (`echo_banner` + install call) +- Ensure the executable bit is preserved: run `chmod +x` and `git update-index --chmod=+x` if the file is recreated + +## Step 3 — Bump the Version + +Update the version in **both** locations: + +### 3a. `src//devcontainer-feature.json` + +Increment the `"version"` field: + +```json +{ + "version": "1.0.1" +} +``` + +### 3b. Root `README.md` + +Find the feature's row in the table and update the version in the last column: + +``` +| [feature-name](...) | `binary` — description | install-method | 1.0.1 | +``` + +**Both files must have the same version.** If they are out of sync before your change, align them to the new bumped version. + +## Step 4 — Update Tests if Needed + +If the change affects the binary name, version output format, or install location, update `test//test.sh` accordingly. + +If the change is purely a bug fix in install logic and the test already verifies the binary works, the test likely needs no changes. + +## Step 5 — Validate + +1. Verify `devcontainer-feature.json` is valid JSON and the version was bumped +2. Verify the version in `README.md` matches the version in `devcontainer-feature.json` +3. Verify `install.sh` still has correct shebang, `set -o` flags, and the executable bit +4. Verify `test.sh` still sources `dev-container-features-test-lib` and calls `reportResults` +5. Run CI tests via GitHub Actions — the test workflow automatically picks up changed features on a PR: + - `devcontainer features test --skip-scenarios -f -i debian:latest .` + - `devcontainer features test --skip-scenarios -f -i ubuntu:latest .` + - `devcontainer features test --skip-scenarios -f -i mcr.microsoft.com/devcontainers/base:ubuntu .` + +## Checklist + +- [ ] Changes applied as described in the issue +- [ ] Version bumped in `src//devcontainer-feature.json` (patch unless stated otherwise) +- [ ] Version bumped in root `README.md` (same version, correct row) +- [ ] Both versions match +- [ ] `install.sh` executable bit preserved (if file was modified) +- [ ] Tests updated if affected by the change +- [ ] CI tests pass on all three base images diff --git a/.github/skills/create-devcontainer-feature/SKILL.md b/.github/skills/create-devcontainer-feature/SKILL.md new file mode 100644 index 0000000..56dd709 --- /dev/null +++ b/.github/skills/create-devcontainer-feature/SKILL.md @@ -0,0 +1,462 @@ +# create-devcontainer-feature + +Create a new devcontainer feature end-to-end: source files, test, README update, and executable permissions — ready for CI. + +## When to Use + +Use this skill when an issue requests adding a new CLI tool or binary as a devcontainer feature. + +## Inputs + +The issue description typically provides: +- **Feature name** (used to derive the directory name / feature `id`) +- **Project URL** (GitHub repo or homepage) +- **Releases URL** (GitHub Releases page) +- **Install method** (if non-default) +- **Any special notes** (e.g., extra config, service setup) + +## Step 1 — Determine Feature Name (`id`) + +Derive the feature directory name (which is also the `id` in `devcontainer-feature.json`). + +**Priority order:** +1. **Domain-style** — preferred when the project has a well-known website (e.g., `starship.rs`, `chezmoi.io`, `deno.com`, `atuin.sh`) +2. **Owner-project** — when the project name alone is ambiguous (e.g., `charmbracelet-gum`, `schpet-linear-cli`). Use hyphens, not slashes. +3. **Plain name** — fallback for widely recognized tools (e.g., `bat`, `jq`, `fzf`) + +The issue usually specifies or strongly implies the name. Use it as given unless it violates these conventions. + +## Step 2 — Determine Install Method + +Unless the issue specifies otherwise, choose the install method in this priority order: + +1. **`gh release`** — project publishes pre-built Linux binaries on GitHub Releases (most common) +2. **`curl`** — project provides an official install script but no GitHub release binaries +3. **`apt`** — package is available in Debian/Ubuntu repos with no better option +4. **Last resort** — `cargo`, `bun`, `npm`, `nix` (prefer `bun install -g` over `npm install -g`) + +## Step 3 — Investigate Release Assets (for `gh release` method) + +Before writing `install.sh`, inspect the project's GitHub Releases page to determine: + +1. **Asset naming pattern** — the exact filename template (e.g., `fzf-${version}-linux_${architecture}.tar.gz`) +2. **Archive format** — `.tar.gz`, `.zip`, or standalone binary +3. **Directory nesting** — is the binary at the archive root (strip=0) or inside a directory (strip=1+)? +4. **Architecture labels** — how the project names architectures (x86_64 vs amd64 vs x86-64, aarch64 vs arm64, etc.) +5. **Tag format** — `v1.0.0` vs `1.0.0` vs other prefix patterns +6. **Linux target triple** — some use `unknown-linux-musl`, some use `linux`, some use `Linux` + +Map Debian architectures to the project's labels in `debian_get_target_arch()`: +- `amd64` → (varies: `x86_64`, `amd64`, `x86-64`) +- `arm64` → (varies: `aarch64`, `arm64`) +- `armhf` → (varies: `arm`, `armv6`, `armv7`) +- `i386` → (varies: `i686`, `x86`, `386`) + +### Fallback: If You Cannot Access the Releases Page + +If web access to the GitHub Releases page is blocked or unavailable: + +1. **Check the issue description** — it should include a releases URL and may describe the asset naming pattern +2. **Use the GitHub API** — `curl -s https://api.github.com/repos/OWNER/REPO/releases/latest` returns JSON with all asset names listed under `assets[].name` +3. **Follow common conventions** — most Go projects use `__linux_.tar.gz`, most Rust projects use `-v--unknown-linux-musl.tar.gz` +4. **Ask the user** — if none of the above work, request the exact asset naming pattern, archive structure, and architecture labels + +Never guess the asset naming pattern. If you cannot verify it, ask. + +## Step 4 — Create Files + +Create these files: + +### 4a. `src//devcontainer-feature.json` + +```json +{ + "name": "", + "id": "", + "version": "1.0.0", + "description": "Install \"\" binary", + "documentationURL": "https://github.com/devcontainer-community/devcontainer-features/tree/main/src/", + "options": { + "version": { + "type": "string", + "default": "latest", + "proposals": [ + "latest" + ], + "description": "Version of \"\" to install." + } + } +} +``` + +Notes: +- `id` MUST match the directory name exactly +- `name` can be human-friendly (e.g., `"AWS CLI"`) but often matches `id` +- `version` starts at `"1.0.0"` for new features +- Add `"installsAfter"` only if there is a real dependency on another feature + +### 4b. `src//install.sh` + +Use `src/fzf/install.sh` as the **canonical template** for `gh release` features. + +Copy helper functions **verbatim** — ideally sourced from https://github.com/devcontainer-community/shell-snippets. The functions to include: + +- `apt_get_update`, `apt_get_checkinstall`, `apt_get_cleanup` +- `check_curl_envsubst_file_tar_installed` +- `curl_check_url`, `curl_download_stdout`, `curl_download_untar` +- `debian_get_arch`, `debian_get_target_arch` +- `echo_banner` +- `github_list_releases`, `github_get_latest_release`, `github_get_tag_for_version` +- `utils_check_version` + +**Only customize these parts:** +- `readonly githubRepository='owner/repo'` +- `readonly binaryName='...'` +- `readonly versionArgument='--version'` +- `debian_get_target_arch()` case mappings (per Step 3) +- `downloadUrlTemplate` (exact asset naming pattern from Step 3) +- `binaryPathInArchive` (path inside the archive, from Step 3) + +**Required header (all install.sh files):** +```bash +#!/bin/bash +set -o errexit +set -o pipefail +set -o noclobber +set -o nounset +set -o allexport +``` + +**Required footer (all install.sh files):** +```bash +echo_banner "devcontainer.community" +echo "Installing $name..." +install "$@" +echo "(*) Done!" +``` + +### `apt` install method template (reference: `src/jq/install.sh`) + +```bash +#!/bin/bash +set -o errexit +set -o pipefail +set -o noclobber +set -o nounset +set -o allexport +readonly name="" +apt_get_update() { + if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update -y + fi +} +apt_get_checkinstall() { + if ! dpkg -s "$@" >/dev/null 2>&1; then + apt_get_update + DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends --no-install-suggests --option 'Debug::pkgProblemResolver=true' --option 'Debug::pkgAcquire::Worker=1' "$@" + fi +} +apt_get_cleanup() { + apt-get clean + rm -rf /var/lib/apt/lists/* +} +echo_banner() { + local text="$1" + echo -e "\e[1m\e[97m\e[41m$text\e[0m" +} +install() { + apt_get_checkinstall + apt_get_cleanup +} +echo_banner "devcontainer.community" +echo "Installing $name..." +install "$@" +echo "(*) Done!" +``` + +### `curl` (official install script) template (reference: `src/bun.sh/install.sh`) + +```bash +#!/bin/bash +set -o errexit +set -o pipefail +set -o noclobber +set -o nounset +set -o allexport +readonly name="" +apt_get_update() { + if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update -y + fi +} +apt_get_checkinstall() { + if ! dpkg -s "$@" >/dev/null 2>&1; then + apt_get_update + DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends --no-install-suggests --option 'Debug::pkgProblemResolver=true' --option 'Debug::pkgAcquire::Worker=1' "$@" + fi +} +apt_get_cleanup() { + apt-get clean + rm -rf /var/lib/apt/lists/* +} +echo_banner() { + local text="$1" + echo -e "\e[1m\e[97m\e[41m$text\e[0m" +} +install() { + apt_get_checkinstall curl ca-certificates # add unzip if needed + # For user-level install (installs to $HOME): use su $_REMOTE_USER -c "..." + # For system-level install (installs to /usr/local): run directly + su $_REMOTE_USER -c "curl -fsSL | bash" + apt_get_cleanup +} +echo_banner "devcontainer.community" +echo "Installing $name..." +install "$@" +echo "(*) Done!" +``` + +Notes for curl method: +- Use `su $_REMOTE_USER -c "..."` when the tool installs to the user's HOME directory +- Run directly (no `su`) when the tool installs to a system path like `/usr/local` +- If VERSION is supported, check for `latest` and resolve it, then pass to the install script (see `src/deno.com/install.sh` for an example) + +### `cargo` install method template (reference: `src/jnsahaj-lumen/install.sh`) + +```bash +#!/bin/bash +set -o errexit +set -o pipefail +set -o noclobber +set -o nounset +set -o allexport +readonly cratesPackage='' +readonly binaryName='' +readonly binaryTargetFolder='/usr/local/bin' +readonly name='' +apt_get_update() { + if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update -y + fi +} +apt_get_checkinstall() { + if ! dpkg -s "$@" >/dev/null 2>&1; then + apt_get_update + DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends --no-install-suggests --option 'Debug::pkgProblemResolver=true' --option 'Debug::pkgAcquire::Worker=1' "$@" + fi +} +apt_get_cleanup() { + apt-get clean + rm -rf /var/lib/apt/lists/* +} +echo_banner() { + local text="$1" + echo -e "\e[1m\e[97m\e[41m$text\e[0m" +} +utils_check_version() { + local version=$1 + if ! [[ "${version:-}" =~ ^(latest|[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + printf >&2 '=== [ERROR] Option "version" (value: "%s") is not "latest" or valid semantic version format "X.Y.Z" !\n' \ + "$version" + exit 1 + fi +} +install() { + utils_check_version "$VERSION" + apt_get_checkinstall curl ca-certificates build-essential + export RUSTUP_HOME=/usr/local/rustup + export CARGO_HOME=/usr/local/cargo + if ! command -v cargo >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \ + sh -s -- -y --no-modify-path --default-toolchain stable + fi + export PATH=/usr/local/cargo/bin:$PATH + if [ "$VERSION" == 'latest' ] || [ -z "$VERSION" ]; then + cargo install "$cratesPackage" + else + cargo install "$cratesPackage" --version "$VERSION" + fi + readonly binaryTargetPath="${binaryTargetFolder}/${binaryName}" + ln -sf /usr/local/cargo/bin/"$binaryName" "$binaryTargetPath" + chmod 755 "$binaryTargetPath" + apt_get_cleanup +} +echo_banner "devcontainer.community" +echo "Installing $name..." +install "$@" +echo "(*) Done!" +``` + +### `bun` install method template (reference: `src/critique.work/install.sh`) + +Prefer `bun install -g` over `npm install -g` when both are viable. + +```bash +#!/bin/bash +set -o errexit +set -o pipefail +set -o noclobber +set -o nounset +set -o allexport +readonly binaryName='' +readonly binaryTargetFolder='/usr/local/bin' +apt_get_update() { + if [ "$(find /var/lib/apt/lists/* | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update -y + fi +} +apt_get_checkinstall() { + if ! dpkg -s "$@" >/dev/null 2>&1; then + apt_get_update + DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends --no-install-suggests --option 'Debug::pkgProblemResolver=true' --option 'Debug::pkgAcquire::Worker=1' "$@" + fi +} +apt_get_cleanup() { + apt-get clean + rm -rf /var/lib/apt/lists/* +} +echo_banner() { + local text="$1" + echo -e "\e[1m\e[97m\e[41m$text\e[0m" +} +utils_check_version() { + local version=$1 + if ! [[ "${version:-}" =~ ^(latest|[0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + printf >&2 '=== [ERROR] Option "version" (value: "%s") is not "latest" or valid semantic version format "X.Y.Z" !\n' \ + "$version" + exit 1 + fi +} +bun_ensure_installed() { + if ! command -v bun >/dev/null 2>&1; then + echo "Bun is not installed. Installing bun to /usr/local..." + apt_get_checkinstall unzip curl ca-certificates + export BUN_INSTALL=/usr/local + curl -fsSL https://bun.sh/install | bash + fi +} +install() { + utils_check_version "$VERSION" + export BUN_INSTALL=/usr/local + bun_ensure_installed + if [ "$VERSION" == 'latest' ] || [ -z "$VERSION" ]; then + bun install -g + else + bun install -g "@${VERSION}" + fi + apt_get_cleanup +} +echo_banner "devcontainer.community" +echo "Installing $binaryName..." +install "$@" +echo "(*) Done!" +``` + +All helper functions should be copied verbatim. The canonical source is https://github.com/devcontainer-community/shell-snippets — check there first for the latest versions of shared functions. + +### 4c. `src//NOTES.md` + +```markdown +# + +## Project + +- []() + +## Description + +<2-3 sentence description of what the tool does and its main use case. Use backticks around the CLI command name.> + +## Installation Method + + +- gh release: "Downloaded as a pre-compiled binary from the [GitHub releases page]() and placed in `/usr/local/bin`." +- apt: "Installed via the system APT package manager (`apt-get install `)." +- curl: "Installed via the official install script." +- cargo/bun/npm: "Installed via ` install `." + +## Other Notes + +_No additional notes._ +``` + +Add real notes under "Other Notes" only if there are genuinely important caveats (e.g., requires specific env vars, requires a running service, user-level vs system-level install). + +### 4d. `src//README.md` + +Do **NOT** create this file manually. It is auto-generated by the release workflow from `devcontainer-feature.json` + `NOTES.md`. + +### 4e. `test//test.sh` + +```bash +#!/bin/bash + +set -e + +# Optional: Import test library bundled with the devcontainer CLI +# See https://github.com/devcontainers/cli/blob/HEAD/docs/features/test.md#dev-container-features-test-lib +# Provides the 'check' and 'reportResults' commands. +source dev-container-features-test-lib + +# Feature-specific tests +check "execute command" bash -c " --version | grep ''" + +# Report results +reportResults +``` + +**Choosing the grep pattern:** +- Run the binary's `--version` (or `version`) command to see its actual output format +- Grep for a stable substring (usually the binary name or `version`) +- If `--version` is not supported, use whichever flag produces identifiable output +- For services (like sshd), test that the service binary exists and test relevant functionality + +### 4f. `.github/workflows/test.yaml` + +Do **NOT** modify this file. The CI workflow automatically detects new features via `src/` and `test/` directory changes. + +## Step 5 — Update Root README.md + +Add a new row to the feature table in `README.md` (repo root). **Maintain alphabetical order.** + +Format: +``` +| [](https://github.com/devcontainer-community/devcontainer-features/tree/main/src/) | `` — | | 1.0.0 | +``` + +Where `` is one of: `gh release`, `apt`, `curl`, `cargo`, `bun`, `npm`, `nix`. + +## Step 6 — Set Executable Permissions + +The `install.sh` file MUST have the executable bit set. Run both: + +```bash +chmod +x src//install.sh +git update-index --chmod=+x src//install.sh +``` + +## Step 7 — Validate + +1. Verify `devcontainer-feature.json` is valid JSON and matches the schema +2. Verify `install.sh` has correct shebang, set -o flags, and the executable bit +3. Verify `test.sh` sources `dev-container-features-test-lib` and calls `reportResults` +4. Verify the README.md table entry is in the correct alphabetical position +5. Run CI tests via GitHub Actions — the test workflow will automatically pick up the new feature on a PR: + - `devcontainer features test --skip-scenarios -f -i debian:latest .` + - `devcontainer features test --skip-scenarios -f -i ubuntu:latest .` + - `devcontainer features test --skip-scenarios -f -i mcr.microsoft.com/devcontainers/base:ubuntu .` + +## Checklist + +- [ ] `src//devcontainer-feature.json` — valid JSON, correct `id`, version `1.0.0` +- [ ] `src//install.sh` — working script, executable bit set, helper functions verbatim from template +- [ ] `src//NOTES.md` — links to project, description, install method documented +- [ ] `test//test.sh` — sources test lib, has at least one `check`, calls `reportResults` +- [ ] `README.md` — new row added in alphabetical order +- [ ] `src//README.md` — NOT manually created (auto-generated) +- [ ] Executable bit set via `chmod +x` AND `git update-index --chmod=+x` +- [ ] CI tests pass on all three base images (debian, ubuntu, devcontainers/base)