|
1 | | -# devcontainer-cli (Go) |
| 1 | +# devcontainer (Go) |
2 | 2 |
|
3 | | -A Go implementation of the [Dev Container CLI](https://containers.dev) — a drop-in |
4 | | -replacement for the reference TypeScript CLI, built to be distributed as a single |
5 | | -**static binary** and to be the primary codebase we evolve and streamline from here. |
| 3 | +**A single static binary that runs your [dev containers](https://containers.dev) — |
| 4 | +a drop-in replacement for the official `@devcontainers/cli`, with none of the |
| 5 | +Node.js baggage and a few superpowers the original doesn't have.** |
| 6 | + |
| 7 | +Point your tooling at this `devcontainer` binary instead of `node devcontainer.js` |
| 8 | +and it behaves the same — `up`, `build`, `exec`, `features`, `templates`, all of it — |
| 9 | +except it starts faster, ships as one file, and fixes the papercuts you hit every day. |
6 | 10 |
|
7 | 11 | --- |
8 | 12 |
|
9 | | -## Why |
10 | | - |
11 | | -The reference [`devcontainers/cli`](https://github.com/devcontainers/cli) is a |
12 | | -Node.js/TypeScript application. Re-implementing it in Go buys us: |
13 | | - |
14 | | -- **A single static binary** — no Node.js runtime, no `node_modules`, no install |
15 | | - step. `CGO_ENABLED=0` gives a fully static executable that runs anywhere |
16 | | - (containers `FROM scratch`, minimal CI images, air-gapped hosts). |
17 | | -- **Fast startup** and low overhead (no interpreter warm-up). |
18 | | -- **Simple distribution** — cross-compiled binaries per OS/arch, or `go install`. |
19 | | -- **A codebase we can evolve** — streamline the internals, add features, and change |
20 | | - behavior deliberately, instead of tracking an upstream we don't control. |
21 | | - |
22 | | -## Supported platform |
23 | | - |
24 | | -> **Linux only, Docker only.** This CLI is supported and validated **exclusively on |
25 | | -> Linux** (amd64 and arm64). Windows and macOS are **not** targets — there is no runtime, |
26 | | -> E2E, or release for them. The only supported container runtime is **Docker**; **Podman |
27 | | -> is not supported**, and only Compose **v2** (`docker compose`) is used. Any cross-platform |
28 | | -> or Podman-detection logic that exists is kept only for parity with the TS oracle and |
29 | | -> carries no support guarantee. |
30 | | -
|
31 | | -## What it is for |
32 | | - |
33 | | -The goal is behavioral compatibility with the TS CLI for the commands people rely on: |
34 | | -`up`, `build`, `exec`, `read-configuration`, `set-up`, `run-user-commands`, |
35 | | -`features` (info/package/publish/test/…), `templates` (apply/publish/…), `outdated`, |
36 | | -`upgrade`. If you point tooling (including the VS Code Dev Containers extension) at |
37 | | -this binary instead of `node devcontainer.js`, it should behave the same. |
38 | | - |
39 | | -## How parity is maintained |
40 | | - |
41 | | -Parity is **not** assumed — it is validated against the real TypeScript CLI, kept in |
42 | | -this repo as a git submodule. |
43 | | - |
44 | | -- **`reference/`** is a submodule pinned to a specific upstream release (currently |
45 | | - **v0.88.0**). It is the behavioral oracle: we compile it and run the exact same |
46 | | - commands through both CLIs. |
47 | | -- **`docs/migration/parity-matrix.yaml`** is a matrix of ~200 cases (flag validation, |
48 | | - `up`/`build`/`exec`/compose/features/templates/publish scenarios). Each case runs |
49 | | - the **same command** against the Go binary and the TS oracle and asserts the outputs |
50 | | - match (exit code, normalized stdout/stderr, and container/registry state via |
51 | | - `verify_cmd`). |
52 | | -- **`internal/cli/parity_matrix_test.go`** (`TestParityMatrix`) drives the matrix. It |
53 | | - has two lanes: |
54 | | - - **contract** — hermetic (flag/output contract, no Docker), fast. |
55 | | - - **runtime** — creates real containers/images (needs Docker); parallel, isolated |
56 | | - per case (unique `--id-label` / `COMPOSE_PROJECT_NAME` / `BUILDX_BUILDER`). |
57 | | -- **`docs/migration/GO-REWRITE-STATUS.md`** is the current parity status: what is |
58 | | - covered and what remains (not a changelog — that lives in `git log`). |
59 | | -- **`docs/migration/REMAINING-WORK.md`** is the single actionable backlog. |
60 | | -- **`docs/migration/RELEASE-CHECKLIST.md`** defines the gates required before the |
61 | | - project may claim complete parity or publish a release. |
62 | | - |
63 | | -Unit, integration and parity tests are separate workflows. `task test:unit` only |
64 | | -targets `cmd/` and `internal/`, so it neither discovers packages under |
65 | | -`reference/node_modules` nor runs the TypeScript oracle, Docker, listeners or the |
66 | | -network. Parity tasks compile the reference explicitly. |
67 | | - |
68 | | -### Maintaining parity as we evolve |
69 | | - |
70 | | -1. When streamlining/refactoring the Go code, run the parity matrix to confirm no |
71 | | - *unintended* behavior change slipped in. |
72 | | -2. When we *intentionally* change behavior, update the matrix case (or remove it) — |
73 | | - the matrix documents deliberate divergences. |
74 | | -3. To track a newer upstream, bump the `reference/` submodule to the new tag and |
75 | | - re-run the matrix; new failures show where upstream moved (features/behavior added |
76 | | - after the last validated version). |
77 | | - |
78 | | -> Note: parity was originally validated end-to-end against **v0.74.0**. The reference |
79 | | -> is now pinned to **v0.88.0**; running the matrix against it may surface gaps |
80 | | -> introduced upstream between 0.74 and 0.88 — that is expected and is the signal for |
81 | | -> what to bring over next. |
82 | | -
|
83 | | -## Layout |
| 13 | +## Why switch? |
| 14 | + |
| 15 | +If you use the official CLI, you already know the friction: |
| 16 | + |
| 17 | +| Daily pain with `@devcontainers/cli` | With this binary | |
| 18 | +| --- | --- | |
| 19 | +| `npm i -g @devcontainers/cli` pulls a Node runtime + hundreds of MB of `node_modules`, and drifts out of sync across machines/CI | **One static binary.** `CGO_ENABLED=0`, no runtime, no `node_modules`. Drop it in `FROM scratch`, a distroless CI image, or an air-gapped host. | |
| 20 | +| Node interpreter warm-up on every invocation | **Fast cold start**, low overhead. | |
| 21 | +| Pulling a **private base image** or `--push`/`--cache-to` to a private registry means running `docker login` by hand — sometimes in several places | **Credential bridge:** the CLI's own auth chain (`DEVCONTAINERS_OCI_AUTH`, cred helpers, `GITHUB_TOKEN`) is handed to `docker build` automatically. | |
| 22 | +| A missing buildx, no cache-export, or a wrong Compose version surfaces as a cryptic error **deep into a build** | **`devcontainer check`** preflights your host and tells you exactly what's wrong (and `setup` fixes what it safely can). | |
| 23 | +| Reusing a prebuilt image means re-implementing config hashing and clone-and-mutate hacks in your orchestrator | **`--cache-key`** emits a deterministic content hash; **`up --cache-image`** boots from a prebuilt image and skips feature install. | |
| 24 | +| A TLS-intercepting proxy breaks image pulls because the CA isn't honored everywhere | Proxy + `NODE_EXTRA_CA_CERTS`/`SSL_CERT_FILE` honored on **every** request path. | |
| 25 | + |
| 26 | +Same behavior where it counts, better ergonomics where it hurts. |
| 27 | + |
| 28 | +## Install |
84 | 29 |
|
| 30 | +Build it yourself — it's one command, and the result is a single self-contained file: |
| 31 | + |
| 32 | +```sh |
| 33 | +git clone https://github.com/spin-stack/devcontainer-cli |
| 34 | +cd devcontainer-cli |
| 35 | +task build # or: go build -o devcontainer ./cmd/devcontainer |
| 36 | +sudo mv devcontainer /usr/local/bin/ |
85 | 37 | ``` |
86 | | -cmd/, internal/ the Go CLI implementation |
87 | | -scripts/ runtime assets used by the CLI (updateUID.Dockerfile) |
88 | | -src/test/configs/ fixtures used by the parity matrix |
89 | | -docs/migration/ parity audit, status log, parity-matrix.yaml |
90 | | -reference/ git submodule → devcontainers/cli @ v0.88.0 (parity oracle) |
91 | | -.github/workflows/ CI: lint/test/build/cross-compile + parity lanes |
| 38 | + |
| 39 | +Tagged releases publish prebuilt **static** binaries (Linux amd64/arm64) and a |
| 40 | +container image to the |
| 41 | +[Releases page](https://github.com/spin-stack/devcontainer-cli/releases) — grab those |
| 42 | +instead once a version is cut. |
| 43 | + |
| 44 | +Confirm it's a single self-contained file: |
| 45 | + |
| 46 | +```sh |
| 47 | +file ./devcontainer # → "statically linked" |
| 48 | +ldd ./devcontainer # → "not a dynamic executable" |
92 | 49 | ``` |
93 | 50 |
|
94 | | -## Build & test |
| 51 | +> **Scope: Linux + Docker.** Supported and validated on **Linux** (amd64, arm64) with |
| 52 | +> **Docker** and Compose **v2**. Windows, macOS and Podman are not targets. |
| 53 | +
|
| 54 | +## Hello, dev container (60 seconds) |
| 55 | + |
| 56 | +Create a project with a minimal dev container config: |
95 | 57 |
|
96 | 58 | ```sh |
97 | | -task build # statically-linked ./devcontainer (CGO_ENABLED=0) |
98 | | -task test:unit # hermetic Go unit tests |
99 | | -task test:integration # local HTTP integration tests |
100 | | -task coverage # reproducible hermetic coverage report |
101 | | -task lint # go vet |
102 | | -task build:cross # static binaries for linux/darwin/windows × amd64/arm64 |
| 59 | +mkdir hello-devcontainer && cd hello-devcontainer |
| 60 | +mkdir -p .devcontainer |
| 61 | +cat > .devcontainer/devcontainer.json <<'JSON' |
| 62 | +{ |
| 63 | + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", |
| 64 | + "features": { |
| 65 | + "ghcr.io/devcontainers/features/node:1": {} |
| 66 | + }, |
| 67 | + "postCreateCommand": "node --version" |
| 68 | +} |
| 69 | +JSON |
| 70 | +``` |
| 71 | + |
| 72 | +Bring it up, then run a command inside it: |
| 73 | + |
| 74 | +```sh |
| 75 | +devcontainer up --workspace-folder . |
| 76 | +devcontainer exec --workspace-folder . bash -lc 'node --version && whoami' |
103 | 77 | ``` |
104 | 78 |
|
105 | | -Verify the binary is static: |
| 79 | +That's it — a real container, your feature installed, your workspace bind-mounted. |
| 80 | +Any automation that shells out to the `devcontainer` CLI (CI pipelines, prebuild |
| 81 | +services, scripts) can call this binary instead of `node devcontainer.js`. |
| 82 | + |
| 83 | +## Superpowers (Go-only) |
| 84 | + |
| 85 | +These commands and flags don't exist in the upstream CLI. Full reference: |
| 86 | +**[docs/go-only-features.md](docs/go-only-features.md)**. |
106 | 87 |
|
107 | 88 | ```sh |
108 | | -file ./devcontainer # → "statically linked" |
109 | | -ldd ./devcontainer # → "not a dynamic executable" |
| 89 | +# Preflight the host — daemon, buildx, cache export, Compose v2, disk — before you build |
| 90 | +devcontainer check |
| 91 | +devcontainer setup # apply the safe fixes (e.g. a cache-capable buildx builder) |
| 92 | + |
| 93 | +# Deterministic cache key for prebuild reuse (hash of config + Dockerfile + features + proxy) |
| 94 | +devcontainer read-configuration --workspace-folder . --cache-key |
| 95 | + |
| 96 | +# Boot from a prebuilt image, skipping build + feature install |
| 97 | +devcontainer up --workspace-folder . --cache-image ghcr.io/acme/app-devcontainer:sha-abc123 |
| 98 | + |
| 99 | +# Pass BuildKit build secrets (RUN --mount=type=secret,id=…) without baking them into a layer |
| 100 | +devcontainer build --workspace-folder . --secrets-file secrets.json |
110 | 101 | ``` |
111 | 102 |
|
112 | | -## Running the parity matrix (optional, needs the submodule + Node) |
| 103 | +Everything else — `up`, `build`, `exec`, `read-configuration`, `set-up`, |
| 104 | +`run-user-commands`, `features` (info/package/publish/test/…), `templates` |
| 105 | +(apply/publish/…), `outdated`, `upgrade` — mirrors the official CLI. |
| 106 | + |
| 107 | +## Is it really compatible? |
| 108 | + |
| 109 | +Compatibility is **validated**, not assumed. A pinned copy of the official |
| 110 | +TypeScript CLI (`reference/`, currently **v0.88.0**) is the behavioral oracle: a |
| 111 | +matrix of ~200 cases runs the *same* command through both CLIs and asserts the |
| 112 | +outputs match (exit code, normalized stdout/stderr, and container/registry state). |
| 113 | +Deliberate divergences (like the Go-only features above) are recorded, not hidden. |
| 114 | + |
| 115 | +If you're evaluating a switch, the parity methodology and current status live in |
| 116 | +[`docs/migration/`](docs/migration/). |
| 117 | + |
| 118 | +## Contributing / building |
| 119 | + |
| 120 | +```sh |
| 121 | +task build # statically-linked ./devcontainer (CGO_ENABLED=0) |
| 122 | +task test:unit # hermetic Go unit tests |
| 123 | +task test:integration # local HTTP integration tests |
| 124 | +task lint # linters |
| 125 | +task build:cross # static binaries for linux/{amd64,arm64} |
| 126 | +``` |
| 127 | + |
| 128 | +Running the parity matrix (needs the submodule + Node + Docker): |
113 | 129 |
|
114 | 130 | ```sh |
115 | 131 | git submodule update --init reference |
116 | | -task reference # install + compile the TypeScript oracle |
117 | | -task parity:contract # hermetic contract lane |
118 | | -task parity:semantic # semantic cases without Docker/network |
119 | | -task parity:network # cases that require external network access |
120 | | -task parity:runtime # complete matrix; requires Docker, ~5–6 min |
| 132 | +task reference # compile the TypeScript oracle |
| 133 | +task parity:contract # hermetic contract lane (no Docker) |
| 134 | +task parity:runtime # full matrix; creates real containers/images via Docker |
121 | 135 | ``` |
122 | 136 |
|
123 | | -## History |
| 137 | +- **[`docs/migration/GO-REWRITE-STATUS.md`](docs/migration/GO-REWRITE-STATUS.md)** — parity status. |
| 138 | +- **[`docs/migration/REMAINING-WORK.md`](docs/migration/REMAINING-WORK.md)** — the actionable backlog. |
| 139 | +- **[`docs/migration/parity-matrix.yaml`](docs/migration/parity-matrix.yaml)** — the case matrix. |
124 | 140 |
|
125 | | -The full commit history of the rewrite (18 blockers, majors, and the parity work) |
126 | | -lives in the [`aledbf/cli`](https://github.com/aledbf/cli) `go-rewrite` branch. This |
127 | | -repo starts from that validated state with the Go code as the primary tree. |
| 141 | +``` |
| 142 | +cmd/, internal/ the Go CLI implementation |
| 143 | +docs/ user + migration docs |
| 144 | +src/test/configs/ fixtures used by the parity matrix |
| 145 | +reference/ git submodule → the official CLI @ v0.88.0 (parity oracle) |
| 146 | +``` |
0 commit comments