Skip to content

Commit ae4dd61

Browse files
bilby91claude
andauthored
devcontainer: add prebuild-based dev environment + CI (#88)
* devcontainer: add prebuild-based dev environment + CI Mirrors the crunchloop/dap devcontainer strategy: a two-file build split where CI prebuilds a multi-arch toolchain image and the runtime config just pulls it. - devcontainer-build.json: base:debian + Go 1.26, Node 22, github-cli, docker-in-docker, and local golangci-lint (v2.5.0, pinned to Makefile) and claude-code features. - devcontainer.json + docker-compose.yml: pull the prebuilt image; dind (privileged) so the integration suite can drive docker compose. - post-create.sh: persist Claude config across rebuilds + go mod download. - devcontainer-cache.yml: multi-arch (amd64/arm64) prebuild on main, merged into a :latest manifest on GHCR. - devcontainer-release.yml: publish local features. The Apple container backend stays darwin/arm64-only and is not built in this Linux container, matching the Linux CI jobs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * devcontainer: trim to Go-only toolchain Drop node, vscode-server, and the claude-code feature, plus all vscode customizations and the claude-config volume/symlink plumbing that only existed for Claude Code. The image is now base + Go, github-cli, docker-in-docker, make, and the golangci-lint local feature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent fdd8054 commit ae4dd61

9 files changed

Lines changed: 327 additions & 0 deletions

File tree

.devcontainer/README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Development Container Configuration
2+
3+
This directory contains the devcontainer configuration for developing the
4+
`crunchloop/devcontainer` CLI.
5+
6+
## Key Concepts
7+
8+
The devcontainer uses a **prebuild strategy** (the same one as
9+
`crunchloop/dap`):
10+
11+
1. CI builds a complete development environment image using
12+
`devcontainer-build.json` (base image + features).
13+
2. The image is published multi-arch (amd64 + arm64) to the GitHub Container
14+
Registry as `ghcr.io/crunchloop/devcontainer/devcontainer:latest`.
15+
3. Developers pull that prebuild image via `devcontainer.json`
16+
`docker-compose.yml` instead of building the toolchain locally.
17+
4. `post-create.sh` runs lightweight, per-checkout setup (Go module download).
18+
19+
This keeps container startup fast while the toolchain stays reproducible.
20+
21+
## Contents
22+
23+
- `devcontainer.json` — local development configuration (used by developers).
24+
- `devcontainer-build.json` — prebuild image configuration (used by CI).
25+
- `docker-compose.yml` — runs the prebuilt `app` service.
26+
- `post-create.sh` — per-checkout setup hook.
27+
- `features/golangci-lint` — local feature installing the linter pinned to the
28+
Makefile / `ci.yml` version (`v2.5.0`).
29+
30+
## Toolchain
31+
32+
The prebuild image provides everything the Linux CI jobs need:
33+
34+
- **Go** 1.26 (CI also exercises 1.25; `go.mod` declares 1.25.0).
35+
- **golangci-lint** `v2.5.0` (keep in sync with `Makefile`'s
36+
`GOLANGCI_LINT_VERSION` and the `ci.yml` lint job).
37+
- **docker-in-docker** so the integration suite
38+
(`go test -tags=integration ./test/integration/...`) can drive
39+
`docker` / `docker compose` from inside the container.
40+
- **GitHub CLI** and `make`.
41+
42+
> The Apple `container` backend (`runtime/applecontainer`) is darwin/arm64-only
43+
> and cannot be built inside this Linux container — exactly as on the Linux CI
44+
> jobs, where `make bridge` is a no-op. Use a native macOS checkout for that
45+
> backend.
46+
47+
## Common tasks
48+
49+
```bash
50+
make lint # golangci-lint run ./...
51+
make test # go test -race ./... (bridge is a no-op on Linux)
52+
make test-integration # docker-backed integration suite
53+
```
54+
55+
## CI
56+
57+
- `.github/workflows/devcontainer-cache.yml` — rebuilds and republishes the
58+
prebuild image on pushes to `main` that touch `.devcontainer/**`.
59+
- `.github/workflows/devcontainer-release.yml` — publishes the local
60+
`features/` to GHCR (manual dispatch).
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "devcontainer-build",
3+
"image": "mcr.microsoft.com/devcontainers/base:debian",
4+
5+
// Features to add to the dev container. More info: https://containers.dev/features.
6+
"features": {
7+
// Registry features
8+
"ghcr.io/devcontainers/features/go:1": {
9+
// Primary dev toolchain. CI also exercises 1.25 (see
10+
// .github/workflows/ci.yml matrix); go.mod declares go 1.25.0.
11+
"version": "1.26"
12+
},
13+
"ghcr.io/devcontainers/features/github-cli:1": {},
14+
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
15+
"ghcr.io/rocker-org/devcontainer-features/apt-packages:1": {
16+
"packages": "make"
17+
},
18+
19+
// Local features
20+
"./features/golangci-lint": { "version": "2.5.0" }
21+
}
22+
}

