diff --git a/claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md b/claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md new file mode 100644 index 0000000..ce58e68 --- /dev/null +++ b/claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md @@ -0,0 +1,299 @@ +--- +name: devcontainer-upstream-sync +description: Use to audit a project's .devcontainer/ and bundled .claude/skills/ against gatezh/devcontainers upstream, adopt missed changes, draft upstream issues for shared bugs, and self-update. Triggers on "check upstream devcontainer", "sync devcontainer with upstream", "is upstream up to date", "check the latest changes", "make sure everything was picked up", "upstream is the source of truth". +metadata: + author: Serge Gatezh + url: https://github.com/gatezh + version: "1.0.0" +--- + +# Devcontainer Upstream Sync + +## Overview + +This project's `.devcontainer/` and bundled `.claude/skills/` track +[`gatezh/devcontainers`](https://github.com/gatezh/devcontainers) (the +`claude-code` template) as the single source of truth. Upstream evolves — +new postStartCommand wiring (e.g. `patch-playwright-mcp`), new bundled +skills, retired allowlist hosts, image refactors. Without an audit loop +those changes silently rot the local config. + +This skill codifies the audit/adopt loop: per-file diff against upstream, +classify drift as intentional vs unwanted, adopt the unwanted, draft +upstream issues for bugs that exist on both sides, and update itself. + +## Decision flow + +```dot +digraph upstream_sync { + rankdir=TB; + "User asked about upstream / sync / drift" [shape=doublecircle]; + "Run audit (Workflow 1)" [shape=box, style=filled, fillcolor="#d4edda"]; + "Per-row verdict" [shape=diamond]; + "Adopt drift (Workflow 2)" [shape=box, style=filled, fillcolor="#d4edda"]; + "Drift = upstream bug?" [shape=diamond]; + "Draft issue (Workflow 3)" [shape=box, style=filled, fillcolor="#fff3cd"]; + "Skill version stale?" [shape=diamond]; + "Self-update (Workflow 4)" [shape=box, style=filled, fillcolor="#d4edda"]; + "Done — report" [shape=doublecircle]; + + "User asked about upstream / sync / drift" -> "Run audit (Workflow 1)"; + "Run audit (Workflow 1)" -> "Per-row verdict"; + "Per-row verdict" -> "Adopt drift (Workflow 2)" [label="framework-track drift"]; + "Per-row verdict" -> "Drift = upstream bug?" [label="local diverges intentionally"]; + "Per-row verdict" -> "Skill version stale?" [label="all clean"]; + "Drift = upstream bug?" -> "Draft issue (Workflow 3)" [label="yes"]; + "Drift = upstream bug?" -> "Skill version stale?" [label="no"]; + "Adopt drift (Workflow 2)" -> "Skill version stale?"; + "Draft issue (Workflow 3)" -> "Skill version stale?"; + "Skill version stale?" -> "Self-update (Workflow 4)" [label="yes"]; + "Skill version stale?" -> "Done — report" [label="no"]; + "Self-update (Workflow 4)" -> "Done — report"; +} +``` + +## Sync manifest + +Authoritative list of what this skill tracks. Spelled out — do not +discover paths from the filesystem; that misses files that should exist +but don't, and includes files that shouldn't be tracked. + +| local path | upstream path | bucket | +|---|---|---| +| `.devcontainer/devcontainer.json` | `claude-code/.devcontainer/devcontainer.json` | framework-track | +| `.devcontainer/docker-compose.yml` | `claude-code/.devcontainer/docker-compose.yml` | starter-customize | +| `.devcontainer/init-plugins.sh` | `claude-code/.devcontainer/init-plugins.sh` | starter-customize | +| `.devcontainer/claude-sandbox/devcontainer.json` | `claude-code/.devcontainer/claude-sandbox/devcontainer.json` | framework-track | +| `.devcontainer/claude-sandbox/docker-compose.yml` | `claude-code/.devcontainer/claude-sandbox/docker-compose.yml` | starter-customize | +| `.devcontainer/claude-sandbox/init-firewall.sh` | `.devcontainer/claude-sandbox/init-firewall.sh` *(repo root, not `claude-code/`)* | exemplar | +| `.claude/skills/sandbox-fetch-docs/SKILL.md` | `claude-code/.claude/skills/sandbox-fetch-docs/SKILL.md` | framework-track | +| `.claude/skills/sandbox-playwright/SKILL.md` | `claude-code/.claude/skills/sandbox-playwright/SKILL.md` | framework-track | +| `.claude/skills/devcontainer-upstream-sync/SKILL.md` | `claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md` | framework-track | +| `.claude/settings.json` | `claude-code/.claude/settings.json` | starter-customize | +| `mise.toml` | `claude-code/mise.toml` | starter-customize | + +### Bucket meanings + +- **framework-track** — should match upstream byte-for-byte modulo + the project-name substitution in §"Customization-preservation rules". + Drift is a bug to fix locally, or a bug to file upstream if upstream + has it too. +- **starter-customize** — upstream is a starter template; downstream + edits are *expected* (project-specific extension lists, allowlists, + marketplaces, tool versions). Don't auto-overwrite. Surface + upstream's diff and ask which hunks to adopt. +- **exemplar** — upstream provides a canonical example at a + *different* path than the framework root. Treat as starter-customize, + but use the special source path noted in the manifest. + +## Customization-preservation rules + +When adopting an upstream change, preserve these per-project patterns: + +1. **Volume-name prefixes.** Upstream uses `myproject-…`; downstream + uses `-…`. Read the project name from + `.devcontainer/docker-compose.yml`'s top-level `name:` field, then + substitute. +2. **VS Code `extensions` list.** Keep local additions (e.g. Hugo, + OpenAI Codex). When adopting upstream, only port over upstream's + *new* additions; don't strip what's local. +3. **`runArgs`** like `--hostname=` — keep verbatim. +4. **`postCreateCommand` / `postStartCommand`.** These chain commands. + Keep local additions (e.g. `bash .devcontainer/init-plugins.sh`, + `ln -sfn /workspace/.codex ~/.codex`); sync upstream's *required* + additions like `/usr/local/bin/patch-playwright-mcp`. +5. **`init-firewall.sh` allowlist** and **`init-plugins.sh` plugin + list** — never auto-overwrite. Surface upstream's recipe; ask + before adopting. +6. **`mise.toml` versions.** Versions may legitimately lead or lag + upstream. Surface; ask. + +## Conventions + +- Upstream branch is `master` (not `main`). +- Raw URL prefix: + `https://raw.githubusercontent.com/gatezh/devcontainers/master/` +- Use `gh api` for repo introspection (commits, issues, file listings) + and `curl -sSL ` for plain file fetches. +- All workflows are read-only by default. Apply changes only after + showing the diff to the user. + +## Workflow 1: Audit + +Walk the manifest, fetch each upstream file, diff, classify, report. + +```sh +# Per-row commands (run for each manifest row): +curl -sSL "https://raw.githubusercontent.com/gatezh/devcontainers/master/${UPSTREAM_PATH}" -o /tmp/upstream-sync/$(basename "${UPSTREAM_PATH}").upstream +diff -u "${LOCAL_PATH}" /tmp/upstream-sync/$(basename "${UPSTREAM_PATH}").upstream || true +``` + +Classification rules per row: + +- Diff is empty → **clean** (or `clean (after project-name substitution)`). +- Diff is non-empty AND row is `framework-track`: + - If every hunk maps to a customization-preservation pattern → **intentional-customization**. + - Otherwise → **drift-needs-adopt**. Note the offending hunks. +- Diff is non-empty AND row is `starter-customize` or `exemplar`: + - **template-divergence**. Show the diff in the report; do not adopt + automatically. + +Report format (one row per manifest entry): + +``` +✓ .devcontainer/claude-sandbox/devcontainer.json framework-track clean +✓ .devcontainer/devcontainer.json framework-track intentional-customization (Hugo extensions, hostname) +⚠ .devcontainer/claude-sandbox/init-firewall.sh exemplar template-divergence: see Workflow 2 +✗ .claude/skills/sandbox-playwright/SKILL.md framework-track drift-needs-adopt: 3 hunks +``` + +Audit is read-only. Don't apply edits in this phase. + +## Workflow 2: Adopt + +Driven by Workflow 1's report. For each row that needs action: + +### `drift-needs-adopt` (framework-track) + +1. Print the diff (already in `/tmp/upstream-sync/…`). +2. Confirm with the user it's not actually intentional. +3. Apply via `Edit` (preserving customization-preservation rules in §3). +4. Stage the file: `git add `. Don't commit — let the user + review and gate. + +### `template-divergence` (starter-customize, exemplar) + +1. Print the diff. +2. Walk hunks one at a time (or in a small batch) with the user. +3. Apply only the hunks they accept. +4. Stage; don't commit. + +### Special cases + +- **`init-firewall.sh` (exemplar):** the canonical upstream copy is at + the **repo root**, not under `claude-code/`. URL is + `…/master/.devcontainer/claude-sandbox/init-firewall.sh`. The + `claude-code/` template intentionally omits this file because + per-project allowlist customization is the whole point. +- **`postStartCommand` chains:** when upstream adds a step (e.g. + `/usr/local/bin/patch-playwright-mcp`), splice it into the local + chain rather than overwriting the chain. Verify the binary is in the + image first: + ```sh + docker run --rm --entrypoint /bin/sh \ + ghcr.io/gatezh/devcontainers/claude-code-sandbox:latest \ + -c 'ls -la /usr/local/bin/' + ``` + +## Workflow 3: Draft upstream issue + +When Workflow 1 surfaces drift that exists in *both* local and +upstream — i.e. the upstream is shipping a bug — draft an issue. + +### Search for existing reports first + +```sh +gh issue list --repo gatezh/devcontainers --search '' --state all --json number,title,state +gh search issues --repo gatezh/devcontainers '' --json title,state,number +``` + +If a relevant open issue exists, **stop**. Add a comment if needed; do +not file a duplicate. + +### Draft the issue body + +Save to `docs/upstream-issue-.md` in the project. Use this +template (replace placeholders): + +```markdown +## Summary + + + +## Reproduction + +1. +2. + +``` + +``` + +## Verification + +``` + +``` + +## Suggested fix + + + +## Affected files in this repo + +- `:` +- `` (if a sibling copy exists) + +## Related + +- +``` + +### Print the file command + +```sh +gh issue create --repo gatezh/devcontainers \ + --title "[BUG] " \ + --body-file docs/upstream-issue-.md +``` + +**Do not run the `gh issue create` command.** Filing public issues is a +user action — print the command and let the user run it. The harness +blocks unattended posts to external systems, correctly. + +## Workflow 4: Self-update + +Check whether this skill itself is up to date. + +1. Fetch upstream's frontmatter: + ```sh + curl -sSL "https://raw.githubusercontent.com/gatezh/devcontainers/master/claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md" \ + | head -30 + ``` +2. Extract `version:` from upstream and compare to the local file's + `version:` (this file's frontmatter). +3. If versions match → done. +4. If upstream is newer: + - Fetch the full upstream SKILL.md to a temp path. + - `diff -u .claude/skills/devcontainer-upstream-sync/SKILL.md /tmp/upstream-skill.md` + - Show the diff to the user. + - On accept: overwrite the local file. **Do not auto-overwrite.** +5. If local is newer (e.g. the user is working on a future version): + report; do nothing. + +## One-time install (for new projects) + +For a new downstream project to adopt this skill, paste these three +lines once: + +```sh +mkdir -p .claude/skills/devcontainer-upstream-sync +curl -sSL https://raw.githubusercontent.com/gatezh/devcontainers/master/claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md \ + > .claude/skills/devcontainer-upstream-sync/SKILL.md +git add .claude/skills/devcontainer-upstream-sync/SKILL.md +``` + +After the one-time copy, the skill manages its own updates via +Workflow 4. + +## Out of scope + +- **Auto-merging Dockerfile changes.** The Dockerfile is a baked-image + concern; downstream consumes the image, not the Dockerfile. +- **`init-plugins.sh` semantic merge.** The marketplace and plugin + lists are legitimately project-specific. Skill surfaces upstream's + diff and stops. +- **Reverse direction (project → upstream PRs).** Filing issues is in + scope; opening upstream PRs uses normal `gh pr create` flow, not + this skill. diff --git a/claude-code/README.md b/claude-code/README.md index 51567b3..5b3937a 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -126,6 +126,20 @@ Both image variants ship system chromium and the `/usr/local/bin/patch-playwrigh Copy `.claude/skills/sandbox-playwright/` into your project's `.claude/skills/` directory so Claude Code picks it up automatically. +### Recommended: Claude Code skill for upstream sync + +To keep your project's `.devcontainer/` and bundled `.claude/skills/` in step with this repo, copy the [devcontainer-upstream-sync](.claude/skills/devcontainer-upstream-sync/SKILL.md) skill into your project's `.claude/skills/` directory. The skill audits drift, helps adopt missed changes, drafts upstream issues for shared bugs, and self-updates when this skill's `version:` bumps. + +```bash +mkdir -p .claude/skills/devcontainer-upstream-sync +curl -sSL https://raw.githubusercontent.com/gatezh/devcontainers/master/claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md \ + > .claude/skills/devcontainer-upstream-sync/SKILL.md +git add .claude/skills/devcontainer-upstream-sync/SKILL.md +``` + +After the one-time copy, the skill manages its own updates. + + ### Sandbox Authentication The sandbox firewall blocks outbound traffic, so `claude login` (which opens a browser OAuth flow) won't work inside the container. Instead, generate a token on the host and inject it via environment variable. @@ -202,8 +216,10 @@ The template includes extensions for Claude Code, Bun, OXC, Tailwind, YAML, Dock └── skills/ ├── sandbox-fetch-docs/ │ └── SKILL.md ← teaches Claude Code to fetch docs within sandbox firewall - └── sandbox-playwright/ - └── SKILL.md ← teaches Claude Code to drive Playwright MCP + @playwright/test + ├── sandbox-playwright/ + │ └── SKILL.md ← teaches Claude Code to drive Playwright MCP + @playwright/test + └── devcontainer-upstream-sync/ + └── SKILL.md ← keeps project's .devcontainer/ + skills synced with this repo ``` ## Workspace Directory Layout