Skip to content

Devcontainer feature#861

Open
coakenfold wants to merge 17 commits intomicrosoft:mainfrom
coakenfold:feat/717-dev-container
Open

Devcontainer feature#861
coakenfold wants to merge 17 commits intomicrosoft:mainfrom
coakenfold:feat/717-dev-container

Conversation

@coakenfold
Copy link
Copy Markdown
Contributor

Description

Packages apm-cli as a reusable Dev Container Feature under devcontainer/.

The feature installs uv, Python 3.10+, git, and apm-cli inside the container. It supports a version option (latest or semver), declares installsAfter: ghcr.io/devcontainers/features/python for correct ordering, and handles the PEP 668 --break-system-packages retry on Ubuntu 24.04.

Also included: 37 bats unit tests covering every branch of install.sh (stub-based, no Docker needed), and a full integration test matrix across Ubuntu 24.04, Ubuntu 22.04, Debian 12, Alpine 3.20, Fedora 41, and a pinned-version + Python-feature scenario.

See devcontainer/README.md for more info

Fixes #717

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Maintenance / refactor

Testing

  • Tested locally
  • All existing tests pass
  • Added tests for new functionality (if applicable)

Unit tests (no Docker required):

cd devcontainer/test/apm/unit
../../bats/bin/bats install.bats

Integration tests (requires Docker and @devcontainers/cli):

devcontainer features test \
  --features apm \
  --skip-autogenerated \
  --project-folder devcontainer

Copilot AI review requested due to automatic review settings April 23, 2026 02:56
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a reusable Dev Container Feature under devcontainer/ to install apm-cli (plus prerequisites) in containers, along with unit + integration test coverage for the feature.

Changes:

  • Introduces a devcontainer feature (devcontainer-feature.json + install.sh) that installs uv, ensures Python 3.10+ and git, and installs apm-cli with a PEP 668 retry path.
  • Adds Bats unit tests for install.sh and devcontainer CLI integration scenarios across several base images and configuration variants.
  • Adds supporting docs (devcontainer/README.md), a local sync helper script, and git submodules for the Bats test framework.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
