diff --git a/README.md b/README.md index 8b728e8..114d293 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Authenticate to your registry before selecting the remote context: the gateway d 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-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. +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 before selecting the context and builder for subsequent job steps using `DOCKER_CONTEXT` and `BUILDX_BUILDER`. CLI setup verifies that Buildx is installed; the build performs BuildKit discovery once, without a redundant setup inspection. 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. diff --git a/builder.mjs b/builder.mjs index af9590c..7e4bd28 100644 --- a/builder.mjs +++ b/builder.mjs @@ -195,8 +195,9 @@ export async function setup(env = process.env, run = command, wait = delay) { } } stage('Remote Docker ready'); - // Inspect alone can exit zero with a node error; version must succeed first. - await run('docker', ['buildx', 'inspect', context, '--bootstrap'], dockerEnv, 300_000); + // CLI setup already verifies the local Buildx plugin and implicit Docker + // driver. The build discovers BuildKit itself; repeating discovery here adds + // remote round trips without making the first build faster. if (use === 'true') { await record(env.GITHUB_STATE, 'selected', 'true'); // The scoped gateway does not expose BuildKit content-record downloads. diff --git a/builder.test.mjs b/builder.test.mjs index 61875ec..1be0046 100644 --- a/builder.test.mjs +++ b/builder.test.mjs @@ -43,14 +43,14 @@ async function fixture(t, inputs = {}, failure = '') { test('setup returns a ready explicit builder and selects it for subsequent steps without persisting its token', async t => { const f = await fixture(t); await setup(f.env, f.run); - assert.deepEqual(f.calls.map(c => c.args.slice(0, 2)), [['--version'], ['docker-builder', 'status'], ['docker-builder', 'setup'], ['--context', f.calls[3].args[1]], ['buildx', 'inspect']]); + assert.deepEqual(f.calls.map(c => c.args.slice(0, 2)), [['--version'], ['docker-builder', 'status'], ['docker-builder', 'setup'], ['--context', f.calls[3].args[1]]]); for (const call of f.calls.slice(1, 3)) { assert.equal(call.env.COMPOSAL_API_TOKEN, f.env.INPUT_TOKEN); assert.equal(call.env.VEX_API_TOKEN, f.env.INPUT_TOKEN); assert.ok(!call.args.join(' ').includes(f.env.INPUT_TOKEN)); } assert.equal(f.calls[3].env.COMPOSAL_API_TOKEN, undefined); - assert.equal(f.calls[4].env.DOCKER_CONTEXT, f.calls[3].args[1]); + assert.equal(f.calls[3].env.DOCKER_CONTEXT, f.calls[3].args[1]); const environment = await readFile(f.env.GITHUB_ENV, 'utf8'); assert.ok(environment.includes('DOCKER_CONTEXT<<')); assert.ok(environment.includes('BUILDX_METADATA_PROVENANCE<<')); @@ -187,14 +187,6 @@ test('a stalled control-plane wake has one bounded deadline and publishes no con await assert.rejects(readFile(f.env.GITHUB_OUTPUT), { code: 'ENOENT' }); }); -test('bootstrap failure retains cleanup state and emits no usable builder', async t => { - const f = await fixture(t, {}, '--bootstrap'); - await assert.rejects(setup(f.env, f.run), /refused/); - await assert.rejects(readFile(f.env.GITHUB_OUTPUT), { code: 'ENOENT' }); - await cleanup({ ...f.env, ...await f.state() }, f.run); - assert.equal(f.calls.at(-2).args[1], 'disconnect'); -}); - test('missing builder fails before setup; CI cannot silently create a misspelled cache', async t => { const f = await fixture(t, {}, 'status'); await assert.rejects(setup(f.env, f.run), /refused/); @@ -253,6 +245,20 @@ test('malformed setup response still disconnects the resolved builder without em }); +test('a non-Docker driver is refused before publishing outputs and remains cleanable', async t => { + const f = await fixture(t); + const run = async (executable, args, env) => { + const result = await f.run(executable, args, env); + return args[1] === 'setup' ? JSON.stringify({ ...JSON.parse(result), driver: 'docker-container' }) : result; + }; + await assert.rejects(setup(f.env, run), /unexpected builder connection/); + await assert.rejects(readFile(f.env.GITHUB_OUTPUT), { code: 'ENOENT' }); + await cleanup({ ...f.env, ...await f.state() }, f.run); + assert.equal(f.calls.at(-2).args[1], 'disconnect'); + assert.equal(f.calls.at(-1).args[1], 'rm'); +}); + + test('explicit-output mode leaves Docker environment untouched', async t => { const f = await fixture(t, { INPUT_USE: 'false' }); await setup(f.env, f.run); @@ -284,7 +290,7 @@ test('local invocation never prints a token-bearing GitHub masking command', asy }); -test('daemon failure refuses setup even when Buildx inspect would return success', async t => { +test('daemon failure retains cleanup state and refuses to publish a usable builder', async t => { const f = await fixture(t, {}, 'version'); await assert.rejects(setup(f.env, f.run), /refused/); await assert.rejects(readFile(f.env.GITHUB_OUTPUT), { code: 'ENOENT' });