Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .actrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Use a slim ubuntu image for the ubuntu-latest runner.
# catthehacker/ubuntu:act-latest is the recommended act runner image.
-P ubuntu-latest=catthehacker/ubuntu:act-latest

# Mount the local repo into the container instead of cloning from GitHub.
# This makes act fast and works without a network connection to GitHub.
--bind

# Remove the container after the run completes.
--rm
82 changes: 82 additions & 0 deletions .github/instructions/FEATURE.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
applyTo: "features/**"
excludeAgent: "code-review"
---

# Copilot Agent Instructions for Devcontainer Features

When the Copilot cloud agent creates or edits any file under `features/`,
it must follow these rules automatically.

## Version Bump Rule

**Any change to a feature's behavior requires a version bump in
`devcontainer-feature.json`.**

The publish workflow (`.github/workflows/publish.yml`) skips features whose
`id:version` OCI image already exists in the registry. If the version is not
bumped after a behavior change, the change will never be published.

Changes that require a version bump:
- New option added or option removed
- Option default value changed (e.g. `biomeVersion` `2.4.12` → `2.5.0`)
- `install.sh` behavior changed or bug fixed
- `platforms` or `installsAfter` array changed

Changes that do not require a version bump:
- Comment-only edits to `install.sh`
- Formatting changes
- Changes to `test.sh` or `docs/` that do not affect install behavior

## Atomic Update Rule

When bumping a version or changing an option default, always update all
affected files together in the same commit:

1. `features/<scope>/<id>/devcontainer-feature.json` — `"version"` field and
option `"default"` value
2. `test/<scope>/<id>/test.sh` — `grep "<version>"` assertions
3. `test/<scope>/<id>/scenarios.json` — step descriptions mentioning the
version
4. `docs/features/<id>.md` — usage snippet and options default listing

Use the `bump-feature` skill for guided step-by-step assistance.

## Five-File Completeness Rule

Every feature must have exactly five files. Before committing, verify all are
present:

```text
features/<scope>/<id>/devcontainer-feature.json
features/<scope>/<id>/install.sh
test/<scope>/<id>/test.sh
test/<scope>/<id>/scenarios.json
docs/features/<id>.md
```

Run `pnpm run validate-feature <scope>/<id>` to check completeness.

## `documentationURL` Must Match Actual Doc File

The `documentationURL` field in `devcontainer-feature.json` must always point
to an existing file in `docs/features/`. The filename must match the feature
`id` exactly (e.g. `claude-code.md` for `id: "claude-code"`).

```json
"documentationURL": "https://github.com/savvy-web/devcontainers/blob/main/docs/features/<id>.md"
```

Rules:
- The URL must use the `https://github.com/savvy-web/devcontainers/blob/main/` prefix
- The URL must resolve to an actual file in `docs/features/`
- Never use a generic URL or the repository homepage
- Run `pnpm run validate-feature <scope>/<id>` to verify the URL resolves

## Scope Assignment

- `features/global/` — language-agnostic tools (Biome, Rust, Zig, act, Claude Code)
- `features/node/` — Node.js ecosystem tools (pnpm)

Place the feature in the scope that best matches its target runtime. When in
doubt, use `global/`.
105 changes: 105 additions & 0 deletions .github/instructions/SHELL.instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
applyTo: "**/*.sh"
excludeAgent: "code-review"
---

# Copilot Agent Instructions for Shell Scripts

When the Copilot cloud agent creates or edits any shell script (`*.sh`), it
must follow these conventions without being asked.

## Required Header

Every shell script must start with exactly these two lines:

```bash
#!/usr/bin/env bash
set -euo pipefail
```

No exceptions. `set -euo pipefail` ensures the script exits immediately on
errors, treats unset variables as errors, and propagates pipe failures.

## Env Var Defaults

Always use a defensive default that handles both unset variables and empty
strings injected by the devcontainer CLI:

