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
60 changes: 60 additions & 0 deletions .devcontainer/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Development Container Configuration

This directory contains the devcontainer configuration for developing the
`crunchloop/devcontainer` CLI.

## Key Concepts

The devcontainer uses a **prebuild strategy** (the same one as
`crunchloop/dap`):

1. CI builds a complete development environment image using
`devcontainer-build.json` (base image + features).
2. The image is published multi-arch (amd64 + arm64) to the GitHub Container
Registry as `ghcr.io/crunchloop/devcontainer/devcontainer:latest`.
3. Developers pull that prebuild image via `devcontainer.json` →
`docker-compose.yml` instead of building the toolchain locally.
4. `post-create.sh` runs lightweight, per-checkout setup (Go module download).

This keeps container startup fast while the toolchain stays reproducible.

## Contents

- `devcontainer.json` — local development configuration (used by developers).
- `devcontainer-build.json` — prebuild image configuration (used by CI).
- `docker-compose.yml` — runs the prebuilt `app` service.
- `post-create.sh` — per-checkout setup hook.
- `features/golangci-lint` — local feature installing the linter pinned to the
Makefile / `ci.yml` version (`v2.5.0`).

## Toolchain

The prebuild image provides everything the Linux CI jobs need:

- **Go** 1.26 (CI also exercises 1.25; `go.mod` declares 1.25.0).
- **golangci-lint** `v2.5.0` (keep in sync with `Makefile`'s
`GOLANGCI_LINT_VERSION` and the `ci.yml` lint job).
- **docker-in-docker** so the integration suite
(`go test -tags=integration ./test/integration/...`) can drive
`docker` / `docker compose` from inside the container.
- **GitHub CLI** and `make`.

> The Apple `container` backend (`runtime/applecontainer`) is darwin/arm64-only
> and cannot be built inside this Linux container — exactly as on the Linux CI
> jobs, where `make bridge` is a no-op. Use a native macOS checkout for that
> backend.

## Common tasks

```bash
make lint # golangci-lint run ./...
make test # go test -race ./... (bridge is a no-op on Linux)
make test-integration # docker-backed integration suite
```

## CI

- `.github/workflows/devcontainer-cache.yml` — rebuilds and republishes the
prebuild image on pushes to `main` that touch `.devcontainer/**`.
- `.github/workflows/devcontainer-release.yml` — publishes the local
`features/` to GHCR (manual dispatch).
22 changes: 22 additions & 0 deletions .devcontainer/devcontainer-build.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "devcontainer-build",
"image": "mcr.microsoft.com/devcontainers/base:debian",

// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
// Registry features
"ghcr.io/devcontainers/features/go:1": {
// Primary dev toolchain. CI also exercises 1.25 (see
// .github/workflows/ci.yml matrix); go.mod declares go 1.25.0.
"version": "1.26"
},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/docker-in-docker:1": {},
"ghcr.io/rocker-org/devcontainer-features/apt-packages:1": {
"packages": "make"
},

// Local features
"./features/golangci-lint": { "version": "2.5.0" }
}
}
19 changes: 19 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "devcontainer",

// Local development configuration. The heavy toolchain image is built in
// CI from devcontainer-build.json and published to GHCR; here we just pull
// it via docker-compose. See README.md for the prebuild strategy.
"dockerComposeFile": [
"docker-compose.yml"
],

"service": "app",

"workspaceFolder": "/workspaces/devcontainer",

// Keep containers running after VS Code shuts down.
"shutdownAction": "stopCompose",

"postCreateCommand": ".devcontainer/post-create.sh"
}
11 changes: 11 additions & 0 deletions .devcontainer/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
services:
app:
image: ghcr.io/crunchloop/devcontainer/devcontainer:latest
# privileged is required by the docker-in-docker feature so the
# in-container dockerd can start. The integration suite shells out to
# `docker` / `docker compose` (see test/integration and ci.yml), so the
# daemon must be available inside the workspace.
privileged: true
command: sleep infinity
volumes:
- ..:/workspaces/devcontainer
17 changes: 17 additions & 0 deletions .devcontainer/features/golangci-lint/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"id": "golangci-lint",
"version": "0.0.1",
"name": "golangci-lint",
"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.",
"documentationURL": "https://golangci-lint.run",
"installsAfter": [
"ghcr.io/devcontainers/features/go"
],
"options": {
"version": {
"type": "string",
"default": "2.5.0",
"description": "Version of golangci-lint to install, without the leading 'v' (e.g. '2.5.0')."
}
}
}
48 changes: 48 additions & 0 deletions .devcontainer/features/golangci-lint/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -e