.devcontainer/devcontainer.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "devcontainer",
3+
4+
// Local development configuration. The heavy toolchain image is built in
5+
// CI from devcontainer-build.json and published to GHCR; here we just pull
6+
// it via docker-compose. See README.md for the prebuild strategy.
7+
"dockerComposeFile": [
8+
"docker-compose.yml"
9+
],
10+
11+
"service": "app",
12+
13+
"workspaceFolder": "/workspaces/devcontainer",
14+
15+
// Keep containers running after VS Code shuts down.
16+
"shutdownAction": "stopCompose",
17+
18+
"postCreateCommand": ".devcontainer/post-create.sh"
19+
}

.devcontainer/docker-compose.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
services:
2+
app:
3+
image: ghcr.io/crunchloop/devcontainer/devcontainer:latest
4+
# privileged is required by the docker-in-docker feature so the
5+
# in-container dockerd can start. The integration suite shells out to
6+
# `docker` / `docker compose` (see test/integration and ci.yml), so the
7+
# daemon must be available inside the workspace.
8+
privileged: true
9+
command: sleep infinity
10+
volumes:
11+
- ..:/workspaces/devcontainer
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"id": "golangci-lint",
3+
"version": "0.0.1",
4+
"name": "golangci-lint",
5+
"description": "Installs golangci-lint, the Go linters aggregator. Keep the default version in sync with the Makefile GOLANGCI_LINT_VERSION and the ci.yml lint job.",
6+
"documentationURL": "https://golangci-lint.run",
7+
"installsAfter": [
8+
"ghcr.io/devcontainers/features/go"
9+
],
10+
"options": {
11+
"version": {
12+
"type": "string",
13+
"default": "2.5.0",
14+
"description": "Version of golangci-lint to install, without the leading 'v' (e.g. '2.5.0')."
15+
}
16+
}
17+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
4+
VERSION=${VERSION:-2.5.0}
5+
6+
echo "Installing golangci-lint (version: $VERSION)..."
7+
8+
# Detect architecture
9+
ARCH=$(uname -m)
10+
case $ARCH in
11+
x86_64)
12+
ARCH="amd64"
13+
;;
14+
aarch64|arm64)
15+
ARCH="arm64"
16+
;;
17+
*)
18+
echo "Unsupported architecture: $ARCH"
19+
exit 1
20+
;;
21+
esac
22+
23+
# Detect OS
24+
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
25+
26+
ASSET="golangci-lint-${VERSION}-${OS}-${ARCH}"
27+
DOWNLOAD_URL="https://github.com/golangci/golangci-lint/releases/download/v${VERSION}/${ASSET}.tar.gz"
28+
29+
echo "Downloading from: $DOWNLOAD_URL"
30+
31+
TEMP_DIR=$(mktemp -d)
32+
cd "$TEMP_DIR"
33+
34+
curl -sL "$DOWNLOAD_URL" -o golangci-lint.tar.gz
35+
tar -xzf golangci-lint.tar.gz
36+
37+
# The archive extracts into a directory named after the asset.
38+
cp "${ASSET}/golangci-lint" /usr/local/bin/golangci-lint
39+
chmod +x /usr/local/bin/golangci-lint
40+
41+
# Cleanup
42+
cd /
43+
rm -rf "$TEMP_DIR"
44+
45+
# Verify installation
46+
golangci-lint --version
47+
48+
echo "golangci-lint feature installed successfully!"

