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
37 changes: 20 additions & 17 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ jobs:
echo "::notice::Diff base = ${MERGE_BASE} → HEAD ($(git rev-parse HEAD))"

# Computed once, reused per-feature below instead of re-running git
# diff in the loop. Covers both src/<name>/ (what ships) and
# test/<name>/test.sh — a sibling of src/, not nested under it, but
# AGENTS.md requires a bump for it too, so it needs its own pathspec.
# diff in the loop. test/ is included only so a test-only PR still
# resolves a feature name to check (and reports "no bump needed"
# correctly) — it never counts toward requiring a bump itself.
touched_files=$(git diff --name-only "${MERGE_BASE}..HEAD" -- src/ test/)

mapfile -t names < <(echo "$touched_files" | awk -F/ '{print $2}' | sort -u)
Expand All @@ -53,22 +53,20 @@ jobs:
new_version=$(jq -r '.version' "$manifest")
old_version=$(git show "${MERGE_BASE}:${manifest}" 2>/dev/null | jq -r '.version' 2>/dev/null || echo "")

# A version bump is required for anything under src/<name>/ except
# its README, or for test/<name>/test.sh specifically — the one
# test file CI runs against every consumer's build. A new
# test/<name>/scenarios.json or scenario script is test-only, like
# a README change: it verifies a non-default option combination
# but doesn't touch what already-published consumers get.
non_readme_touched=$({ \
echo "$touched_files" | grep -E "^src/${name}/" | grep -v "^src/${name}/README\.md$"; \
echo "$touched_files" | grep -E "^test/${name}/test\.sh$"; \
} || true)
# A version bump is required for anything under src/<name>/
# except its README — nothing under test/<name>/ counts,
# including test.sh itself. Tests and CI are invisible to the
# consumer: bumping for them publishes a new GHCR tag and
# triggers a docs rebuild for a change nobody running the
# feature can actually see.
non_readme_touched=$(echo "$touched_files" \
| grep -E "^src/${name}/" | grep -v "^src/${name}/README\.md$" || true)

if [ -z "$old_version" ]; then
echo "✅ ${name}: new feature, no bump required"
elif [ "$old_version" = "$new_version" ]; then
if [ -z "$non_readme_touched" ]; then
echo "ℹ️ ${name}: README-only change, version bump optional (still ${new_version})"
echo "ℹ️ ${name}: README/test-only change, no bump needed (still ${new_version})"
else
echo "❌ ${name}: touched but version unchanged (${new_version})"
FAILED=1
Expand Down Expand Up @@ -160,6 +158,8 @@ jobs:
baseImage: mcr.microsoft.com/devcontainers/base:debian
- features: github-dev
baseImage: mcr.microsoft.com/devcontainers/base:ubuntu
- features: copilot-dev
baseImage: mcr.microsoft.com/devcontainers/base:ubuntu
- features: mistral-dev
baseImage: ubuntu:latest
- features: mistral-dev
Expand Down Expand Up @@ -211,6 +211,8 @@ jobs:
# exists. Add an entry here whenever a feature gains a
# scenarios.json; see AGENTS.md "Testing non-default options".
- features: claude-dev
- features: cline-dev
- features: mistral-dev

steps:
- name: Checkout
Expand All @@ -233,9 +235,10 @@ jobs:
with:
severity: warning
scandir: src
# Preserve the previous scope (install.sh only) — path-keys.sh, sync-files.sh,
# and gh-auth.sh have never been linted here and may have pre-existing issues.
ignore_names: "path-keys.sh sync-files.sh gh-auth.sh"
# gh-auth.sh is sourced from /etc/profile.d/, so it can't carry a
# shebang — shellcheck's SC2148 ("target shell unknown") on that is
# expected and not fixable without breaking how the file is used.
ignore_names: "gh-auth.sh"

- name: Verify AGENTS.md's feature table matches manifest versions
run: |
Expand Down
7 changes: 7 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ jobs:
- features: github-dev
baseImage: mcr.microsoft.com/devcontainers/base:ubuntu

# copilot-dev - no dependsOn on github-dev; gh CLI absent by
# default, install.sh's own graceful-skip path
- features: copilot-dev
baseImage: mcr.microsoft.com/devcontainers/base:ubuntu

# dotfiles-sync - works on any base image
- features: dotfiles-sync
baseImage: mcr.microsoft.com/devcontainers/base:debian
Expand Down Expand Up @@ -166,6 +171,8 @@ jobs:
# exists. Add an entry here whenever a feature gains a
# scenarios.json; see AGENTS.md "Testing non-default options".
- features: claude-dev
- features: cline-dev
- features: mistral-dev