VERSION=${VERSION:-2.5.0}

echo "Installing golangci-lint (version: $VERSION)..."

# Detect architecture
ARCH=$(uname -m)
case $ARCH in
x86_64)
ARCH="amd64"
;;
aarch64|arm64)
ARCH="arm64"
;;
*)
echo "Unsupported architecture: $ARCH"
exit 1
;;
esac

# Detect OS
OS=$(uname -s | tr '[:upper:]' '[:lower:]')

ASSET="golangci-lint-${VERSION}-${OS}-${ARCH}"
DOWNLOAD_URL="https://github.com/golangci/golangci-lint/releases/download/v${VERSION}/${ASSET}.tar.gz"

echo "Downloading from: $DOWNLOAD_URL"

TEMP_DIR=$(mktemp -d)
cd "$TEMP_DIR"

curl -sL "$DOWNLOAD_URL" -o golangci-lint.tar.gz
tar -xzf golangci-lint.tar.gz

# The archive extracts into a directory named after the asset.
cp "${ASSET}/golangci-lint" /usr/local/bin/golangci-lint
chmod +x /usr/local/bin/golangci-lint

# Cleanup
cd /
rm -rf "$TEMP_DIR"

# Verify installation
golangci-lint --version

echo "golangci-lint feature installed successfully!"
15 changes: 15 additions & 0 deletions .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/bin/bash
# Post-create script for the devcontainer dev environment.
# Runs once after the container is created.

set -e

cd /workspaces/devcontainer

# Warm the Go module cache so the first `make test` / `make lint` is fast.
# golangci-lint is baked into the image (local feature), so we only need to
# fetch dependencies here.
echo "Downloading Go module dependencies..."
go mod download

echo "Post-create setup complete."
87 changes: 87 additions & 0 deletions .github/workflows/devcontainer-cache.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: DevContainer Prebuild

on:
push:
branches:
- main
paths:
- '.devcontainer/**'
- '.github/workflows/devcontainer-cache.yml'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read
packages: write
id-token: write

env:
DEVCONTAINER_IMAGE: ghcr.io/crunchloop/devcontainer/devcontainer

jobs:
prebuild:
name: Build devcontainer prebuild (${{ matrix.platform }})
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
suffix: amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
suffix: arm64
runs-on: ${{ matrix.runner }}
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# devcontainers/ci doesn't cleanly expose digest-only push, so we push
# per-arch tags and merge them into a multi-arch manifest below.
- name: Build devcontainer prebuild image
uses: devcontainers/ci@v0.3
with:
configFile: .devcontainer/devcontainer-build.json
imageName: ${{ env.DEVCONTAINER_IMAGE }}
imageTag: build-${{ github.run_id }}-${{ matrix.suffix }}
platform: ${{ matrix.platform }}
cacheFrom: ${{ env.DEVCONTAINER_IMAGE }}:buildcache-${{ matrix.suffix }}
push: always

merge:
name: Merge multi-arch manifest
needs: prebuild
runs-on: ubuntu-latest
steps:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Create manifest list and push
run: |
docker buildx imagetools create \
-t ${{ env.DEVCONTAINER_IMAGE }}:latest \
${{ env.DEVCONTAINER_IMAGE }}:build-${{ github.run_id }}-amd64 \
${{ env.DEVCONTAINER_IMAGE }}:build-${{ github.run_id }}-arm64
- name: Inspect manifest
run: docker buildx imagetools inspect ${{ env.DEVCONTAINER_IMAGE }}:latest
48 changes: 48 additions & 0 deletions .github/workflows/devcontainer-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
name: Devcontainer Features Release
on:
workflow_dispatch:

jobs:
publish:
if: ${{ github.ref == 'refs/heads/main' }}
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
packages: write
steps:
- uses: actions/checkout@v4

- name: Publish Features
uses: devcontainers/action@v1
with:
publish-features: "true"
base-path-to-features: "./.devcontainer/features"
generate-docs: "true"
features-namespace: "crunchloop/devcontainer/devcontainers-features"

env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Create PR for Documentation
id: push_image_info
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -e
echo "Start."
# Configure git and Push updates
git config --global user.email github-actions[bot]@users.noreply.github.com
git config --global user.name github-actions[bot]
git config pull.rebase false
branch=automated-documentation-update-$GITHUB_RUN_ID
git checkout -b $branch
message='Automated documentation update'
# Add / update and commit
git add */**/README.md
git commit -m 'Automated documentation update [skip ci]' || export NO_UPDATES=true
# Push
if [ "$NO_UPDATES" != "true" ] ; then
git push origin "$branch"
gh pr create --title "$message" --body "$message"
fi
Loading