.devcontainer/post-create.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#!/bin/bash
2+
# Post-create script for the devcontainer dev environment.
3+
# Runs once after the container is created.
4+
5+
set -e
6+
7+
cd /workspaces/devcontainer
8+
9+
# Warm the Go module cache so the first `make test` / `make lint` is fast.
10+
# golangci-lint is baked into the image (local feature), so we only need to
11+
# fetch dependencies here.
12+
echo "Downloading Go module dependencies..."
13+
go mod download
14+
15+
echo "Post-create setup complete."
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
name: DevContainer Prebuild
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
paths:
8+
- '.devcontainer/**'
9+
- '.github/workflows/devcontainer-cache.yml'
10+
workflow_dispatch:
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: true
15+
16+
permissions:
17+
contents: read
18+
packages: write
19+
id-token: write
20+
21+
env:
22+
DEVCONTAINER_IMAGE: ghcr.io/crunchloop/devcontainer/devcontainer
23+
24+
jobs:
25+
prebuild:
26+
name: Build devcontainer prebuild (${{ matrix.platform }})
27+
strategy:
28+
fail-fast: false
29+
matrix:
30+
include:
31+
- platform: linux/amd64
32+
runner: ubuntu-latest
33+
suffix: amd64
34+
- platform: linux/arm64
35+
runner: ubuntu-24.04-arm
36+
suffix: arm64
37+
runs-on: ${{ matrix.runner }}
38+
steps:
39+
- name: Checkout
40+
uses: actions/checkout@v4
41+
42+
- name: Set up Docker Buildx
43+
uses: docker/setup-buildx-action@v3
44+
45+
- name: Log in to GitHub Container Registry
46+
uses: docker/login-action@v3
47+
with:
48+
registry: ghcr.io
49+
username: ${{ github.actor }}
50+
password: ${{ secrets.GITHUB_TOKEN }}
51+
52+
# devcontainers/ci doesn't cleanly expose digest-only push, so we push
53+
# per-arch tags and merge them into a multi-arch manifest below.
54+
- name: Build devcontainer prebuild image
55+
uses: devcontainers/ci@v0.3
56+
with:
57+
configFile: .devcontainer/devcontainer-build.json
58+
imageName: ${{ env.DEVCONTAINER_IMAGE }}
59+
imageTag: build-${{ github.run_id }}-${{ matrix.suffix }}
60+
platform: ${{ matrix.platform }}
61+
cacheFrom: ${{ env.DEVCONTAINER_IMAGE }}:buildcache-${{ matrix.suffix }}
62+
push: always
63+
64+
merge:
65+
name: Merge multi-arch manifest
66+
needs: prebuild
67+
runs-on: ubuntu-latest
68+
steps:
69+
- name: Set up Docker Buildx
70+
uses: docker/setup-buildx-action@v3
71+
72+
- name: Log in to GitHub Container Registry
73+
uses: docker/login-action@v3
74+
with:
75+
registry: ghcr.io
76+
username: ${{ github.actor }}
77+
password: ${{ secrets.GITHUB_TOKEN }}
78+
79+
- name: Create manifest list and push
80+
run: |
81+
docker buildx imagetools create \
82+
-t ${{ env.DEVCONTAINER_IMAGE }}:latest \
83+
${{ env.DEVCONTAINER_IMAGE }}:build-${{ github.run_id }}-amd64 \
84+
${{ env.DEVCONTAINER_IMAGE }}:build-${{ github.run_id }}-arm64
85+
86+
- name: Inspect manifest
87+
run: docker buildx imagetools inspect ${{ env.DEVCONTAINER_IMAGE }}:latest
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Devcontainer Features Release
2+
on:
3+
workflow_dispatch:
4+
5+
jobs:
6+
publish:
7+
if: ${{ github.ref == 'refs/heads/main' }}
8+
runs-on: ubuntu-latest
9+
permissions:
10+
contents: write
11+
pull-requests: write
12+
packages: write
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Publish Features
17+
uses: devcontainers/action@v1
18+
with:
19+
publish-features: "true"
20+
base-path-to-features: "./.devcontainer/features"
21+
generate-docs: "true"
22+
features-namespace: "crunchloop/devcontainer/devcontainers-features"
23+
24+
env:
25+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26+
27+
- name: Create PR for Documentation
28+
id: push_image_info
29+
env:
30+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31+
run: |
32+
set -e
33+
echo "Start."
34+
# Configure git and Push updates
35+
git config --global user.email github-actions[bot]@users.noreply.github.com
36+
git config --global user.name github-actions[bot]
37+
git config pull.rebase false
38+
branch=automated-documentation-update-$GITHUB_RUN_ID
39+
git checkout -b $branch
40+
message='Automated documentation update'
41+
# Add / update and commit
42+
git add */**/README.md
43+
git commit -m 'Automated documentation update [skip ci]' || export NO_UPDATES=true
44+
# Push
45+
if [ "$NO_UPDATES" != "true" ] ; then
46+
git push origin "$branch"
47+
gh pr create --title "$message" --body "$message"
48+
fi

0 commit comments

Comments
 (0)