steps:
- name: Checkout
Expand Down
58 changes: 30 additions & 28 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,9 @@ re-run once per that job's `baseImage` matrix entries. Not every option needs a
reserve it for an off-by-default path whose failure mode is silent (a warn-and-continue on
install failure, not a hard `exit 1`) rather than one that already fails loudly if broken.

Adding a `scenarios.json` or scenario script doesn't need a version bump — like a README, it
verifies an existing option combination without changing what already-published consumers get.
Only `test/<name>/test.sh` itself (the one CI runs against every consumer's build) triggers the
version-bump-check below.
Adding a `scenarios.json`, a scenario script, or editing `test.sh` itself never needs a version
bump — none of it is visible to a consumer of the feature, see "Modifying an existing feature —
version bump" below.

**Available features:**

Expand Down Expand Up @@ -92,32 +91,35 @@ version-bump-check below.

**Modifying an existing feature — version bump:**

Any change under `src/<name>/` that touches `install.sh`,
`devcontainer-feature.json`, or `test/<name>/test.sh` must bump that
feature's `version` field (patch by default, minor/major when warranted) —
`release.yml` only tags and publishes a feature whose `version` changed
between the base branch and HEAD, so an unbumped change to something that
actually ships silently never gets published. Bump it **once per branch**:
if the version on the branch already differs from `main`'s, a further commit
on that same branch/PR must *not* bump it again — check the diff against
`main` first, don't bump reflexively on every commit.
Any change under `src/<name>/` that touches `install.sh` or
`devcontainer-feature.json` must bump that feature's `version` field (patch
by default, minor/major when warranted) — `release.yml` only tags and
publishes a feature whose `version` changed between the base branch and
HEAD, so an unbumped change to something that actually ships silently
never gets published. Bump it **once per branch**: if the version on the
branch already differs from `main`'s, a further commit on that same
branch/PR must *not* bump it again — check the diff against `main` first,
don't bump reflexively on every commit.

A **README-only** change doesn't require a bump — nothing about what ships
in the image changes. It's still worth bumping when the doc fix is
safety-relevant (e.g. a corrected `initializeCommand` requirement, like
`dotfiles-sync` v1.0.8), since `release.yml`'s version-diff gate is also
what triggers the website docs rebuild — an unbumped README fix never
reaches the published site. Judgment call, not enforced either way.
**Never bump for something invisible to the consumer** — nothing under
`test/<name>/` (including `test.sh` itself and any scenario file), and no
CI workflow change, ever needs a bump. A version bump publishes a new GHCR
tag and triggers a website docs rebuild; doing that for a change nobody
running the feature can actually see is pure noise. A **README-only**
change doesn't require one either — nothing about what ships in the image
changes. It's still worth bumping a README fix when it's safety-relevant
(e.g. a corrected `initializeCommand` requirement, like `dotfiles-sync`
v1.0.8), since the docs-rebuild trigger is exactly what's needed there.
Judgment call, not enforced either way.

Enforced by the `version-bump-check` job in `pr-validation.yml`: it fails the
PR if a touched feature's `version` is unchanged from `main` *and* something
other than `src/<name>/README.md` changed under `src/<name>/`, or
`test/<name>/test.sh` specifically changed (a new `test/<name>/scenarios.json`
or scenario script doesn't count — see "Testing non-default options" above).
It's a blocking check only — it never commits a bump on your behalf (deliberately:
no bot commits, no push-permission/fork edge cases, consistent with how
`conventional-commits` already works in this repo). Bump the version
yourself and push again.
Enforced by the `version-bump-check` job in `pr-validation.yml`: it fails
the PR if a touched feature's `version` is unchanged from `main` *and*
something other than `src/<name>/README.md` changed under `src/<name>/`.
Nothing under `test/<name>/` ever counts toward this check — see "Testing
non-default options" above. It's a blocking check only — it never commits
a bump on your behalf (deliberately: no bot commits, no push-permission/
fork edge cases, consistent with how `conventional-commits` already works
in this repo). Bump the version yourself and push again.

**A lost version bump after merge gets flagged, not just caught before merge.**
`version-bump-check` only validates the PR branch — it can't catch a bump
Expand Down
10 changes: 10 additions & 0 deletions test/cline-dev/scenarios.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"with_cli": {
"image": "mcr.microsoft.com/devcontainers/typescript-node:22",
"features": {
"cline-dev": {
"installCli": true
}
}
}
}
28 changes: 28 additions & 0 deletions test/cline-dev/with_cli.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash

