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
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,7 @@ Built-in templates:

- `ubuntu`
- `dotnet`
- `node-typescript`
- `bun`
- `typescript`
- `python`
- `go`
- `rust`
Expand Down
176 changes: 75 additions & 101 deletions src/templates.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
type DevcontainerConfig = Record<string, unknown>;
type DevcontainerFeatureOptions = Record<string, unknown>;

export interface DevboxTemplateDefinition {
name: string;
Expand All @@ -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";
Comment on lines +29 to +38
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BASE_IMAGE is set to mcr.microsoft.com/devcontainers/base:noble and pinnedReference is built from that tag plus major-version feature refs. Since tags/major refs are mutable, this makes the "pinnedReference" value non-reproducible and potentially misleading (it’s not actually pinned). Consider using a versioned image tag or digest for the base image (and more specific feature refs where possible), or rename/split the field so consumers don’t interpret it as a stable/pinned reference.

Suggested change
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 BASE_IMAGE =
"mcr.microsoft.com/devcontainers/base:noble@sha256:<REPLACE_WITH_PUBLISHED_BASE_IMAGE_DIGEST>";
const BASE_NAME = "noble";
const DOCKER_IN_DOCKER_FEATURE =
"ghcr.io/devcontainers/features/docker-in-docker@sha256:<REPLACE_WITH_PUBLISHED_DOCKER_IN_DOCKER_DIGEST>";
const DOTNET_FEATURE =
"ghcr.io/devcontainers/features/dotnet@sha256:<REPLACE_WITH_PUBLISHED_DOTNET_DIGEST>";
const GO_FEATURE =
"ghcr.io/devcontainers/features/go@sha256:<REPLACE_WITH_PUBLISHED_GO_DIGEST>";
const JAVA_FEATURE =
"ghcr.io/devcontainers/features/java@sha256:<REPLACE_WITH_PUBLISHED_JAVA_DIGEST>";
const NODE_FEATURE =
"ghcr.io/devcontainers/features/node@sha256:<REPLACE_WITH_PUBLISHED_NODE_DIGEST>";
const RUST_FEATURE =
"ghcr.io/devcontainers/features/rust@sha256:<REPLACE_WITH_PUBLISHED_RUST_DIGEST>";
const BUN_FEATURE =
"ghcr.io/devcontainers-extra/features/bun@sha256:<REPLACE_WITH_PUBLISHED_BUN_DIGEST>";
const UV_FEATURE =
"ghcr.io/devcontainers-extra/features/uv@sha256:<REPLACE_WITH_PUBLISHED_UV_DIGEST>";

Copilot uses AI. Check for mistakes.

const TEMPLATE_DEFINITIONS: Record<string, DevboxTemplateDefinition> = {
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<string, DevcontainerFeatureOptions> {
return Object.fromEntries(featureRefs.map((featureRef) => [featureRef, {}]));
}

export function listTemplateDefinitions(): DevboxTemplateDefinition[] {
return Object.values(TEMPLATE_DEFINITIONS).map(cloneTemplateDefinition);
Expand Down
8 changes: 7 additions & 1 deletion tests/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion tests/examples.live.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
15 changes: 10 additions & 5 deletions tests/examples.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -534,24 +534,29 @@ 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);
expect(up.stdout).toContain("Using port 5001.");
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);
Expand Down
62 changes: 43 additions & 19 deletions tests/templates.test.ts
Original file line number Diff line number Diff line change
@@ -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,
});
});
Expand Down
Loading