From 7d28842bb290b57c66aa7c97a1a3cd263cb062d7 Mon Sep 17 00:00:00 2001 From: Pandelis Zembashis Date: Fri, 23 Oct 2026 17:43:00 -0700 Subject: [PATCH 1/3] Require AWS EBS Gen2 builders for action v2 --- README.md | 14 ++++++++------ action.yml | 4 ++-- builder.mjs | 18 +++++++++++++++++- builder.test.mjs | 13 +++++++++++++ 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 9520a30..ceb54d9 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ Connect a Linux GitHub Actions runner to an existing Composal organization builder. Docker layers and `RUN --mount=type=cache` data stay on its persistent remote disk. Images push from the builder to your registry; deployment hosts pull the published digest. -Use `ComposalAI/docker-builder@1` (also available as `@v1`), or pin a release commit for reproducibility. Remote builders are in preview. +Version 2 requires an AWS/EBS Gen2 builder and checks the backend before waking it. It refuses Classic builders without fallback; existing `@1` / `@v1` releases remain available for Classic. + +Use `ComposalAI/docker-builder@2` (also available as `@v2`), or pin a release commit for reproducibility. Remote builders are in preview. ```yaml concurrency: @@ -16,12 +18,12 @@ steps: registry: registry.example.com username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} - - uses: ComposalAI/docker-builder@1 + - uses: ComposalAI/docker-builder@2 id: remote with: token: ${{ secrets.COMPOSAL_API_TOKEN }} org: acme - builder: ci + builder: ci-gen2 - uses: docker/build-push-action@v6 with: context: . @@ -32,9 +34,9 @@ steps: Authenticate to your registry before selecting the remote context: the gateway does not expose the daemon registry-login endpoint. For the Composal registry, `com registry login --write-config` can instead write client credentials after setup without using that endpoint. BuildKit receives registry authentication through the normal Docker client session. -An organization admin must provision the builder first with `com docker-builder create ci --org acme`. The token needs membership in that organization. Setup fails if the builder is missing, unavailable, or refused; it never falls back to a local build or creates a cache accidentally. +An organization admin must request Gen2 builder activation first. Composal provisions a separate `ci-gen2` builder through the organization API with `backend: gen2`; an existing Classic builder cannot be converted in place. The token needs membership in that organization. Setup fails if the builder is missing, unavailable, or refused; it never falls back to a local build or creates a cache accidentally. -Inputs are `token` and `org` (required), `builder` (default `main`), `wake-timeout` (default `900` seconds, range `30`–`3600`), `use` (default `true`), `api-url` (default `https://composal.ai`), and `install-cli` (default `true`). The installed CLI is added to the job PATH. Set `install-cli: 'false'` to use an already installed released `com` supporting background Docker builder setup. Docker and Buildx must be installed on the runner. Outputs are `builder`, `context`, and `driver` (`docker`). Each job isolates CLI supervisor state in its temporary directory. Setup logs the CLI version, checks gateway health and waits for the shared builder to become ready before opening native Docker connections. Cold starts can take several minutes; `wake-timeout` controls this wait. Progress includes the builder state and generation. Setup requests a retry when it finds a builder already starting, so a failed earlier wake can resume. Canceling a job disconnects only that job; it does not cancel the shared wake. It retries transient status-read server errors, then checks the remote daemon with bounded, retryable probes and verifies Buildx readiness before selecting the context and builder for subsequent job steps using `DOCKER_CONTEXT` and `BUILDX_BUILDER`. It leaves Docker's saved defaults unchanged and restores the previous job environment during cleanup. +Inputs are `token` and `org` (required), `builder` (default `main-gen2`), `wake-timeout` (default `900` seconds, range `30`–`3600`), `use` (default `true`), `api-url` (default `https://composal.ai`), and `install-cli` (default `true`). The installed CLI is added to the job PATH. Set `install-cli: 'false'` to use an already installed released `com` supporting background Docker builder setup. Docker and Buildx must be installed on the runner. Outputs are `builder`, `context`, and `driver` (`docker`). Each job isolates CLI supervisor state in its temporary directory. Setup logs the CLI version, checks gateway health and waits for the shared builder to become ready before opening native Docker connections. Cold starts can take several minutes; `wake-timeout` controls this wait. Progress includes the builder state and generation. Setup requests a retry when it finds a builder already starting, so a failed earlier wake can resume. Canceling a job disconnects only that job; it does not cancel the shared wake. It retries transient status-read server errors, then checks the remote daemon with bounded, retryable probes and verifies Buildx readiness before selecting the context and builder for subsequent job steps using `DOCKER_CONTEXT` and `BUILDX_BUILDER`. It leaves Docker's saved defaults unchanged and restores the previous job environment during cleanup. If a startup attempt fails, setup reports its generation, stage and failure reason immediately. A fresh job can retry a failed startup against the same builder and retained cache. Simultaneous jobs share that startup; canceling one job keeps the shared startup running. @@ -44,7 +46,7 @@ The normal setup works with `docker/build-push-action`, `docker buildx build`, a Use one connection per job. Post-job cleanup disconnects the local proxy and removes its context, including after build failure. It keeps the remote builder and cache; organization idle policy sleeps compute. Runner termination can prevent cleanup. Disconnecting does not prove a remote operation has stopped: uncertain operations retain their slot until the service reconciles them or an administrator recovers the builder. -One builder can serve multiple native builds on the same daemon and retained cache. Organization concurrency defaults to one across all its builders; an admin can raise it with `com docker-builder settings --org acme --max-concurrency 4`. New VMs receive 2 CPUs and 4 GiB RAM per configured concurrent operation, with a 256 GiB cache disk. Changing the policy does not resize an already running VM: drain its builds, sleep it to checkpoint the cache, then wake it. Qualify image size, cold build memory and warm performance before switching production builds. +One builder can serve multiple native builds on the same daemon and retained cache. Organization concurrency defaults to one across all its builders; an admin can raise it with `com docker-builder settings --org acme --max-concurrency 4`. Gen2 builders run on AWS Boxmad nodes with fair CPU shares and 4 GiB RAM reserved per configured concurrent operation. Each builder mounts its own encrypted 512 GiB gp3 EBS cache disk (16,000 provisioned IOPS / 1,000 MiB/s) directly as ext4 for Docker overlay2. The volume persists when compute sleeps. Changing the policy does not resize an already running builder: drain its builds, sleep it to checkpoint the cache, then wake it. Qualify image size, cold build memory and warm performance before switching production builds. Coordinate CI jobs to fit the organization's configured capacity. The single concurrency group in the example serializes jobs; multiple groups can admit more jobs when capacity has been increased. GitHub concurrency only coordinates workflows in the same repository, and concurrent work outside those groups can still be refused. Use `queue: max` to retain pending builds (up to 100); without it GitHub replaces the previous pending build. Use separate builders when workloads need independent caches. Remote builders are in preview. diff --git a/action.yml b/action.yml index e73ca77..1dc25d9 100644 --- a/action.yml +++ b/action.yml @@ -1,6 +1,6 @@ name: Composal Docker builder author: ComposalAI -description: Connect Buildx to an existing Composal builder with a persistent remote Docker cache. +description: Connect Buildx to an existing Composal builder with a persistent EBS cache on the AWS Gen2 fleet. branding: icon: package color: purple @@ -13,7 +13,7 @@ inputs: required: true builder: description: Existing organization builder slug or public ID. - default: main + default: main-gen2 wake-timeout: description: Maximum seconds to wait for the shared builder to become ready, before connecting Docker. default: '900' diff --git a/builder.mjs b/builder.mjs index 8d039e5..af9590c 100644 --- a/builder.mjs +++ b/builder.mjs @@ -46,7 +46,7 @@ export async function setup(env = process.env, run = command, wait = delay) { if (!token) throw new Error('token is required'); if (env.GITHUB_ACTIONS === 'true') console.log(`::add-mask::${escapeCommand(token)}`); const org = input(env, 'org'); - const builder = input(env, 'builder', 'main'); + const builder = input(env, 'builder', 'main-gen2'); if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(org)) throw new Error('org is required and must be a slug or public ID'); if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(builder)) throw new Error('builder must be a slug or public ID'); const api = new URL(input(env, 'api-url', 'https://composal.ai')); @@ -107,6 +107,22 @@ export async function setup(env = process.env, run = command, wait = delay) { if (!/^org_[a-zA-Z0-9]+$/.test(resource?.organization_id) || !/^docker_builder_[a-zA-Z0-9]+$/.test(resource?.id)) { throw new Error('Composal CLI returned an unexpected builder identity'); } + // Read the new backend field directly until all released CLIs retain it. + // Fail closed: a v2 action must never quietly build on Classic or locally. + let backend; + try { + const response = await fetch(`${api.origin}/api/v1/organizations/${resource.organization_id}/docker_builders/${resource.id}`, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, + redirect: 'error', signal: AbortSignal.timeout(15_000), + }); + if (!response.ok) throw new Error('backend lookup refused'); + backend = (await response.json()).docker_builder; + } catch { + throw new Error('Could not verify Gen2 builder placement'); + } + if (backend?.id !== resource.id || backend.organization_id !== resource.organization_id || backend.backend !== 'gen2') { + throw new Error('docker-builder@2 requires an enabled Gen2 builder; select its builder name'); + } stage(`Builder resolved: ${resource.status || 'unknown state'}`); if (resource.status !== 'ready') { // Complete the control-plane wake before Docker opens its first HTTP diff --git a/builder.test.mjs b/builder.test.mjs index c799d8e..61875ec 100644 --- a/builder.test.mjs +++ b/builder.test.mjs @@ -6,6 +6,9 @@ import { join } from 'node:path'; import { command, cleanup, reportFailure, setup } from './builder.mjs'; async function fixture(t, inputs = {}, failure = '') { + t.mock.method(globalThis, 'fetch', async () => ({ ok: true, json: async () => ({ docker_builder: { + id: 'docker_builder_456', organization_id: 'org_123', backend: 'gen2', + } }) })); const directory = await mkdtemp(join(tmpdir(), 'com-builder-test-')); t.after(() => rm(directory, { recursive: true, force: true })); const env = { @@ -410,3 +413,13 @@ test('command failures preserve timeout and signal details without stdout or env return true; }); }); + + +test('v2 refuses a Classic builder before waking or connecting Docker', async t => { + const f = await fixture(t); + t.mock.method(globalThis, 'fetch', async () => ({ok: true, json: async () => ({docker_builder: { + id: 'docker_builder_456', organization_id: 'org_123', backend: 'classic', + }})})); + await assert.rejects(setup(f.env, f.run), /requires an enabled Gen2 builder/); + assert.ok(!f.calls.some(c => c.args[1] === 'wake' || c.args[1] === 'setup')); +}); From 753887dc5dff9ae583aaedec750058fedd199f8b Mon Sep 17 00:00:00 2001 From: Pandelis Zembashis Date: Fri, 23 Oct 2026 18:24:04 -0700 Subject: [PATCH 2/3] Document Gen2 preview capacity limits --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ceb54d9..b43cd89 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The normal setup works with `docker/build-push-action`, `docker buildx build`, a Use one connection per job. Post-job cleanup disconnects the local proxy and removes its context, including after build failure. It keeps the remote builder and cache; organization idle policy sleeps compute. Runner termination can prevent cleanup. Disconnecting does not prove a remote operation has stopped: uncertain operations retain their slot until the service reconciles them or an administrator recovers the builder. -One builder can serve multiple native builds on the same daemon and retained cache. Organization concurrency defaults to one across all its builders; an admin can raise it with `com docker-builder settings --org acme --max-concurrency 4`. Gen2 builders run on AWS Boxmad nodes with fair CPU shares and 4 GiB RAM reserved per configured concurrent operation. Each builder mounts its own encrypted 512 GiB gp3 EBS cache disk (16,000 provisioned IOPS / 1,000 MiB/s) directly as ext4 for Docker overlay2. The volume persists when compute sleeps. Changing the policy does not resize an already running builder: drain its builds, sleep it to checkpoint the cache, then wake it. Qualify image size, cold build memory and warm performance before switching production builds. +One builder can serve multiple native builds on the same daemon and retained cache. Organization concurrency defaults to one across all its builders; an admin can raise it with `com docker-builder settings --org acme --max-concurrency 4`. Gen2 organizations currently support at most eight concurrent builds and a six-hour compute lifetime; activation and later policy updates reject larger settings. Gen2 builders run on AWS Boxmad nodes with fair CPU shares and 4 GiB RAM reserved per configured concurrent operation. Each builder mounts its own encrypted 512 GiB gp3 EBS cache disk (16,000 provisioned IOPS / 1,000 MiB/s) directly as ext4 for Docker overlay2. The volume persists when compute sleeps. Changing the policy does not resize an already running builder: drain its builds, sleep it to checkpoint the cache, then wake it. Qualify image size, cold build memory and warm performance before switching production builds. Coordinate CI jobs to fit the organization's configured capacity. The single concurrency group in the example serializes jobs; multiple groups can admit more jobs when capacity has been increased. GitHub concurrency only coordinates workflows in the same repository, and concurrent work outside those groups can still be refused. Use `queue: max` to retain pending builds (up to 100); without it GitHub replaces the previous pending build. Use separate builders when workloads need independent caches. Remote builders are in preview. From d05d5a8361b9f752a6468fd187dbd3414dda1d12 Mon Sep 17 00:00:00 2001 From: Pandelis Zembashis Date: Fri, 23 Oct 2026 18:33:57 -0700 Subject: [PATCH 3/3] Clarify Gen2 admission and resource reservation limits --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b43cd89..8b728e8 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ The normal setup works with `docker/build-push-action`, `docker buildx build`, a Use one connection per job. Post-job cleanup disconnects the local proxy and removes its context, including after build failure. It keeps the remote builder and cache; organization idle policy sleeps compute. Runner termination can prevent cleanup. Disconnecting does not prove a remote operation has stopped: uncertain operations retain their slot until the service reconciles them or an administrator recovers the builder. -One builder can serve multiple native builds on the same daemon and retained cache. Organization concurrency defaults to one across all its builders; an admin can raise it with `com docker-builder settings --org acme --max-concurrency 4`. Gen2 organizations currently support at most eight concurrent builds and a six-hour compute lifetime; activation and later policy updates reject larger settings. Gen2 builders run on AWS Boxmad nodes with fair CPU shares and 4 GiB RAM reserved per configured concurrent operation. Each builder mounts its own encrypted 512 GiB gp3 EBS cache disk (16,000 provisioned IOPS / 1,000 MiB/s) directly as ext4 for Docker overlay2. The volume persists when compute sleeps. Changing the policy does not resize an already running builder: drain its builds, sleep it to checkpoint the cache, then wake it. Qualify image size, cold build memory and warm performance before switching production builds. +One builder can serve multiple native builds on the same daemon and retained cache. Organization concurrency defaults to one across all its builders; an admin can raise it with `com docker-builder settings --org acme --max-concurrency 4`. Organization admission limits support up to 32 concurrent builds. Gen2 builders run on AWS Boxmad nodes with fair CPU shares and 4 GiB RAM reserved per build slot, capped at eight resource slots (32 GiB RAM). Higher admission limits share those resources. The six-hour compute lifetime limit is validated before activation and policy changes. Each builder mounts its own encrypted 512 GiB gp3 EBS cache disk (16,000 provisioned IOPS / 1,000 MiB/s) directly as ext4 for Docker overlay2. The volume persists when compute sleeps. Changing the policy does not resize an already running builder: drain its builds, sleep it to checkpoint the cache, then wake it. Qualify image size, cold build memory and warm performance before switching production builds. Coordinate CI jobs to fit the organization's configured capacity. The single concurrency group in the example serializes jobs; multiple groups can admit more jobs when capacity has been increased. GitHub concurrency only coordinates workflows in the same repository, and concurrent work outside those groups can still be refused. Use `queue: max` to retain pending builds (up to 100); without it GitHub replaces the previous pending build. Use separate builders when workloads need independent caches. Remote builders are in preview.