# This file is part of helpers4.
# Copyright (C) 2026 baxyz
# SPDX-License-Identifier: LGPL-3.0-or-later
#
# Exercises installCli:true — the default-off code path plain test.sh never
# runs. install.sh already degrades gracefully (warns instead of aborting)
# on a failed npm install, which is correct, but that means a real failure
# here would otherwise be silent — assert the CLI is actually there.

set -e

echo "Testing cline-dev with installCli:true..."

if ! command -v cline >/dev/null 2>&1; then
echo "❌ FAIL: cline CLI not found — installCli:true did not install it"
exit 1
fi
echo "✅ PASS: cline CLI on PATH"

if ! cline --version >/dev/null 2>&1; then
echo "❌ FAIL: cline is on PATH but does not run"
exit 1
fi
echo "✅ PASS: cline --version runs"

echo "🎉 Test passed."
28 changes: 28 additions & 0 deletions test/copilot-dev/test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash

# This file is part of helpers4.
# Copyright (C) 2026 baxyz
# SPDX-License-Identifier: LGPL-3.0-or-later
#
# copilot-dev has no dependsOn on github-dev (AI assistant only, deliberately
# separate from platform tooling — see the README), so a standalone test has
# no gh CLI to install the extension against. That's the actual default
# path most consumers hit before adding github-dev themselves; assert it
# degrades the way the feature documents, not just "didn't crash".

set -e

echo "Testing copilot-dev feature..."

if command -v gh >/dev/null 2>&1; then
if gh extension list 2>/dev/null | grep -qi copilot; then
echo "✅ PASS: gh copilot extension installed (gh CLI was present)"
else
echo "❌ FAIL: gh CLI present but gh copilot extension not installed"
exit 1
fi
else
echo "✅ PASS: gh CLI absent — install.sh's documented graceful-skip path, as expected without github-dev"
fi

echo "🎉 Test passed."
20 changes: 17 additions & 3 deletions test/essential-dev/test.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,22 @@ set -e

echo "🧪 Testing essential-dev feature..."

# The feature is configuration-only (VS Code extensions)
# These tests verify the feature executes without errors
# The only real, build-time artifact this feature writes to disk — the VS
# Code extensions/settings in its manifest are applied by the devcontainer
# CLI itself, not by install.sh, so there's nothing else on the filesystem
# to assert here.
FIXUP="$(git config --system --get alias.fixup || true)"
if [ "${FIXUP}" != "commit --fixup" ]; then
echo "❌ FAIL: git alias.fixup is '${FIXUP}', expected 'commit --fixup'"
exit 1
fi
echo "✅ PASS: git alias.fixup configured"

POLISH="$(git config --system --get alias.polish || true)"
if [ "${POLISH}" != "rebase -i --autosquash origin/HEAD" ]; then
echo "❌ FAIL: git alias.polish is '${POLISH}', expected 'rebase -i --autosquash origin/HEAD'"
exit 1
fi
echo "✅ PASS: git alias.polish configured"

echo "✅ Feature executed successfully"
echo "✅ essential-dev tests passed"
10 changes: 10 additions & 0 deletions test/mistral-dev/scenarios.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"with_cli": {
"image": "mcr.microsoft.com/devcontainers/python:3.12",
"features": {
"mistral-dev": {
"installCli": true
}
}
}
}
28 changes: 28 additions & 0 deletions test/mistral-dev/with_cli.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash

# This file is part of helpers4.
# Copyright (C) 2026 baxyz
# SPDX-License-Identifier: LGPL-3.0-or-later
#
# Exercises installCli:true — the default-off code path plain test.sh never
# runs. install.sh's uv branch in particular trusts `uv tool install`
# without verifying the binary actually landed afterward — assert it here
# rather than leaving that path unverified.

set -e

echo "Testing mistral-dev with installCli:true..."

if ! command -v vibe >/dev/null 2>&1; then
echo "❌ FAIL: vibe CLI not found — installCli:true did not install it"
exit 1
fi
echo "✅ PASS: vibe CLI on PATH"

if ! vibe --version >/dev/null 2>&1; then
echo "❌ FAIL: vibe is on PATH but does not run"
exit 1
fi
echo "✅ PASS: vibe --version runs"

echo "🎉 Test passed."
Loading