```bash
# Correct — handles unset and empty string
VERSION="${BIOME_VERSION:-latest}"

# Wrong — only handles unset, not empty string
VERSION="${BIOME_VERSION:?must be set}"
```

## Logging Conventions

Use these prefixes consistently. Error messages must go to stderr.

| Prefix | Destination | When to use |
| :----- | :---------- | :---------- |
| `[INFO]` | stdout | Normal progress steps |
| `[ERROR]` | stderr (`>&2`) | Fatal errors — always followed by `exit 1` |
| `[SUCCESS]` | stdout | Final confirmation of a successful install |
| `[FAIL]` | stderr (`>&2`) | Test assertion failures in `test.sh` |
| `[PASS]` | stdout | Test assertion success in `test.sh` |

## Architecture Detection

Normalize `uname -m` output early. Match the naming convention used by the
upstream download URL:

```bash
ARCH=$(uname -m)
OS=$(uname -s | tr '[:upper:]' '[:lower:]')

# For tools that use "x64" / "arm64":
if [[ "$ARCH" == "x86_64" ]]; then
ARCH="x64"
elif [[ "$ARCH" == "arm64" || "$ARCH" == "aarch64" ]]; then
ARCH="arm64"
else
echo "[ERROR] Unsupported architecture: $ARCH" >&2
exit 1
fi
```

Always include an `else` branch that errors on unsupported architectures.

## Install Validation

Every `install.sh` must end with a validation step that verifies the tool is
on `$PATH` and prints its version:

```bash
if ! command -v <binary> &>/dev/null; then
echo "[ERROR] <binary> not found in PATH after install." >&2
exit 1
fi
<binary> --version
```

## Idempotency

Guard installs that would fail or corrupt state on a re-run:

```bash
if ! command -v <tool> >/dev/null 2>&1; then
echo "[INFO] Installing <tool>..."
# ... install commands ...
fi
```

For tools installed to a fixed path where overwriting is safe, no guard is
needed.

## Linting

After editing a shell script, run shellcheck if it is available:

```bash
shellcheck <file>
```

If shellcheck is not installed, note any remaining issues in a comment.
39 changes: 39 additions & 0 deletions .github/scripts/collect-test-dirs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env node
// Discovers all devcontainer test directories under test/ and outputs them
// as a JSON array for use as a GitHub Actions matrix.
//
// Usage: node .github/scripts/collect-test-dirs.js
// Output: JSON array of { scope, id, testdir } objects

import { existsSync, readdirSync, statSync } from "node:fs";
import { join, resolve } from "node:path";

const testRoot = resolve(import.meta.dirname, "../../test");

if (!existsSync(testRoot)) {
process.stdout.write("[]");
process.exit(0);
}

const entries = [];

for (const scope of readdirSync(testRoot)) {
const scopeDir = join(testRoot, scope);
if (!statSync(scopeDir).isDirectory()) continue;

for (const id of readdirSync(scopeDir)) {
const idDir = join(scopeDir, id);
if (!statSync(idDir).isDirectory()) continue;

const testSh = join(idDir, "test.sh");
if (existsSync(testSh)) {
entries.push({
scope,
id,
testdir: `test/${scope}/${id}`,
});
}
}
}

process.stdout.write(JSON.stringify(entries));
139 changes: 139 additions & 0 deletions .github/skills/bump-feature/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
---
name: bump-feature
allowed-tools: read_file, write_file, list_files
description: >-
Use when asked to bump the version of a devcontainer feature. Triggers for
prompts like "bump version for X", "release a new version of the X feature",
"update the X feature version", or "the X feature changed and needs a
version bump".
---

# Bump Devcontainer Feature Version

Bumps a feature version atomically — ensuring that every place the version or
pinned default appears is updated together. A partial bump causes the publish
workflow to either skip the feature or publish it with mismatched metadata.

## When to Use

Bump the feature version whenever:

