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
5 changes: 4 additions & 1 deletion builder.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,14 @@ export async function setup(env = process.env, run = command, wait = delay) {
// the remote session. Retry this read only; never replay a build or push.
// The control-plane wake is complete. Bound each native probe separately
// so a stale connection cannot consume the entire readiness deadline.
// Docker negotiates its API with a ping before requesting the version. Each
// request establishes an authenticated tunnel; allow both to finish under
// load without restarting the shared 90-second readiness budget.
const readinessDeadline = Date.now() + 90_000;
for (let attempt = 0; attempt < 5; attempt++) {
try {
const version = await run('docker', ['--context', context, 'version', '--format', '{{.Server.Version}}'], dockerEnv,
Math.max(1, Math.min(15_000, readinessDeadline - Date.now())));
Math.max(1, Math.min(45_000, readinessDeadline - Date.now())));
if (!/^\d+\.\d+\.\d+/.test(version.trim())) throw new Error('Remote Docker daemon is not ready');
break;
} catch (error) {
Expand Down
26 changes: 24 additions & 2 deletions builder.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ test('a stalled native connection uses one readiness budget and cannot restart t
return f.run(exe, args, env, timeout);
};
await assert.rejects(setup(f.env, run, async ms => waits.push(ms)), /connection reset/);
assert.deepEqual(timeouts, [15_000]);
assert.deepEqual(timeouts, [45_000]);
assert.deepEqual(waits, []);
await assert.rejects(readFile(f.env.GITHUB_OUTPUT), { code: 'ENOENT' });
});
Expand All @@ -389,7 +389,7 @@ test('a long shared wake completes before opening short-lived Docker probes', as
} });
if (args.includes('version')) {
assert.equal(now, 760_000);
assert.equal(timeout, 15_000);
assert.equal(timeout, 45_000);
}
return result;
};
Expand All @@ -409,6 +409,28 @@ test('native probe timeouts are retried without repeating setup or a build', asy
assert.equal(f.calls.filter(c => c.args[1] === 'setup').length, 1);
});

test('slow native negotiation can finish and retries share the remaining readiness budget', async t => {
const f = await fixture(t);
let now = 0;
t.mock.method(Date, 'now', () => now);
const timeouts = [];
const run = async (exe, args, env, timeout) => {
if (args.includes('version')) {
timeouts.push(timeout);
if (timeouts.length === 1) {
now += timeout;
throw new Error('timed out after 45s');
}
// A healthy negotiation may take longer than the former 15s limit.
now += 25_000;
}
return f.run(exe, args, env, timeout);
};
await setup(f.env, run, async ms => { now += ms; });
assert.deepEqual(timeouts, [45_000, 43_000]);
assert.ok((await readFile(f.env.GITHUB_OUTPUT, 'utf8')).includes('driver<<'));
});

test('command failures preserve timeout and signal details without stdout or environment', async () => {
await assert.rejects(command(process.execPath, ['-e', "console.log('private-stdout'); setInterval(() => {}, 1000)"],
{ PRIVATE_TOKEN: 'private-env' }, 100), error => {
Expand Down