Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e60ab10
chore(terraform): ignore .terraform/ cache and pin tflint ruleset
neekolas Apr 21, 2026
99b7b21
chore(tooling): install terraform + tflint in devcontainer and config…
neekolas Apr 21, 2026
8c3dde9
ci(terraform): add lint PR gate and task-beta template deploy workflows
neekolas Apr 21, 2026
b304bac
docs: document task-beta Coder template and its CI workflows
neekolas Apr 21, 2026
5f73eff
ci(terraform): add terraform test harness with mocked providers
neekolas Apr 21, 2026
88bccb4
test(terraform): add failing parsing and precondition tests
neekolas Apr 21, 2026
d9c460a
refactor(terraform): decode TaskMetadata JSON from prompt with fail-h…
neekolas Apr 21, 2026
8dae2b8
fix(terraform): tighten json_valid, null-safe base_branch, and backfi…
neekolas Apr 21, 2026
906e750
test(terraform): cover non-object JSON for EARS-1 regression
neekolas Apr 21, 2026
9f9a588
test(terraform): add failing size-profile and dind-constant tests
neekolas Apr 21, 2026
2851766
feat(terraform): add small/medium/large size profiles (default large)
neekolas Apr 21, 2026
91e93bf
test(terraform): verify dind constancy at each size and cover ephemer…
neekolas Apr 21, 2026
18f0840
test(terraform): add failing docker-sidecar-gating tests
neekolas Apr 21, 2026
479938b
feat(terraform): gate dind sidecar and DOCKER_HOST env on docker_enabled
neekolas Apr 21, 2026
4980398
Revert "feat(terraform): gate dind sidecar and DOCKER_HOST env on doc…
neekolas Apr 21, 2026
cd6f332
feat(terraform): gate dind sidecar on docker_enabled via module outputs
neekolas Apr 21, 2026
9fab391
fix(terraform): redact sensitive env values from pod_containers output
neekolas Apr 21, 2026
6b221eb
test(terraform): add failing volume-mapping tests
neekolas Apr 21, 2026
13b18df
feat(terraform): map extra_volumes and gate docker-cache on local.docker
neekolas Apr 21, 2026
2021161
test(terraform): cover multi-entry and docker+extras combined volume …
neekolas Apr 21, 2026
365648f
test(terraform): assert docker-cache size==10Gi
neekolas Apr 21, 2026
15202f9
Add initial TF config
neekolas Apr 21, 2026
8502827
Update template inputs
neekolas Apr 21, 2026
5ac5e33
Bump package-lock.json
neekolas Apr 21, 2026
045a242
Fix formatting
neekolas Apr 21, 2026
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
6 changes: 3 additions & 3 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
"name": "task-action",
"image": "mcr.microsoft.com/devcontainers/typescript-node:24",
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
"ghcr.io/devcontainers/features/github-cli:1": {},
},
"postCreateCommand": "npm install",
"customizations": {
"vscode": {
"extensions": [
"biomejs.biome"
"biomejs.biome",
],
"settings": {
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.tabSize": 2,
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
}
},
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion .zed/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@
},
"JSONC": {
"formatter": { "language_server": { "name": "biome" } }
}
},
}
}
4 changes: 4 additions & 0 deletions src/config/app-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ const AppConfigSchema = z.object({
coderTaskNamePrefix: z.string().min(1).default("gh"),
coderTemplateName: z.string().min(1).default("task-template"),
coderTemplateNameCodex: z.string().min(1).default("task-template-codex"),
// Hardcoded template name used when a repo-level config is present. Not
// env-overridable: the field is intentionally omitted from `loadConfig`'s
// env→raw mapping so it always falls through to this default.
codeFactoryTemplate: z.string().min(1).default("code-factory"),
coderTemplatePreset: z.string().min(1).optional(),
coderOrganization: z.string().min(1).default("default"),
logFormat: z.string().optional(),
Expand Down
61 changes: 54 additions & 7 deletions src/config/repo-config-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ path = "/data"
size = "20gb"

[harness]
provider = "claude"
provider = "claude_code"

[[scheduled_jobs]]
name = "nightly"
Expand All @@ -35,9 +35,9 @@ prompt = "Do the thing"
expect(parsed.sandbox?.docker).toBe(true);
expect(parsed.sandbox?.volumes?.[0]).toEqual({
path: "/data",
size: "20gb",
size: "20Gi",
});
expect(parsed.harness?.provider).toBe("claude");
expect(parsed.harness?.provider).toBe("claude_code");
expect(parsed.scheduled_jobs?.[0]?.name).toBe("nightly");
});
test("unknown keys are dropped (write-side loose-parse)", () => {
Expand Down Expand Up @@ -94,21 +94,21 @@ describe("resolveRepoConfigSettings — defaults applied on read", () => {
expect(r.sandbox.size).toBe("medium");
expect(r.sandbox.docker).toBe(false);
expect(r.sandbox.volumes).toEqual([]);
expect(r.harness.provider).toBe("claude");
expect(r.harness.provider).toBe("claude_code");
expect(r.scheduled_jobs).toEqual([]);
});
test("empty object → full defaults", () => {
expect(resolveRepoConfigSettings({})).toEqual({
sandbox: { size: "medium", docker: false, volumes: [] },
harness: { provider: "claude" },
harness: { provider: "claude_code" },
scheduled_jobs: [],
});
});
test("volume with path-only → size defaulted to '10gb'", () => {
test("volume with path-only → size defaulted to '10Gi'", () => {
const r = resolveRepoConfigSettings({
sandbox: { volumes: [{ path: "/data" }] },
});
expect(r.sandbox.volumes[0]).toEqual({ path: "/data", size: "10gb" });
expect(r.sandbox.volumes[0]).toEqual({ path: "/data", size: "10Gi" });
});
test("partial override: explicit size beats default", () => {
const r = resolveRepoConfigSettings({
Expand All @@ -118,3 +118,50 @@ describe("resolveRepoConfigSettings — defaults applied on read", () => {
expect(r.sandbox.docker).toBe(false);
});
});

describe("volume size normalization → canonical Kubernetes binary-SI form", () => {
test.each([
["10gb", "10Gi"],
["10GB", "10Gi"],
["10Gb", "10Gi"],
["10G", "10Gi"],
["10g", "10Gi"],
["10gi", "10Gi"],
["10Gi", "10Gi"],
["500mb", "500Mi"],
["500M", "500Mi"],
["500Mi", "500Mi"],
["2tb", "2Ti"],
["64k", "64Ki"],
[" 20 GB ", "20Gi"],
])("parseRepoConfigToml normalizes %s → %s on write", (input, expected) => {
const parsed = parseRepoConfigToml(
`[[sandbox.volumes]]\npath = "/data"\nsize = "${input}"`,
);
expect(parsed.sandbox?.volumes?.[0]?.size).toBe(expected);
});

test("resolveRepoConfigSettings normalizes legacy stored values on read", () => {
// Simulate a stored record written before the normalization transform
// existed — the resolved schema must re-normalize on read.
const r = resolveRepoConfigSettings({
sandbox: { volumes: [{ path: "/data", size: "20gb" }] },
});
expect(r.sandbox.volumes[0]).toEqual({ path: "/data", size: "20Gi" });
});

test.each([
"10",
"gb",
"10bb",
"10.5gb",
"10eb",
"abc",
])("invalid volume size %s → parse rejects", (input) => {
expect(() =>
parseRepoConfigToml(
`[[sandbox.volumes]]\npath = "/data"\nsize = "${input}"`,
),
).toThrow(/Invalid RepoConfig/);
});
});
40 changes: 34 additions & 6 deletions src/config/repo-config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,35 @@ import { z } from "zod";
export const SandboxSizeSchema = z.enum(["small", "medium", "large"]);

/** Allowed values for `harness.provider`. */
export const HarnessProviderSchema = z.enum(["claude", "codex"]);
export const HarnessProviderSchema = z.enum(["claude_code", "codex"]);

// ── Volume size normalization ────────────────────────────────────────────────
// Kubernetes PVCs require binary-SI suffixes like `10Gi`. Users routinely
// write `10gb` / `10GB` / `10G` / `10gi`; we accept those shapes and normalize
// everything to `<digits><Prefix>i` before the value leaves the write path.

const VOLUME_SIZE_REGEX = /^\s*(\d+)\s*(k|kb|ki|m|mb|mi|g|gb|gi|t|tb|ti)\s*$/i;

function normalizeVolumeSize(input: string): string {
const match = VOLUME_SIZE_REGEX.exec(input);
if (!match) return input; // unreachable: regex validated before transform
const digits = match[1] ?? "";
const prefix = (match[2] ?? "").charAt(0).toUpperCase();
return `${digits}${prefix}i`;
}

/**
* A Kubernetes-compatible volume size. Accepts common variants (`10gb`,
* `10GB`, `10G`, `10gi`, `10Gi`, etc.) and always emits the canonical
* binary-SI form (`10Gi`). Supports `K/M/G/T` prefixes.
*/
export const VolumeSizeSchema = z
.string()
.regex(
VOLUME_SIZE_REGEX,
'expected a size like "10Gi" (K/M/G/T with optional b/i suffix)',
)
.transform(normalizeVolumeSize);

// ── Sparse (stored) schemas ──────────────────────────────────────────────────
// Sparse schemas mirror what users actually wrote in TOML. No `.default()`:
Expand All @@ -18,7 +46,7 @@ export const HarnessProviderSchema = z.enum(["claude", "codex"]);
/** Sparse shape for a single sandbox volume entry. `path` is required. */
export const StoredSandboxVolumeSchema = z.object({
path: z.string(),
size: z.string().optional(),
size: VolumeSizeSchema.optional(),
});

/** Sparse shape for the `[sandbox]` section. */
Expand Down Expand Up @@ -55,10 +83,10 @@ export const StoredRepoConfigSettingsSchema = z.object({
// Resolved schemas apply defaults on read so every consumer sees a fully
// populated object without worrying about whether a field was written.

/** Resolved volume: `size` defaults to `"10gb"` when absent. */
/** Resolved volume: `size` defaults to `"10Gi"` when absent. */
export const ResolvedSandboxVolumeSchema = z.object({
path: z.string(),
size: z.string().default("10gb"),
size: VolumeSizeSchema.default("10Gi"),
});

/** Resolved sandbox: size/docker/volumes all have defaults. */
Expand All @@ -68,9 +96,9 @@ export const ResolvedSandboxSchema = z.object({
volumes: z.array(ResolvedSandboxVolumeSchema).default([]),
});

/** Resolved harness: provider defaults to `"claude"`. */
/** Resolved harness: provider defaults to `"claude_code"`. */
export const ResolvedHarnessSchema = z.object({
provider: HarnessProviderSchema.default("claude"),
provider: HarnessProviderSchema.default("claude_code"),
});

/**
Expand Down
4 changes: 2 additions & 2 deletions src/durable-objects/repo-config-do.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe("RepoConfigDO — get/set round-trip", () => {
expect(read?.settings.sandbox.size).toBe("medium");
expect(read?.settings.sandbox.docker).toBe(false);
expect(read?.settings.sandbox.volumes).toEqual([]);
expect(read?.settings.harness.provider).toBe("claude");
expect(read?.settings.harness.provider).toBe("claude_code");
expect(read?.settings.scheduled_jobs).toEqual([]);
});

Expand Down Expand Up @@ -86,7 +86,7 @@ describe("RepoConfigDO — get/set round-trip", () => {
});
const read = await stub.getRepoConfig();
expect(read?.settings.sandbox.volumes).toEqual([
{ path: "/data", size: "10gb" },
{ path: "/data", size: "10Gi" },
]);
});

Expand Down
2 changes: 1 addition & 1 deletion src/events/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export type TaskRequestedEvent = {
type: "task_requested";
source: EventSource;
repository: { owner: string; name: string };
issue: { number: number; url: string };
issue: { id: number; number: number; url: string };
requester: { login: string; externalId: number };
};

Expand Down
6 changes: 4 additions & 2 deletions src/services/coder/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,10 @@ export class CoderService implements TaskRunner {
taskName: TaskName;
owner: string;
input: string;
templateName?: string;
}): Promise<Task> {
const { taskName, owner, input } = params;
const { taskName, owner, input, templateName } = params;
const resolvedTemplateName = templateName ?? this.config.templateName;

// 1. Check for an existing task
const existing = await this.findTask(taskName, owner);
Expand All @@ -373,7 +375,7 @@ export class CoderService implements TaskRunner {
}

// 2. Resolve template
const templateEndpoint = `/api/v2/organizations/${encodeURIComponent(this.config.organization)}/templates/${encodeURIComponent(this.config.templateName)}`;
const templateEndpoint = `/api/v2/organizations/${encodeURIComponent(this.config.organization)}/templates/${encodeURIComponent(resolvedTemplateName)}`;
const rawTemplate = await this.request<unknown>(templateEndpoint);
const template = CoderSDKTemplateSchema.parse(rawTemplate);
const templateVersionId = template.active_version_id;
Expand Down
2 changes: 2 additions & 0 deletions src/services/task-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ export interface TaskRunner {

/**
* Create a task. Returns the existing one if `(taskName, owner)` collides.
* `templateName` overrides the runner-configured default when provided.
*/
create(params: {
taskName: TaskName;
owner: string;
input: string;
templateName?: string;
}): Promise<Task>;

/**
Expand Down
1 change: 1 addition & 0 deletions src/webhooks/github/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ describe("WebhookRouter", () => {
expect(event.repository.owner).toBe("xmtplabs");
expect(event.repository.name).toBe("coder-action");
expect(event.issue.number).toBe(65);
expect(event.issue.id).toBe(4132709157);
expect(event.issue.url).toBe(
"https://github.com/xmtplabs/coder-action/issues/65",
);
Expand Down
1 change: 1 addition & 0 deletions src/webhooks/github/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ export class WebhookRouter {
name: payload.repository.name,
},
issue: {
id: payload.issue.id,
number: payload.issue.number,
url: payload.issue.html_url,
},
Expand Down
6 changes: 3 additions & 3 deletions src/workflows/instance-id.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe("buildInstanceId", () => {
type: "task_requested",
source: { type: "github", installationId: 1 },
repository: { owner: "acme", name: "repo" },
issue: { number: 42, url: "u" },
issue: { id: 1, number: 42, url: "u" },
requester: { login: "u", externalId: 1 },
};
const id = buildInstanceId(event, "abc-123");
Expand Down Expand Up @@ -88,7 +88,7 @@ describe("buildInstanceId", () => {
type: "task_requested",
source: { type: "github", installationId: 1 },
repository: { owner: "ACME", name: "Repo.With.Dots" },
issue: { number: 1, url: "u" },
issue: { id: 1, number: 1, url: "u" },
requester: { login: "u", externalId: 1 },
};
const id = buildInstanceId(event, "d/e/l");
Expand All @@ -100,7 +100,7 @@ describe("buildInstanceId", () => {
type: "task_requested",
source: { type: "github", installationId: 1 },
repository: { owner: "o", name: "a".repeat(100) },
issue: { number: 1, url: "u" },
issue: { id: 1, number: 1, url: "u" },
requester: { login: "u", externalId: 1 },
};
const id = buildInstanceId(event, "d");
Expand Down
Loading
Loading