- The install script behavior changes (new tool version default, new option,
changed download URL)
- The feature options change (new option added, option default changed)
- A bug in `install.sh` is fixed
- The `devcontainer-feature.json` metadata changes (name, description,
`installsAfter`, `platforms`)

**Do not** bump the version for:

- Changes to `test.sh` that don't affect feature behavior
- Changes to `docs/features/<id>.md` that don't reflect a behavior change
- Formatting or comment-only changes to `install.sh`

## Workflow

### Step 1 — Identify the feature

Ask the user which feature to bump if not already specified.
Read `features/<scope>/<id>/devcontainer-feature.json` to find the current
version.

### Step 2 — Determine the new version

Apply standard semver rules:

| Change type | Bump | Example |
| :---------- | :--- | :------ |
| Bug fix in `install.sh`, no behavior change | `patch` | `0.1.0` → `0.1.1` |
| New option added (backward-compatible) | `minor` | `0.1.0` → `0.2.0` |
| Option removed, renamed, or semantics changed | `major` | `0.1.0` → `1.0.0` |
| Default version of installed tool updated | `patch` | `0.1.0` → `0.1.1` |

If the feature is pre-1.0 (`0.x.y`), breaking changes may use `minor` instead
of `major` at the author's discretion — document the decision.

Confirm the new version with the user before proceeding.

### Step 3 — Update all version references

Update every file that contains the old version. The full set to check:

#### `features/<scope>/<id>/devcontainer-feature.json`

Update the `"version"` field:

```json
"version": "<new-version>"
```

#### `test/<scope>/<id>/test.sh`

Update any `grep "<old-version>"` assertions that check the pinned default
tool version. Only update the version being bumped — leave other version
strings alone.

#### `test/<scope>/<id>/scenarios.json`

Update step descriptions that mention the old version string.

#### `docs/features/<id>.md`

Update the version string in the `## Usage` jsonc block:

```jsonc
"ghcr.io/savvy-web/<id>:<new-version>": {}
```

If the `## Options` section lists a default value that changed, update it.

### Step 4 — Update `install.sh` defaults (when the pinned tool version changed)

If the reason for the bump is a new pinned tool version (e.g. Biome `2.4.12` →
`2.5.0`), update the option default in `devcontainer-feature.json`:

```json
"biomeVersion": {
"type": "string",
"default": "2.5.0",
...
}
```

This default flows into `test.sh` assertions and docs — update them too.

### Step 5 — Verify consistency

After all edits, run a final consistency check:

1. `version` in `devcontainer-feature.json` matches the new version
2. All `grep "<version>"` assertions in `test.sh` use the new pinned defaults
3. `docs/features/<id>.md` usage snippet references the new feature version
4. `documentationURL` still points to the correct path (it should never change)

Run the validation script to catch anything missed:

```bash
pnpm run validate-feature <scope>/<id>
```

## Completion Checklist

- [ ] `devcontainer-feature.json` `"version"` updated
- [ ] `test/<scope>/<id>/test.sh` version assertions updated (if tool default changed)
- [ ] `test/<scope>/<id>/scenarios.json` step descriptions updated (if version mentioned)
- [ ] `docs/features/<id>.md` usage snippet updated
- [ ] `install.sh` option defaults updated (if pinned tool version changed)
- [ ] `pnpm run validate-feature <scope>/<id>` passes

## Common Mistakes

- **Bumping version but not test assertions** — if you changed the default
Biome version from `2.4.12` to `2.5.0` but left `grep "2.4.12"` in
`test.sh`, the test will fail on the new default
- **Not bumping version after changing install behavior** — the publish
workflow skips features whose `id:version` image already exists in the
registry. If the version is not bumped, the change will never be published
- **Forgetting the docs snippet** — the `## Usage` block in the doc file
typically contains the feature version; if it is not updated, users will
reference the old version in their `devcontainer.json`
Loading
Loading