From c5cb8ce8f5ef2eb11da9ef9f292d8524badb000b Mon Sep 17 00:00:00 2001 From: Pablo Zaidenvoren Date: Sat, 25 Apr 2026 10:55:58 -0400 Subject: [PATCH] refactor: unify devcontainer templates under noble base image and update features Co-authored-by: Copilot --- README.md | 3 +- src/templates.ts | 176 +++++++++++++++--------------------- tests/core.test.ts | 8 +- tests/examples.live.test.ts | 2 +- tests/examples.test.ts | 15 ++- tests/templates.test.ts | 62 +++++++++---- 6 files changed, 137 insertions(+), 129 deletions(-) diff --git a/README.md b/README.md index 6232fc9..7e71f61 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,7 @@ Built-in templates: - `ubuntu` - `dotnet` -- `node-typescript` -- `bun` +- `typescript` - `python` - `go` - `rust` diff --git a/src/templates.ts b/src/templates.ts index 1bc66ec..8d2ed9b 100644 --- a/src/templates.ts +++ b/src/templates.ts @@ -1,4 +1,5 @@ type DevcontainerConfig = Record; +type DevcontainerFeatureOptions = Record; export interface DevboxTemplateDefinition { name: string; @@ -25,124 +26,97 @@ export interface DevboxTemplateSummary { runnerCompatible: boolean; } -const BUN_VERSION = "1.3.13"; -const BASE_IMAGE = "mcr.microsoft.com/devcontainers/base:2.1.8-ubuntu24.04"; -const BUN_IMAGE = `oven/bun:${BUN_VERSION}`; +const BASE_IMAGE = "mcr.microsoft.com/devcontainers/base:noble"; +const BASE_NAME = "noble"; +const DOCKER_IN_DOCKER_FEATURE = "ghcr.io/devcontainers/features/docker-in-docker:2"; +const DOTNET_FEATURE = "ghcr.io/devcontainers/features/dotnet:2"; +const GO_FEATURE = "ghcr.io/devcontainers/features/go:1"; +const JAVA_FEATURE = "ghcr.io/devcontainers/features/java:1"; +const NODE_FEATURE = "ghcr.io/devcontainers/features/node:1"; +const RUST_FEATURE = "ghcr.io/devcontainers/features/rust:1"; +const BUN_FEATURE = "ghcr.io/devcontainers-extra/features/bun:1"; +const UV_FEATURE = "ghcr.io/devcontainers-extra/features/uv:1"; const TEMPLATE_DEFINITIONS: Record = { - ubuntu: { + ubuntu: createTemplateDefinition({ name: "ubuntu", - description: "Ubuntu 24.04 base image with common devcontainer tooling.", - source: "built-in", - base: "ubuntu24.04", - image: BASE_IMAGE, - pinnedReference: BASE_IMAGE, - runtimeVersion: "Ubuntu 24.04", + description: "Ubuntu noble base image with Docker-in-Docker preinstalled.", + runtimeVersion: "Ubuntu noble", languages: [], - runnerCompatible: true, - config: { - image: BASE_IMAGE, - }, - }, - dotnet: { + }), + dotnet: createTemplateDefinition({ name: "dotnet", - description: ".NET 10 SDK on Ubuntu 24.04.", - source: "built-in", - base: "ubuntu24.04", - image: "mcr.microsoft.com/devcontainers/dotnet:2.0.7-10.0-noble", - pinnedReference: "mcr.microsoft.com/devcontainers/dotnet:2.0.7-10.0-noble", - runtimeVersion: ".NET 10.0", + description: ".NET SDK on Ubuntu noble via the official devcontainer feature.", + runtimeVersion: ".NET SDK", languages: ["dotnet", "csharp", "fsharp"], - runnerCompatible: true, - config: { - image: "mcr.microsoft.com/devcontainers/dotnet:2.0.7-10.0-noble", - }, - }, - "node-typescript": { - name: "node-typescript", - description: "Node.js 24 with TypeScript tooling on Debian bookworm.", - source: "built-in", - base: "bookworm", - image: "mcr.microsoft.com/devcontainers/typescript-node:4.0.8-24-bookworm", - pinnedReference: "mcr.microsoft.com/devcontainers/typescript-node:4.0.8-24-bookworm", - runtimeVersion: "Node.js 24", - languages: ["node", "typescript", "javascript"], - runnerCompatible: true, - config: { - image: "mcr.microsoft.com/devcontainers/typescript-node:4.0.8-24-bookworm", - }, - }, - bun: { - name: "bun", - description: `Official Bun ${BUN_VERSION} image on Debian trixie.`, - source: "built-in", - base: "trixie", - image: BUN_IMAGE, - pinnedReference: BUN_IMAGE, - runtimeVersion: `Bun ${BUN_VERSION}`, - languages: ["bun", "javascript", "typescript"], - runnerCompatible: true, - config: { - image: BUN_IMAGE, - }, - }, - python: { + features: [DOTNET_FEATURE], + }), + typescript: createTemplateDefinition({ + name: "typescript", + description: "Node.js and Bun on Ubuntu noble via devcontainer features.", + runtimeVersion: "Node.js + Bun", + languages: ["node", "bun", "typescript", "javascript"], + features: [NODE_FEATURE, BUN_FEATURE], + }), + python: createTemplateDefinition({ name: "python", - description: "Python 3.14 on Debian bookworm.", - source: "built-in", - base: "bookworm", - image: "mcr.microsoft.com/devcontainers/python:3.0.7-3.14-bookworm", - pinnedReference: "mcr.microsoft.com/devcontainers/python:3.0.7-3.14-bookworm", - runtimeVersion: "Python 3.14", + description: "Python workflows on Ubuntu noble via the uv feature.", + runtimeVersion: "Python via uv", languages: ["python"], - runnerCompatible: true, - config: { - image: "mcr.microsoft.com/devcontainers/python:3.0.7-3.14-bookworm", - }, - }, - go: { + features: [UV_FEATURE], + }), + go: createTemplateDefinition({ name: "go", - description: "Go 1.26 on Debian bookworm.", - source: "built-in", - base: "bookworm", - image: "mcr.microsoft.com/devcontainers/go:2.1.2-1.26-bookworm", - pinnedReference: "mcr.microsoft.com/devcontainers/go:2.1.2-1.26-bookworm", - runtimeVersion: "Go 1.26", + description: "Go on Ubuntu noble via the official devcontainer feature.", + runtimeVersion: "Go", languages: ["go"], - runnerCompatible: true, - config: { - image: "mcr.microsoft.com/devcontainers/go:2.1.2-1.26-bookworm", - }, - }, - rust: { + features: [GO_FEATURE], + }), + rust: createTemplateDefinition({ name: "rust", - description: "Rust stable toolchain image on Debian bookworm.", - source: "built-in", - base: "bookworm", - image: "mcr.microsoft.com/devcontainers/rust:2.0.10-1-bookworm", - pinnedReference: "mcr.microsoft.com/devcontainers/rust:2.0.10-1-bookworm", - runtimeVersion: "Rust stable (image release 2.0.10)", + description: "Rust on Ubuntu noble via the official devcontainer feature.", + runtimeVersion: "Rust", languages: ["rust"], - runnerCompatible: true, - config: { - image: "mcr.microsoft.com/devcontainers/rust:2.0.10-1-bookworm", - }, - }, - java: { + features: [RUST_FEATURE], + }), + java: createTemplateDefinition({ name: "java", - description: "Java 25 LTS on Debian bookworm.", - source: "built-in", - base: "bookworm", - image: "mcr.microsoft.com/devcontainers/java:3.0.7-25-bookworm", - pinnedReference: "mcr.microsoft.com/devcontainers/java:3.0.7-25-bookworm", - runtimeVersion: "Java 25 LTS", + description: "Java on Ubuntu noble via the official devcontainer feature.", + runtimeVersion: "Java", languages: ["java"], + features: [JAVA_FEATURE], + }), +}; + +function createTemplateDefinition(input: { + name: string; + description: string; + runtimeVersion: string; + languages: string[]; + features?: string[]; +}): DevboxTemplateDefinition { + const featureRefs = [DOCKER_IN_DOCKER_FEATURE, ...(input.features ?? [])]; + + return { + name: input.name, + description: input.description, + source: "built-in", + base: BASE_NAME, + image: BASE_IMAGE, + pinnedReference: [BASE_IMAGE, ...featureRefs].join(" + "), + runtimeVersion: input.runtimeVersion, + languages: [...input.languages], runnerCompatible: true, config: { - image: "mcr.microsoft.com/devcontainers/java:3.0.7-25-bookworm", + image: BASE_IMAGE, + features: buildFeatureMap(featureRefs), }, - }, -}; + }; +} + +function buildFeatureMap(featureRefs: string[]): Record { + return Object.fromEntries(featureRefs.map((featureRef) => [featureRef, {}])); +} export function listTemplateDefinitions(): DevboxTemplateDefinition[] { return Object.values(TEMPLATE_DEFINITIONS).map(cloneTemplateDefinition); diff --git a/tests/core.test.ts b/tests/core.test.ts index 1bfbeaa..d2dad97 100644 --- a/tests/core.test.ts +++ b/tests/core.test.ts @@ -446,7 +446,13 @@ describe("resolveWorkspaceConfig", () => { expect(rebuildStyleResolution.sourceConfigPath).toBeNull(); expect(rebuildStyleResolution.generatedConfigPath).toBe(state.generatedConfigPath); expect(rebuildStyleResolution.template?.name).toBe("python"); - expect(rebuildStyleResolution.config.image).toBe("mcr.microsoft.com/devcontainers/python:3.0.7-3.14-bookworm"); + expect(rebuildStyleResolution.config).toEqual({ + image: "mcr.microsoft.com/devcontainers/base:noble", + features: { + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers-extra/features/uv:1": {}, + }, + }); }); test("normalizes legacy template generated config paths from saved state", async () => { diff --git a/tests/examples.live.test.ts b/tests/examples.live.test.ts index 46d8705..1a48079 100644 --- a/tests/examples.live.test.ts +++ b/tests/examples.live.test.ts @@ -345,7 +345,7 @@ describe("example workspaces (real devcontainers)", () => { expect(arise.stdout).toContain("Scanning for stopped managed devbox containers..."); expect(arise.stdout).toContain(`Recovered ${fixture.workspacePath}`); expect(arise.stdout).toContain(`Running \`devbox up\` again for ${fixture.workspacePath}...`); - expect(arise.stdout).toContain("Arise summary: restarted 1"); + expect(arise.stdout).toMatch(/Arise summary: restarted \d+, skipped \d+ workspace\(s\), ignored \d+ container\(s\), failed 0\./); const restartedState = await readJson(fixture.statePath); const restartedContainerId = String(restartedState.lastContainerId); diff --git a/tests/examples.test.ts b/tests/examples.test.ts index 18e4dbd..75b56b4 100644 --- a/tests/examples.test.ts +++ b/tests/examples.test.ts @@ -534,7 +534,7 @@ describe("example workspaces (simulated host tools)", () => { expect(templates.exitCode).toBe(0); const templateList = JSON.parse(templates.stdout); expect(templateList.some((entry: { name: string }) => entry.name === "ubuntu")).toBe(true); - expect(templateList.some((entry: { name: string }) => entry.name === "bun")).toBe(true); + expect(templateList.some((entry: { name: string }) => entry.name === "typescript")).toBe(true); const up = runCli(fixture, ["up", "--template", "ubuntu", "--allow-missing-ssh"]); expect(up.exitCode).toBe(0); @@ -542,16 +542,21 @@ describe("example workspaces (simulated host tools)", () => { expect(existsSync(fixture.generatedConfigPath)).toBe(true); const generatedConfig = await readJson(fixture.generatedConfigPath); - expect(generatedConfig.image).toBe("mcr.microsoft.com/devcontainers/base:2.1.8-ubuntu24.04"); + expect(generatedConfig.image).toBe("mcr.microsoft.com/devcontainers/base:noble"); + expect(generatedConfig.features).toEqual({ + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + }); expect(generatedConfig.postCreateCommand).toBeUndefined(); const state = await readJson(fixture.statePath); expect(state.configSource).toBe("template"); expect(state.sourceConfigPath).toBeNull(); expect(state.template.name).toBe("ubuntu"); - expect(state.template.image).toBe("mcr.microsoft.com/devcontainers/base:2.1.8-ubuntu24.04"); - expect(state.template.pinnedReference).toBe("mcr.microsoft.com/devcontainers/base:2.1.8-ubuntu24.04"); - expect(state.template.runtimeVersion).toBe("Ubuntu 24.04"); + expect(state.template.image).toBe("mcr.microsoft.com/devcontainers/base:noble"); + expect(state.template.pinnedReference).toBe( + "mcr.microsoft.com/devcontainers/base:noble + ghcr.io/devcontainers/features/docker-in-docker:2", + ); + expect(state.template.runtimeVersion).toBe("Ubuntu noble"); const statusWhileRunning = runCli(fixture, ["status"]); expect(statusWhileRunning.exitCode).toBe(0); diff --git a/tests/templates.test.ts b/tests/templates.test.ts index 7969bcc..5180f0b 100644 --- a/tests/templates.test.ts +++ b/tests/templates.test.ts @@ -1,35 +1,59 @@ import { describe, expect, test } from "bun:test"; -import { getTemplateDefinition, listTemplateSummaries } from "../src/templates"; +import { getTemplateDefinition, listTemplateDefinitions, listTemplateSummaries } from "../src/templates"; describe("built-in templates", () => { - test("uses the official Bun image instead of an installer script", () => { - const template = getTemplateDefinition("bun"); + test("standardizes every template on the noble base image with docker-in-docker", () => { + for (const template of listTemplateDefinitions()) { + expect(template.base).toBe("noble"); + expect(template.image).toBe("mcr.microsoft.com/devcontainers/base:noble"); + expect(template.pinnedReference).toContain("mcr.microsoft.com/devcontainers/base:noble"); + expect(template.config).toEqual( + expect.objectContaining({ + image: "mcr.microsoft.com/devcontainers/base:noble", + features: expect.objectContaining({ + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + }), + }), + ); + } + }); + + test("builds the typescript template from node and bun features", () => { + const template = getTemplateDefinition("typescript"); expect(template).not.toBeNull(); if (!template) { - throw new Error("Expected the built-in bun template to exist."); + throw new Error("Expected the built-in typescript template to exist."); } - expect(template.description).toBe("Official Bun 1.3.13 image on Debian trixie."); - expect(template.base).toBe("trixie"); - expect(template.image).toBe("oven/bun:1.3.13"); - expect(template.pinnedReference).toBe("oven/bun:1.3.13"); + expect(template.description).toBe("Node.js and Bun on Ubuntu noble via devcontainer features."); + expect(template.base).toBe("noble"); + expect(template.image).toBe("mcr.microsoft.com/devcontainers/base:noble"); + expect(template.pinnedReference).toBe( + "mcr.microsoft.com/devcontainers/base:noble + ghcr.io/devcontainers/features/docker-in-docker:2 + ghcr.io/devcontainers/features/node:1 + ghcr.io/devcontainers-extra/features/bun:1", + ); expect(template.runnerCompatible).toBe(true); expect(template.config).toEqual({ - image: "oven/bun:1.3.13", + image: "mcr.microsoft.com/devcontainers/base:noble", + features: { + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/node:1": {}, + "ghcr.io/devcontainers-extra/features/bun:1": {}, + }, }); }); - test("exposes the pinned Bun image in template summaries", () => { - const bunTemplate = listTemplateSummaries().find((template) => template.name === "bun"); - expect(bunTemplate).toEqual({ - name: "bun", - description: "Official Bun 1.3.13 image on Debian trixie.", + test("exposes the unified typescript template in summaries", () => { + const typescriptTemplate = listTemplateSummaries().find((template) => template.name === "typescript"); + expect(typescriptTemplate).toEqual({ + name: "typescript", + description: "Node.js and Bun on Ubuntu noble via devcontainer features.", source: "built-in", - base: "trixie", - image: "oven/bun:1.3.13", - pinnedReference: "oven/bun:1.3.13", - runtimeVersion: "Bun 1.3.13", - languages: ["bun", "javascript", "typescript"], + base: "noble", + image: "mcr.microsoft.com/devcontainers/base:noble", + pinnedReference: + "mcr.microsoft.com/devcontainers/base:noble + ghcr.io/devcontainers/features/docker-in-docker:2 + ghcr.io/devcontainers/features/node:1 + ghcr.io/devcontainers-extra/features/bun:1", + runtimeVersion: "Node.js + Bun", + languages: ["node", "bun", "typescript", "javascript"], runnerCompatible: true, }); });