devcontainer/src/apm/install.sh Feature install script: uv install, Python/git setup, pip install of apm-cli with PEP 668 retry
devcontainer/src/apm/devcontainer-feature.json Feature manifest and version option + installsAfter ordering
devcontainer/test/apm/scenarios.json Integration test matrix (distros + pinned version + python feature)
devcontainer/test/apm/*.sh Scenario validation scripts + shared generic checks
devcontainer/test/apm/unit/install.bats Stub-based unit tests covering install.sh branches
devcontainer/scripts/sync-local-devcontainer.sh Helper to copy the local feature into .devcontainer/ for VS Code consumption
devcontainer/README.md Feature usage, structure, and test documentation
.gitmodules Adds bats-core, bats-support, bats-assert as submodules

Comment thread devcontainer/README.md
Comment on lines +1 to +56
# APM Dev Container Feature — Overview

A comprehensive reference for the APM (Agent Package Manager) Dev Container Feature: what it does, how it's structured, how to use it, how it's tested, and where it's supported.

---

## 1. Feature Overview

The APM Dev Container Feature packages the `apm-cli` tool as a reusable, declarative unit that can be added to any project's `devcontainer.json`. It eliminates the need for manual `postCreateCommand` installs and makes APM discoverable through the standard [Dev Container Features ecosystem](https://containers.dev/features).

**What it installs**

- [uv](https://github.com/astral-sh/uv) — Astral's fast Python tool (installed to `/usr/local/bin`)
- Python 3.10+ — only if not already present
- `git` — required by `apm-cli` (uses GitPython at startup)
- `apm-cli` — installed via `pip` (with automatic PEP 668 fallback)

**What motivated it**

- APM was previously only installable via ad-hoc `postCreateCommand` lines — not reusable, not discoverable, hard to standardise.
- See [docs/01-feature-717.md](docs/01-feature-717.md) for the original feature request.

**Options**

| Option | Type | Default | Description |
| --------- | ------ | -------- | -------------------------------------------------------------------- |
| `version` | string | `latest` | Version of `apm-cli` to install. `latest`, or a semver like `1.2.3`. |

The feature declares `installsAfter: ghcr.io/devcontainers/features/python` so the official Python feature (when present) runs first and provides Python.

---

## 2. `devcontainer` Directory Structure

```
devcontainer/
├── src/
│ └── apm/
│ ├── devcontainer-feature.json # Feature manifest (id, options, metadata)
│ └── install.sh # Install script executed inside the container
└── test/
├── apm/
│ ├── scenarios.json # Integration test matrix (base image × options)
│ ├── generic-checks.sh # Shared post-install checks (apm on PATH, --version, --help)
│ ├── default-ubuntu-24.sh # Ubuntu 24.04 scenario (PEP 668 path)
│ ├── default-debian-12.sh # Debian 12 scenario (apt-get path)
│ ├── default-alpine-3.sh # Alpine 3.20 scenario (apk path)
│ ├── default-fedora.sh # Fedora 41 scenario (dnf path)
│ ├── pinned-version.sh # Confirms `version: "0.8.11"` option is honoured
│ ├── with-python-feature.sh # Confirms compatibility with the Python feature
│ ├── test.sh # Fallback "auto" test (currently unused)
│ └── unit/
│ └── install.bats # Bats unit tests for install.sh (37 tests)
├── bats/ # git submodule — bats-core runner
└── test_helper/ # git submodules — bats-support, bats-assert
```
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

This README contains multiple non-ASCII characters (em dashes, ">="-like symbols, multiplication sign in "image x options", section sign, and box-drawing tree characters). Repo convention requires Markdown docs to stay within printable ASCII for cross-platform terminals. Please replace these characters with ASCII equivalents and adjust the tree diagram accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines +22 to +28
# ── Install uv (idempotent — skip if already on PATH) ────────────────────────
if command -v uv >/dev/null 2>&1; then
echo "uv already installed at $(command -v uv) — skipping"
else
# curl is only needed to fetch the uv installer
if ! command -v curl >/dev/null 2>&1; then
echo "curl not found — installing..."
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

Non-ASCII characters are present in this script (box-drawing separator characters like "--"-looking glyphs and em dashes in output such as "uv already installed ..."). Repo convention requires all source files and CLI output strings to stay within printable ASCII to avoid UnicodeEncodeError on Windows cp1252 terminals. Please replace these with plain ASCII (e.g., use "--" in comments and "-" in messages).

Suggested change
# ── Install uv (idempotent skip if already on PATH) ────────────────────────
if command -v uv >/dev/null 2>&1; then
echo "uv already installed at $(command -v uv) skipping"
else
# curl is only needed to fetch the uv installer
if ! command -v curl >/dev/null 2>&1; then
echo "curl not found installing..."
# -- Install uv (idempotent - skip if already on PATH) --
if command -v uv >/dev/null 2>&1; then
echo "uv already installed at $(command -v uv) - skipping"
else
# curl is only needed to fetch the uv installer
if ! command -v curl >/dev/null 2>&1; then
echo "curl not found - installing..."

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +116
#!/usr/bin/env bats
# Unit tests for devcontainer/src/apm/install.sh
# PATH is fully isolated to STUB_BIN — no network, no real packages, no Docker.

load "../../test_helper/bats-support/load"
load "../../test_helper/bats-assert/load"

INSTALL_SH="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../../src/apm" && pwd)/install.sh"

# ── Helpers ──────────────────────────────────────────────────────────────────

setup() {
STUB_BIN="$BATS_TEST_TMPDIR/bin"
/bin/mkdir -p "$STUB_BIN"
export STUB_BIN

# Delegate stubs for utilities install.sh needs with real behaviour.
# Resolved against the real PATH before we lock it down.
local real_grep real_sh real_mktemp
real_grep="$(PATH=/usr/bin:/bin command -v grep)"
real_sh="$(PATH=/usr/bin:/bin command -v sh)"
real_mktemp="$(PATH=/usr/bin:/bin command -v mktemp)"
printf '#!/bin/sh\nexec "%s" "$@"\n' "$real_grep" > "$STUB_BIN/grep"
printf '#!/bin/sh\nexec "%s" "$@"\n' "$real_sh" > "$STUB_BIN/sh"
printf '#!/bin/sh\nexec "%s" "$@"\n' "$real_mktemp" > "$STUB_BIN/mktemp"
/bin/chmod +x "$STUB_BIN/grep" "$STUB_BIN/sh" "$STUB_BIN/mktemp"

# Pre-stage python3 stub content; package-manager stubs cp this into place.
/bin/cat > "$STUB_BIN/_python3_stub" <<'EOF'
#!/bin/sh
case "$*" in
*version_info.minor*) echo "12" ;;
*version_info.major*) echo "3" ;;
*version_info*3*) echo "3.12.0" ;;
*) exit 0 ;;
esac
EOF
/bin/chmod +x "$STUB_BIN/_python3_stub"
# NOTE: PATH is NOT locked here — test code needs rm, cat, etc.
# We'll lock it per-test using run_with_stubs()
}

# Helper: runs sh with PATH locked to STUB_BIN + /bin (for sh, core utilities)
# STUB_BIN is first so stubs shadow any real system commands.
run_with_stubs() {
PATH="$STUB_BIN:/bin" run sh "$INSTALL_SH" "$@"
}

# make_stub <name> <exit_code> [output_text]
make_stub() {
local name="$1" rc="$2" out="${3:-}"
{
printf '#!/bin/sh\n'
[ -n "$out" ] && printf 'echo "%s"\n' "$out"
printf 'exit %d\n' "$rc"
} > "$STUB_BIN/$name"
/bin/chmod +x "$STUB_BIN/$name"
}

# Copies the pre-staged python3 stub into STUB_BIN.
make_python3_stub() {
/bin/cp "$STUB_BIN/_python3_stub" "$STUB_BIN/python3"
}

# make_old_python3_stub <major> <minor> — simulates an older Python.
make_old_python3_stub() {
local major="${1:-3}" minor="${2:-8}"
/bin/cat > "$STUB_BIN/python3" <<EOF
#!/bin/sh
case "\$*" in
*version_info.minor*) echo "$minor" ;;
*version_info.major*) echo "$major" ;;
*version_info*3*) echo "$major.$minor.0" ;;
*) exit 0 ;;
esac
EOF
/bin/chmod +x "$STUB_BIN/python3"
}

# make_pkg_mgr_stub <cmd> — creates a package-manager stub that side-effects
# a python3 stub (simulating a successful install of python3) and records args.
make_pkg_mgr_stub() {
local cmd="$1"
/bin/cat > "$STUB_BIN/$cmd" <<EOF
#!/bin/sh
echo "\$@" >> "${STUB_BIN}/_${cmd}_args"
/bin/cp "${STUB_BIN}/_python3_stub" "${STUB_BIN}/python3"
exit 0
EOF
/bin/chmod +x "$STUB_BIN/$cmd"
}

# Full happy-path environment: root, all tools present, install succeeds.
setup_happy_path() {
make_stub id 0 "0"
make_stub uv 0 "0.4.0"
make_python3_stub
make_stub git 0
make_stub pip3 0 "Successfully installed apm-cli"
make_stub apm 0 "0.9.0"
}

# ── Root check ────────────────────────────────────────────────────────────────

@test "exits 1 with clear message when not run as root" {
make_stub id 0 "1" # id -u → 1 (non-root)

run_with_stubs

assert_failure
assert_output --partial "must run as root"
}

# ── Python 3 install ──────────────────────────────────────────────────────────

@test "installs python3 via apt-get when missing" {
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

This bats test file includes non-ASCII characters (box-drawing separators, em dashes, and a right-arrow in a comment). Repo convention requires all source files to be printable ASCII; please replace these with ASCII equivalents (e.g., "--" and "->").

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +12

# ── Ubuntu 24.04 specific: PEP 668 distro ──────────────────────────────────────

check "Running on Ubuntu 24.04 (PEP 668 distro)" \
bash -c "grep -q 'PRETTY_NAME=\"Ubuntu 24.04' /etc/os-release"

Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

This test script uses non-ASCII box-drawing characters in comment separators. Repo convention requires printable ASCII only; please replace those separator lines with ASCII-only equivalents (e.g., "# --" style).

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +12

# ── Debian-specific: confirm apt-get path was exercised ──────────────────────

check "apt-get is the system package manager" \
command -v apt-get

Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

This test script uses non-ASCII box-drawing characters in comment separators. Repo convention requires printable ASCII only; please replace those separator lines with ASCII-only equivalents (e.g., "# --" style).

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +12

# ── Alpine-specific: confirm apk path was exercised ──────────────────────────

check "apk is the system package manager" \
command -v apk

Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

This test script uses non-ASCII box-drawing characters in comment separators. Repo convention requires printable ASCII only; please replace those separator lines with ASCII-only equivalents (e.g., "# --" style).

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +12

# ── Fedora-specific: confirm dnf path was exercised ──────────────────────────

check "dnf is the system package manager" \
command -v dnf

Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

This test script uses non-ASCII box-drawing characters in comment separators. Repo convention requires printable ASCII only; please replace those separator lines with ASCII-only equivalents (e.g., "# --" style).

Copilot uses AI. Check for mistakes.
Comment thread devcontainer/README.md
Comment on lines +86 to +90
## 4. How to use `devcontainer` in your project

### Option A — test it locally

Recent versions of the Dev Containers CLI (bundled with `ms-vscode-remote.remote-containers` ≥ 0.454.0) enforce that a local Feature path must resolve **inside** the `.devcontainer/` folder. An upward `../devcontainer/src/apm` path — and symlinks pointing outside `.devcontainer/` — are rejected with:
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

This adds a new user-facing installation path (Dev Container Feature), but the main Starlight docs under docs/src/content/docs/ do not appear to mention it. Please add a short section to docs/src/content/docs/getting-started/installation.md (or another appropriate page) describing how to consume the feature and the supported version option.

Copilot uses AI. Check for mistakes.

# ── Verify ───────────────────────────────────────────────────────────────────
if command -v apm >/dev/null 2>&1; then
echo "✓ APM $(apm --version) installed at $(command -v apm)"
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

The success message prints a non-ASCII checkmark character. Please replace it with an ASCII-safe status marker (prefer the repo's bracket convention like "[+]"), and keep the rest of the message ASCII-only.

Suggested change
echo " APM $(apm --version) installed at $(command -v apm)"
echo "[+] APM $(apm --version) installed at $(command -v apm)"

Copilot uses AI. Check for mistakes.
"id": "apm",
"version": "1.0.0",
"name": "APM (Agent Package Manager)",
"description": "Installs the APM CLI — a manifest-driven dependency manager for AI agents. Lets you declare agent skills, plugins, and MCP servers in apm.yml and reproduce them across environments.",
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

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

The feature manifest description contains a non-ASCII em dash character. Repo convention requires all source text to be printable ASCII; please replace it with a plain '-' or rephrase.

Suggested change
"description": "Installs the APM CLI a manifest-driven dependency manager for AI agents. Lets you declare agent skills, plugins, and MCP servers in apm.yml and reproduce them across environments.",
"description": "Installs the APM CLI - a manifest-driven dependency manager for AI agents. Lets you declare agent skills, plugins, and MCP servers in apm.yml and reproduce them across environments.",

Copilot uses AI. Check for mistakes.
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.

[FEATURE] Create a Devcontainer feature

2 participants