From 24b733eed53f126a4159ca03b29af4dd20ed383d Mon Sep 17 00:00:00 2001 From: Serge Gatezh <2880401+gatezh@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:47:18 -0600 Subject: [PATCH] feat(claude-code): bake gh-stack extension and add stacked-PRs skill Agents in claude-code containers fell back to hand-chaining PRs with `gh pr create --base`, which GitHub cannot merge atomically. Native stacks need the github/gh-stack extension, but a runtime `gh extension install` writes to ~/.local/share/gh on the container overlay (only ~/.config/gh is a volume), so it vanished on every rebuild. A runtime install is also likely blocked by the sandbox firewall (release-assets.githubusercontent.com). - Install gh-stack at build time as node in both the claude-code image (default + sandbox, via base) and the repo-root devcontainer. The install works unauthenticated. Pinned via GH_STACK_VERSION with a renovate annotation, grouped with the other devcontainer tools (3-day soak). - Assert `gh stack --version` in the PR-time and post-build verify steps. `gh extension list` is not usable there: it requires gh auth. - Add a stacked-prs skill: gh stack init/add/submit for new work, link for existing PRs, merge --yes for atomic merges, the non-interactive flags, and never hand-chain --base PRs. Track it in the devcontainer-upstream-sync manifest (skill bumped to 1.2.0). - Document the extension, arg and skill in the READMEs. Fixes #160 --- .devcontainer/Dockerfile | 6 +++ .github/renovate.json5 | 1 + .github/workflows/build-claude-code.yml | 8 ++-- .github/workflows/ci.yml | 4 +- README.md | 11 +++-- .../devcontainer-upstream-sync/SKILL.md | 3 +- .../.claude/skills/stacked-prs/SKILL.md | 48 +++++++++++++++++++ claude-code/.devcontainer/Dockerfile | 7 +++ claude-code/README.md | 14 +++++- 9 files changed, 88 insertions(+), 14 deletions(-) create mode 100644 claude-code/.claude/skills/stacked-prs/SKILL.md diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 2d6b0db..d9a0763 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -179,6 +179,12 @@ COPY --from=ralphex-download /usr/local/bin/ralphex /usr/local/bin/ralphex COPY --from=hadolint-download /usr/local/bin/hadolint /usr/local/bin/hadolint COPY --from=actionlint-download /usr/local/bin/actionlint /usr/local/bin/actionlint +# ── gh-stack (native stacked PRs) ──────────────────────────────────────────── +# Installed as node so it lands in ~/.local/share/gh/extensions. See #160. +# renovate: datasource=github-releases depName=github/gh-stack +ARG GH_STACK_VERSION=0.1.1 +RUN gh extension install github/gh-stack --pin "v${GH_STACK_VERSION}" + # ── Claude Code CLI ────────────────────────────────────────────────────────── # npm install (not native installer) to avoid rate-limiting in parallel builds. # See: claude-code/.devcontainer/Dockerfile for rationale. diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 6d686ee..6864f17 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -37,6 +37,7 @@ '@anthropic-ai/claude-code', 'agent-browser', 'cli/cli', // gh — upstream .deb; apt's trixie build is frozen at 2.46.0 + 'github/gh-stack', // gh extension for native stacked PRs 'docker/cli', // docker CLI static binary (download.docker.com). // github-tags, NOT github-releases: moby/moby tags its // releases 'docker-v29.8.0', which extractVersion cannot diff --git a/.github/workflows/build-claude-code.yml b/.github/workflows/build-claude-code.yml index 1d4c6b1..b122872 100644 --- a/.github/workflows/build-claude-code.yml +++ b/.github/workflows/build-claude-code.yml @@ -75,19 +75,19 @@ jobs: matrix: include: - image-suffix: claude-code - verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" + verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && gh stack --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" runner: ubuntu-24.04 arch: amd64 - image-suffix: claude-code - verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" + verify-command: "bun --version || true && claude --version && mise --version && zsh --version && gh --version && gh stack --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" runner: ubuntu-24.04-arm arch: arm64 - image-suffix: claude-code-sandbox - verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" + verify-command: "claude --version && mise --version && zsh --version && gh --version && gh stack --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" runner: ubuntu-24.04 arch: amd64 - image-suffix: claude-code-sandbox - verify-command: "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" + verify-command: "claude --version && mise --version && zsh --version && gh --version && gh stack --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" runner: ubuntu-24.04-arm arch: arm64 runs-on: ${{ matrix.runner }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61a8355..fdca654 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,12 +117,12 @@ jobs: add_image "claude-code" \ "claude-code/.devcontainer" \ "claude-code/.devcontainer/Dockerfile" \ - "bun --version || true && claude --version && mise --version && zsh --version && gh --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" \ + "bun --version || true && claude --version && mise --version && zsh --version && gh --version && gh stack --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && printenv AGENT_BROWSER_EXECUTABLE_PATH | grep -qx /usr/bin/chromium && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" \ "default" add_image "claude-code-sandbox" \ "claude-code/.devcontainer" \ "claude-code/.devcontainer/Dockerfile" \ - "claude --version && mise --version && zsh --version && gh --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" \ + "claude --version && mise --version && zsh --version && gh --version && gh stack --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp && test -r /etc/claude-code/managed-settings.json && jq -r '.hooks.SessionStart[0].hooks[0].command' /etc/claude-code/managed-settings.json | grep -qx /usr/local/bin/patch-playwright-mcp && zsh -ic 'typeset -p ZSH_THEME' | grep -q powerlevel10k/powerlevel10k && stat -c %U /home/node/.local/share | grep -qx node" \ "sandbox" fi diff --git a/README.md b/README.md index 08d4415..67b8fcc 100644 --- a/README.md +++ b/README.md @@ -168,11 +168,12 @@ Images from this repository are built and published to GitHub Container Registry ### Automatically, via Renovate The agent tooling in the `claude-code` and `ralphex-fe` images — `rtk`, `ralphex`, the Claude Code -CLI, and `agent-browser` — is pinned as `ARG`s carrying `# renovate:` annotations. Renovate watches -their releases and opens a single grouped bump PR when one ships; CI verifies it, it auto-merges, and -that merge rebuilds the affected images. No upstream release means no PR and no rebuild. Scope and -grouping live in [`.github/renovate.json5`](./.github/renovate.json5); the Dependency Dashboard -issue tracks what is pending. Everything else — including base images and Bun/Hugo — stays manual. +CLI, `agent-browser`, and the `gh-stack` extension — is pinned as `ARG`s carrying `# renovate:` +annotations. Renovate watches their releases and opens a single grouped bump PR when one ships; CI +verifies it, it auto-merges, and that merge rebuilds the affected images. No upstream release means no +PR and no rebuild. Scope and grouping live in [`.github/renovate.json5`](./.github/renovate.json5); +the Dependency Dashboard issue tracks what is pending. Everything else — including base images and +Bun/Hugo — stays manual. > **Setup requirement — Mend portal toggles.** Installing the Renovate app with "All repositories" > makes Mend default the repo to **Silent mode** (`dryRun=lookup`), where it scans and shows updates diff --git a/claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md b/claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md index 8b9080a..eb1eb18 100644 --- a/claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md +++ b/claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md @@ -4,7 +4,7 @@ description: Use to audit a project's .devcontainer/ and bundled .claude/skills/ metadata: author: Serge Gatezh url: https://github.com/gatezh - version: "1.1.0" + version: "1.2.0" --- # Devcontainer Upstream Sync @@ -68,6 +68,7 @@ but don't, and includes files that shouldn't be tracked. | `.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/stacked-prs/SKILL.md` | `claude-code/.claude/skills/stacked-prs/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 | diff --git a/claude-code/.claude/skills/stacked-prs/SKILL.md b/claude-code/.claude/skills/stacked-prs/SKILL.md new file mode 100644 index 0000000..cba779f --- /dev/null +++ b/claude-code/.claude/skills/stacked-prs/SKILL.md @@ -0,0 +1,48 @@ +--- +name: stacked-prs +description: Use when opening PRs for branches that depend on each other, splitting work into a chain of PRs, or merging several dependent PRs together — in THIS devcontainer. Triggers on "stacked PRs", "stack these PRs", "open PRs for dependent branches", "chain PRs", "PR on top of PR", "merge all at once", or whenever about to run `gh pr create --base `. +compatibility: Designed for the gatezh/devcontainers claude-code image, which bakes in the github/gh-stack gh extension. Elsewhere, run `gh extension install github/gh-stack` first. +metadata: + author: Serge Gatezh + url: https://github.com/gatezh + version: "1.0.0" +--- + +# Stacked PRs with `gh stack` + +## Overview + +GitHub has native stacked PRs, driven by the `gh stack` extension (preinstalled in this image — check with `gh stack --version`). A native stack links the PRs on GitHub, keeps each PR's base on the branch below it, and can merge the stack atomically. + +**Never hand-chain PRs with `gh pr create --base `.** GitHub cannot merge that chain as one unit, and every merge below forces a manual retarget and rebase above. If PRs are already hand-chained, convert them with `gh stack link`. + +Order is always **bottom → top**: the bottom branch is based on trunk and merges first. + +## Which command + +| Situation | Command | +|---|---| +| New multi-part work | `gh stack init `, commit, then `gh stack add ` per layer | +| Adopt existing local branches | `gh stack init ` | +| Push everything and open the PRs | `gh stack submit --auto` (drafts; add `--open` for ready-for-review) | +| PRs or branches already exist | `gh stack link ... ` — PR numbers, URLs or branch names | +| Merge up to and including PR N | `gh stack merge --yes` — all-or-nothing | +| Trunk moved / a lower PR merged | `gh stack sync` (fetch, rebase, push, refresh PR state) | +| Inspect the stack | `gh stack view --json` | + +`link` pushes branch arguments, opens PRs for branches that lack one, and fixes wrong base branches. It only adds to a stack, never removes. + +## Rules for non-interactive use + +Agent shells may look like a TTY, and bare commands then block on a prompt or TUI. Always pass arguments and flags: + +- `init` and `add` with explicit branch names; `submit --auto`; `merge --yes`; `view --json`. +- Never run `gh stack modify` or `gh stack switch` — both are TUI-only. +- With more than one git remote, pass `--remote ` to `submit`, `push`, `sync`, `rebase` and `link`, or set `git config remote.pushDefault `. +- `submit --auto` generates PR titles and bodies. Set them afterwards with `gh pr edit --title ... --body-file ...`. +- Use `gh stack merge`, not `gh pr merge`, for stacked PRs. Pass `--squash`, `--merge` or `--rebase`, or it reuses the last method. + +## More detail + +- `gh stack --help` is authoritative. `gh stack help ` prints only the top-level help. +- Docs: https://gh.io/stacks. For the upstream skill, which covers rebase conflicts, stack design and troubleshooting, run `gh skill install github/gh-stack`. diff --git a/claude-code/.devcontainer/Dockerfile b/claude-code/.devcontainer/Dockerfile index 21682a1..1650c3d 100644 --- a/claude-code/.devcontainer/Dockerfile +++ b/claude-code/.devcontainer/Dockerfile @@ -195,6 +195,13 @@ RUN mkdir -p /etc/claude-code COPY --chown=root:root --chmod=0644 managed-settings.json /etc/claude-code/managed-settings.json USER node +# ── gh-stack (native stacked PRs) ──────────────────────────────────────────── +# Baked as node into ~/.local/share/gh/extensions, which is on the image layer (only +# ~/.config/gh is a volume); a volume over ~/.local/share/gh would shadow it. See #160. +# renovate: datasource=github-releases depName=github/gh-stack +ARG GH_STACK_VERSION=0.1.1 +RUN gh extension install github/gh-stack --pin "v${GH_STACK_VERSION}" + # ── Claude Code CLI ─────────────────────────────────────────────────────────── # npm, not the native installer: the installer rate-limits (429) under parallel # Docker builds. npm stays supported "for compatibility reasons" — this is that diff --git a/claude-code/README.md b/claude-code/README.md index fc00253..725962c 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -19,6 +19,7 @@ Projects consume these pre-built images and control their own tool versions via | Shell | zsh, oh-my-zsh (`git`, `fzf` plugins), powerlevel10k | Completions, git aliases and prompt integration | | Tools | gh CLI, git, curl, jq, less, fzf, procps, openssh-client | Standard dev utilities (`openssh-client` provides `ssh`/`ssh-keygen` — enables SSH-format commit signing) | | Mise | The tool manager itself (not the tools) | Projects run `mise install` at container creation for their tool versions | +| gh-stack | `gh` extension, pinned `ARG` bumped by Renovate | Native stacked PRs (`gh stack`). Baked in because `~/.local/share/gh` is not a volume, so a runtime `gh extension install` is lost on rebuild | | rtk, ralphex | Pinned `ARG`s, bumped by Renovate on each GitHub release | Dev infrastructure (like Claude Code) — the image tracks the versions so projects don't have to | | Claude Code | npm global install | npm avoids rate limiting that affects the native installer in parallel CI builds | @@ -42,7 +43,7 @@ Both variants are built for: ## Automatic Rebuilds -The image rebuilds automatically whenever one of its pinned tools — Claude Code, agent-browser, rtk, or ralphex — publishes a new release: Renovate opens a version-bump PR, CI verifies it, it auto-merges, and the merge builds the image on native runners for both amd64 and arm64 (no QEMU emulation). Manual rebuilds can be triggered via the "Run workflow" button in the Actions UI. +The image rebuilds automatically whenever one of its pinned tools — Claude Code, agent-browser, gh, gh-stack, rtk, or ralphex — publishes a new release: Renovate opens a version-bump PR, CI verifies it, it auto-merges, and the merge builds the image on native runners for both amd64 and arm64 (no QEMU emulation). Manual rebuilds can be triggered via the "Run workflow" button in the Actions UI. ## Quick Start @@ -148,6 +149,12 @@ 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 stacked PRs + +Both image variants bake in the official [`github/gh-stack`](https://github.com/github/gh-stack) extension, so `gh stack` can open, link and atomically merge native [stacked PRs](https://gh.io/stacks). The [stacked-prs](.claude/skills/stacked-prs/SKILL.md) skill tells Claude Code to use it instead of hand-chaining PRs with `gh pr create --base`, and which flags keep it non-interactive. + +Copy `.claude/skills/stacked-prs/` 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. @@ -240,6 +247,8 @@ The template includes extensions for Claude Code, Bun, OXC, Tailwind, YAML, Dock │ └── SKILL.md ← teaches Claude Code to fetch docs within sandbox firewall ├── sandbox-playwright/ │ └── SKILL.md ← teaches Claude Code to drive Playwright MCP + @playwright/test + ├── stacked-prs/ + │ └── SKILL.md ← teaches Claude Code to use native stacked PRs (gh stack) └── devcontainer-upstream-sync/ └── SKILL.md ← keeps project's .devcontainer/ + skills synced with this repo ``` @@ -413,10 +422,11 @@ more often than right. | `CLAUDE_CODE_VERSION` | Renovate | Claude Code CLI | | `AGENT_BROWSER_VERSION` | Renovate | agent-browser, default target only | | `GH_VERSION` | Renovate | GitHub CLI — from the upstream `.deb`, not apt (trixie freezes gh at 2.46.0) | +| `GH_STACK_VERSION` | Renovate | gh-stack extension (`gh stack`) | | `OH_MY_ZSH_REF` | by hand | oh-my-zsh, pinned to a commit SHA | | `POWERLEVEL10K_REF` | by hand | powerlevel10k, pinned to a commit SHA | -The five Renovate-managed args carry `# renovate:` annotations in the Dockerfile; edit them by +The six Renovate-managed args carry `# renovate:` annotations in the Dockerfile; edit them by hand only for a local build. Bumps land as auto-merged PRs — see [Automatic Rebuilds](#automatic-rebuilds). ## Building Locally / Local Fallback