@yadurajfleetos/cli 0.18.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/args.js +1 -1
- package/dist/commands/services.js +69 -32
- package/dist/commands/up.js +18 -27
- package/dist/config.js +1 -1
- package/dist/deploy-wait.js +105 -0
- package/dist/index.js +4 -0
- package/package.json +1 -1
package/dist/args.js
CHANGED
|
@@ -46,7 +46,7 @@ export const KNOWN_FLAGS = new Set([
|
|
|
46
46
|
'plan', 'dry-run', 'force', 'dir', 'file', 'manifest',
|
|
47
47
|
// per command
|
|
48
48
|
'ai', 'all', 'apply', 'channel', 'deploy', 'email', 'events', 'f', 'follow', 'limit',
|
|
49
|
-
'name', 'node', 'only', 'out', 'password', 'secret', 'service', 'sha',
|
|
49
|
+
'name', 'node', 'only', 'out', 'password', 'project', 'secret', 'service', 'sha',
|
|
50
50
|
'since', 'terminal', 'to', 'token', 'url',
|
|
51
51
|
]);
|
|
52
52
|
/** The closest known flag to a mistyped one, or null when nothing is close. */
|
|
@@ -7,6 +7,7 @@ import { withLadder } from '../ladder.js';
|
|
|
7
7
|
import { ask, canPrompt, confirm, selectOrThrow } from '../prompt.js';
|
|
8
8
|
import { DEPLOY_STEPS, follow, phaseWalker, } from '../progress.js';
|
|
9
9
|
import { planFromDiscovery, renderPlan, toAssistPlan } from '../deployment-plan.js';
|
|
10
|
+
import { requireRunning } from '../deploy-wait.js';
|
|
10
11
|
import { planFromManifest, projectNameFor } from '../plan.js';
|
|
11
12
|
import { uploadContext, humanBytes } from '../archive.js';
|
|
12
13
|
import { localSource } from '../source.js';
|
|
@@ -132,13 +133,51 @@ export const servicesCommand = {
|
|
|
132
133
|
}
|
|
133
134
|
},
|
|
134
135
|
};
|
|
135
|
-
|
|
136
|
+
/**
|
|
137
|
+
* The service a name refers to, in a fleet where a name is no longer unique.
|
|
138
|
+
*
|
|
139
|
+
* Services are identified by (fleet, project, name), so two projects may both
|
|
140
|
+
* have a "backend". This used to take the first match, which meant a command
|
|
141
|
+
* could act on another project's service without saying so — the same class of
|
|
142
|
+
* mistake as the apply that silently took one over.
|
|
143
|
+
*
|
|
144
|
+
* Resolved in the order the operator's intent is clearest:
|
|
145
|
+
*
|
|
146
|
+
* --project an explicit answer, and an error if it does not match
|
|
147
|
+
* the directory the project a manifest here would apply as, which is how
|
|
148
|
+
* the service got its project in the first place
|
|
149
|
+
* otherwise refuse, and list the projects to choose between
|
|
150
|
+
*
|
|
151
|
+
* An id always wins: it identifies a row on its own and needs no project.
|
|
152
|
+
*/
|
|
153
|
+
/** `--project`, when the operator gave one. */
|
|
154
|
+
const projectFlag = (flags) => typeof flags.project === 'string' ? flags.project : undefined;
|
|
155
|
+
async function findService(fleetId, name, project) {
|
|
136
156
|
const { body } = await request('GET', `/fleets/${fleetId}/services`);
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
157
|
+
const byId = body.services.find((s) => s.id === name);
|
|
158
|
+
if (byId)
|
|
159
|
+
return byId;
|
|
160
|
+
const matches = body.services.filter((s) => s.name === name);
|
|
161
|
+
if (!matches.length) {
|
|
139
162
|
throw new CliError(`No service called "${name}". Known: ${body.services.map((s) => s.name).join(', ') || 'none'}`, EXIT.usage);
|
|
140
163
|
}
|
|
141
|
-
|
|
164
|
+
if (matches.length === 1)
|
|
165
|
+
return matches[0];
|
|
166
|
+
if (project) {
|
|
167
|
+
const scoped = matches.find((s) => s.project === project);
|
|
168
|
+
if (scoped)
|
|
169
|
+
return scoped;
|
|
170
|
+
throw new CliError(`No service called "${name}" in project "${project}". ` +
|
|
171
|
+
`It exists in: ${matches.map((s) => s.project).join(', ')}.`, EXIT.usage);
|
|
172
|
+
}
|
|
173
|
+
// The project this directory would apply as. Not a guess: it is the same
|
|
174
|
+
// value `fleet apply` uses, so it names the service this directory owns.
|
|
175
|
+
const here = projectNameFor(process.cwd());
|
|
176
|
+
const local = matches.find((s) => s.project === here);
|
|
177
|
+
if (local)
|
|
178
|
+
return local;
|
|
179
|
+
throw new CliError(`"${name}" exists in more than one project: ${matches.map((s) => s.project).join(', ')}. ` +
|
|
180
|
+
`Say which with --project <name>.`, EXIT.usage);
|
|
142
181
|
}
|
|
143
182
|
async function deployPlan(fleetId, service) {
|
|
144
183
|
return (await request('GET', `/services/${service.id}/placement-preview`)).body.decision;
|
|
@@ -185,27 +224,25 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
185
224
|
* follows it to conclusion rather than reporting "scheduled" and leaving the
|
|
186
225
|
* operator to guess.
|
|
187
226
|
*/
|
|
188
|
-
async function waitUntilRunning(fleetId, name,
|
|
227
|
+
async function waitUntilRunning(fleetId, name, project) {
|
|
228
|
+
const service = await findService(fleetId, name, project);
|
|
189
229
|
await task(`waiting for ${c.bold(name)} to come up`, async (s) => {
|
|
190
230
|
s.hints([
|
|
191
231
|
'the agent picks up desired state on its next poll',
|
|
192
|
-
|
|
232
|
+
"a cold image pull takes as long as the node's uplink does",
|
|
193
233
|
'this clears once the agent reports the container running',
|
|
194
234
|
]);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
throw new CliError(`"${name}" was scheduled but has not reported running. \`fleet deployments ${name}\` has the detail.`, EXIT.healthCheckFailed);
|
|
208
|
-
}, { done: () => `${c.bold(name)} is running` });
|
|
235
|
+
// The shared waiter, on the shared deadline. This used to be its own
|
|
236
|
+
// loop with its own three-minute deadline, which failed a deploy that
|
|
237
|
+
// reported running three seconds later.
|
|
238
|
+
await requireRunning(fleetId, service.id, name, {
|
|
239
|
+
onPoll: (status, elapsedMs) => {
|
|
240
|
+
if (!status)
|
|
241
|
+
return;
|
|
242
|
+
s.update(`${c.bold(name)} · ${status} · ${Math.round(elapsedMs / 1000)}s`);
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}, { done: () => 'running' });
|
|
209
246
|
}
|
|
210
247
|
/**
|
|
211
248
|
* The build context a service declares, if any, read from the local manifest.
|
|
@@ -234,7 +271,7 @@ export const deployCommand = {
|
|
|
234
271
|
const [name] = args;
|
|
235
272
|
if (!name)
|
|
236
273
|
throw new CliError('usage: fleet deploy <service> [--sha <git-sha>] [--no-wait] [--dir <path>]', EXIT.usage);
|
|
237
|
-
const service = await findService(fleetId, name);
|
|
274
|
+
const service = await findService(fleetId, name, projectFlag(flags));
|
|
238
275
|
const gitSha = typeof flags.sha === 'string' ? flags.sha : undefined;
|
|
239
276
|
const plan = await task('checking deployment plan', async () => deployPlan(fleetId, service));
|
|
240
277
|
const viable = plan.outcome === 'placed' && Boolean(plan.nodeName);
|
|
@@ -304,7 +341,7 @@ export const deployCommand = {
|
|
|
304
341
|
if (body.url)
|
|
305
342
|
console.log(`${glyph.info} ${c.cyan(body.url)}`);
|
|
306
343
|
if (!flags['no-wait'])
|
|
307
|
-
await waitUntilRunning(fleetId, service.name);
|
|
344
|
+
await waitUntilRunning(fleetId, service.name, service.project);
|
|
308
345
|
},
|
|
309
346
|
};
|
|
310
347
|
export const whereCommand = {
|
|
@@ -313,7 +350,7 @@ export const whereCommand = {
|
|
|
313
350
|
const [name] = args;
|
|
314
351
|
if (!name)
|
|
315
352
|
throw new CliError('usage: fleet where <service>', EXIT.usage);
|
|
316
|
-
const service = await findService(fleetId, name);
|
|
353
|
+
const service = await findService(fleetId, name, projectFlag(flags));
|
|
317
354
|
const { body } = await request('GET', `/services/${service.id}/placement-preview`);
|
|
318
355
|
const d = body.decision;
|
|
319
356
|
if (flags.json)
|
|
@@ -346,7 +383,7 @@ export const rescheduleCommand = {
|
|
|
346
383
|
const [name] = args;
|
|
347
384
|
if (!name)
|
|
348
385
|
throw new CliError('usage: fleet reschedule <service>', EXIT.usage);
|
|
349
|
-
const service = await findService(fleetId, name);
|
|
386
|
+
const service = await findService(fleetId, name, projectFlag(flags));
|
|
350
387
|
const { body } = await request('POST', `/services/${service.id}/reschedule`);
|
|
351
388
|
console.log(`${c.green('moved')} ${service.name} → ${c.bold(body.movedTo.name)}`);
|
|
352
389
|
},
|
|
@@ -375,7 +412,7 @@ export const deploymentsCommand = {
|
|
|
375
412
|
const [name] = args;
|
|
376
413
|
if (!name)
|
|
377
414
|
throw new CliError('usage: fleet deployments <service>', EXIT.usage);
|
|
378
|
-
const service = await findService(fleetId, name);
|
|
415
|
+
const service = await findService(fleetId, name, projectFlag(flags));
|
|
379
416
|
const { body } = await request('GET', `/services/${service.id}/deployments`);
|
|
380
417
|
if (flags.json)
|
|
381
418
|
return console.log(JSON.stringify(body.deployments, null, 2));
|
|
@@ -396,13 +433,13 @@ export const restartCommand = {
|
|
|
396
433
|
const [name] = args;
|
|
397
434
|
if (!name)
|
|
398
435
|
throw new CliError('usage: fleet restart <service>', EXIT.usage);
|
|
399
|
-
const service = await findService(fleetId, name);
|
|
436
|
+
const service = await findService(fleetId, name, projectFlag(flags));
|
|
400
437
|
const { body } = await request('POST', `/services/${service.id}/restart`, { body: {} });
|
|
401
438
|
if (flags.json)
|
|
402
439
|
return console.log(JSON.stringify(body, null, 2));
|
|
403
440
|
console.log(`${glyph.ok} ${c.green('restart scheduled')} ${service.name} ${c.dim(body.deployment.id.slice(0, 8))}`);
|
|
404
441
|
if (!flags['no-wait'])
|
|
405
|
-
await waitUntilRunning(fleetId, service.name);
|
|
442
|
+
await waitUntilRunning(fleetId, service.name, service.project);
|
|
406
443
|
},
|
|
407
444
|
};
|
|
408
445
|
export const rollbackCommand = {
|
|
@@ -411,7 +448,7 @@ export const rollbackCommand = {
|
|
|
411
448
|
const [name, deploymentId] = args;
|
|
412
449
|
if (!name)
|
|
413
450
|
throw new CliError('usage: fleet rollback <service> [deployment-id]', EXIT.usage);
|
|
414
|
-
const service = await findService(fleetId, name);
|
|
451
|
+
const service = await findService(fleetId, name, projectFlag(flags));
|
|
415
452
|
if (!flags.yes && !flags.y && !(await confirmDeploy())) {
|
|
416
453
|
console.log(c.dim('Rollback cancelled.'));
|
|
417
454
|
return;
|
|
@@ -421,7 +458,7 @@ export const rollbackCommand = {
|
|
|
421
458
|
return console.log(JSON.stringify(body, null, 2));
|
|
422
459
|
console.log(`${glyph.ok} ${c.green('rollback scheduled')} ${service.name} ← ${c.dim(body.rolledBackTo.slice(0, 8))}`);
|
|
423
460
|
if (!flags['no-wait'])
|
|
424
|
-
await waitUntilRunning(fleetId, service.name);
|
|
461
|
+
await waitUntilRunning(fleetId, service.name, service.project);
|
|
425
462
|
},
|
|
426
463
|
};
|
|
427
464
|
export const logsCommand = {
|
|
@@ -430,7 +467,7 @@ export const logsCommand = {
|
|
|
430
467
|
const [name] = args;
|
|
431
468
|
if (!name)
|
|
432
469
|
throw new CliError('usage: fleet logs <service> [--follow] [--since 1h]', EXIT.usage);
|
|
433
|
-
const service = await findService(fleetId, name);
|
|
470
|
+
const service = await findService(fleetId, name, projectFlag(flags));
|
|
434
471
|
if (flags.since)
|
|
435
472
|
console.error(c.dim('note: agent log tails are live snapshots; --since is limited to the current retained tail.'));
|
|
436
473
|
let previous = '';
|
|
@@ -859,7 +896,7 @@ export const removeServiceCommand = {
|
|
|
859
896
|
if (!name)
|
|
860
897
|
throw new CliError('usage: fleet rm <service> [--yes]', EXIT.usage);
|
|
861
898
|
const fleetId = await requireFleet(typeof flags.fleet === 'string' ? flags.fleet : undefined);
|
|
862
|
-
const service = await findService(fleetId, name);
|
|
899
|
+
const service = await findService(fleetId, name, projectFlag(flags));
|
|
863
900
|
const confirmed = flags.yes === true || flags.y === true;
|
|
864
901
|
if (!confirmed) {
|
|
865
902
|
if (!process.stdin.isTTY) {
|
|
@@ -972,7 +1009,7 @@ export const explainCommand = {
|
|
|
972
1009
|
const name = args[0];
|
|
973
1010
|
if (!name)
|
|
974
1011
|
throw new CliError('name a service, or pass --deploy <id>', EXIT.usage);
|
|
975
|
-
const service = await findService(id, name);
|
|
1012
|
+
const service = await findService(id, name, projectFlag(flags));
|
|
976
1013
|
const { body } = await request('GET', `/services/${service.id}/deployments`);
|
|
977
1014
|
// The most recent failure, which is what someone asking "why did that
|
|
978
1015
|
// fail" means — not the most recent deployment, which may since have
|
package/dist/commands/up.js
CHANGED
|
@@ -14,6 +14,7 @@ import { c } from '../render.js';
|
|
|
14
14
|
import { task, glyph } from '../ui.js';
|
|
15
15
|
import { withLadder } from '../ladder.js';
|
|
16
16
|
import { DEPLOY_STEPS, follow, phaseWalker } from '../progress.js';
|
|
17
|
+
import { requireRunning } from '../deploy-wait.js';
|
|
17
18
|
import { planFromManifest, deployOrder, projectNameFor } from '../plan.js';
|
|
18
19
|
import { uploadContext, humanBytes } from '../archive.js';
|
|
19
20
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
@@ -179,22 +180,21 @@ async function deployOne(service, opts) {
|
|
|
179
180
|
// building afterwards, so this is the window in which a multi-arch
|
|
180
181
|
// build has to finish - and an arm64 build emulated on an amd64 host
|
|
181
182
|
// is measured in tens of minutes, not minutes.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
// 477-second deploy was indistinguishable from a hang.
|
|
183
|
+
// The same waiter `fleet deploy` uses, on the same deadline. Two
|
|
184
|
+
// commands watching the same thing in two loops is how one of them
|
|
185
|
+
// ended up giving up at three minutes.
|
|
186
|
+
await requireRunning(opts.fleetId, service.id, service.name, {
|
|
187
|
+
onPoll: async () => {
|
|
188
|
+
// What the builder is doing, rather than a hint about what it
|
|
189
|
+
// might be doing. Every field here has been reaching /progress
|
|
190
|
+
// since the phase writer was added and nothing asked for it.
|
|
191
|
+
// Failures are swallowed: this is a label, and losing it must not
|
|
192
|
+
// end a deploy that is going fine.
|
|
193
|
+
const line = await request('GET', `/services/${service.id}/progress`)
|
|
194
|
+
.then((r) => r.body)
|
|
195
|
+
.catch(() => null);
|
|
196
|
+
if (!line || !['queued', 'building', 'pushing', 'deploying'].includes(line.status))
|
|
197
|
+
return;
|
|
198
198
|
const parts = [line.phase ?? line.status];
|
|
199
199
|
if (line.step && line.ofSteps)
|
|
200
200
|
parts.push(`${line.step}/${line.ofSteps}`);
|
|
@@ -205,17 +205,8 @@ async function deployOne(service, opts) {
|
|
|
205
205
|
s.update(`${c.bold(service.name)} · ${parts.join(' · ')}`);
|
|
206
206
|
if (line.detail)
|
|
207
207
|
s.hints([line.detail]);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const current = body.services.find((s) => s.id === service.id)?.current;
|
|
211
|
-
if (current?.status === 'running')
|
|
212
|
-
return;
|
|
213
|
-
if (current?.status === 'failed') {
|
|
214
|
-
throw new CliError(`"${service.name}" did not start. \`fleet deployments ${service.name}\` has the reason.`, EXIT.healthCheckFailed);
|
|
215
|
-
}
|
|
216
|
-
await sleep(2000);
|
|
217
|
-
}
|
|
218
|
-
throw new CliError(`"${service.name}" was scheduled but has not reported running.`, EXIT.healthCheckFailed);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
219
210
|
}, { done: () => `${c.bold(service.name)} is running` });
|
|
220
211
|
}
|
|
221
212
|
return deployResult.url;
|
package/dist/config.js
CHANGED
|
@@ -23,7 +23,7 @@ export async function loadProfile() {
|
|
|
23
23
|
catch {
|
|
24
24
|
// The public control plane is the useful default for a first-time install.
|
|
25
25
|
// Self-hosters and CI can always override it with FLEET_API or --api.
|
|
26
|
-
return { api: fromEnv.api || 'https://fleetapi.plastikworld.xyz'
|
|
26
|
+
return { ...fromEnv, api: fromEnv.api || 'https://fleetapi.plastikworld.xyz' };
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
export async function saveProfile(profile) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Waiting for a deploy to reach a terminal state, once, for every command.
|
|
3
|
+
*
|
|
4
|
+
* `fleet up` and `fleet deploy` both watched a deploy and drifted apart while
|
|
5
|
+
* doing it. `up` was widened to 45 minutes when it took over the build, because
|
|
6
|
+
* an arm64 image emulated on an amd64 control plane is measured in tens of
|
|
7
|
+
* minutes. `deploy` kept a three-minute deadline nobody revisited.
|
|
8
|
+
*
|
|
9
|
+
* A real deploy of landing-page started at 15:44:54 and reported running at
|
|
10
|
+
* 15:47:57 — 183 seconds. `fleet deploy` gave up at 181 and told the operator
|
|
11
|
+
* the service "was scheduled but has not reported running", about a service
|
|
12
|
+
* that was running. Two seconds of drift produced a false failure, and the
|
|
13
|
+
* message sent somebody to debug a healthy container.
|
|
14
|
+
*
|
|
15
|
+
* So the deadline is one constant and the loop is one function. Two commands
|
|
16
|
+
* waiting for the same thing in two places is how the drift happened, and
|
|
17
|
+
* sharing the constant without sharing the loop would only slow it down.
|
|
18
|
+
*/
|
|
19
|
+
import { request, CliError, EXIT } from './api.js';
|
|
20
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
21
|
+
/**
|
|
22
|
+
* How long a deploy may take before the CLI stops watching.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately longer than the control plane's own BUILD_TIMEOUT_MS (20
|
|
25
|
+
* minutes): the client must not call a deploy dead while the server is still
|
|
26
|
+
* legitimately working on it. The server owns "this build is stuck"; this
|
|
27
|
+
* number only owns "nobody is watching any more".
|
|
28
|
+
*/
|
|
29
|
+
export const DEPLOY_READY_TIMEOUT_MS = 45 * 60_000;
|
|
30
|
+
/** The service's current deployment status, or null when it cannot be read. */
|
|
31
|
+
const liveStatus = (fleetId, serviceId) => request('GET', `/fleets/${fleetId}/services`)
|
|
32
|
+
.then((r) => r.body.services.find((s) => s.id === serviceId)?.current ?? null)
|
|
33
|
+
// Unreadable is not the same as failed: a blip must not end a deploy that
|
|
34
|
+
// is going fine, so this reads as "no answer yet" and the loop continues.
|
|
35
|
+
.catch(() => null);
|
|
36
|
+
/**
|
|
37
|
+
* Wait until a service is running, has failed, or the deadline passes.
|
|
38
|
+
*
|
|
39
|
+
* Three behaviours the previous loops did not have:
|
|
40
|
+
*
|
|
41
|
+
* A service that is ALREADY running returns immediately. `fleet deploy` on an
|
|
42
|
+
* unchanged service creates no new deployment, so waiting for a transition
|
|
43
|
+
* meant waiting for something that could not happen.
|
|
44
|
+
*
|
|
45
|
+
* The deadline triggers one final authoritative read before failing. The bug
|
|
46
|
+
* this file exists for was a state change three seconds the wrong side of the
|
|
47
|
+
* deadline; a poll loop that gives up without looking one more time will always
|
|
48
|
+
* be able to miss by a second.
|
|
49
|
+
*
|
|
50
|
+
* And a timeout is reported as a timeout, carrying the last status seen —
|
|
51
|
+
* never as "has not reported running", which reads as a verdict on the service
|
|
52
|
+
* rather than on the waiting.
|
|
53
|
+
*/
|
|
54
|
+
export async function waitForRunning(fleetId, serviceId, opts = {}) {
|
|
55
|
+
const timeoutMs = opts.timeoutMs ?? DEPLOY_READY_TIMEOUT_MS;
|
|
56
|
+
const pollMs = opts.pollMs ?? 2000;
|
|
57
|
+
const currentOf = opts.read ?? liveStatus;
|
|
58
|
+
const startedAt = Date.now();
|
|
59
|
+
const deadline = startedAt + timeoutMs;
|
|
60
|
+
let last = null;
|
|
61
|
+
// Before waiting at all. A deploy that changed nothing has nothing to wait
|
|
62
|
+
// for, and the desired state is already the current one.
|
|
63
|
+
const already = await currentOf(fleetId, serviceId);
|
|
64
|
+
if (already?.status === 'running')
|
|
65
|
+
return { state: 'running' };
|
|
66
|
+
while (Date.now() < deadline) {
|
|
67
|
+
await opts.onPoll?.(last, Date.now() - startedAt);
|
|
68
|
+
const current = await currentOf(fleetId, serviceId);
|
|
69
|
+
last = current?.status ?? last;
|
|
70
|
+
if (current?.status === 'running')
|
|
71
|
+
return { state: 'running' };
|
|
72
|
+
if (current?.status === 'failed') {
|
|
73
|
+
return { state: 'failed', reason: current.failureReason ?? null };
|
|
74
|
+
}
|
|
75
|
+
await sleep(pollMs);
|
|
76
|
+
}
|
|
77
|
+
// One last look. The deadline is a decision to stop waiting, not evidence
|
|
78
|
+
// about the service, and the two are only ever a poll interval apart.
|
|
79
|
+
const settled = await currentOf(fleetId, serviceId);
|
|
80
|
+
if (settled?.status === 'running')
|
|
81
|
+
return { state: 'running' };
|
|
82
|
+
if (settled?.status === 'failed') {
|
|
83
|
+
return { state: 'failed', reason: settled.failureReason ?? null };
|
|
84
|
+
}
|
|
85
|
+
return { state: 'timeout', last: settled?.status ?? last, elapsedMs: Date.now() - startedAt };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The same wait, as an exception for callers that want one.
|
|
89
|
+
*
|
|
90
|
+
* Keeps the outcome type available to anything that would rather branch than
|
|
91
|
+
* catch, while giving the two commands the single line they had before.
|
|
92
|
+
*/
|
|
93
|
+
export async function requireRunning(fleetId, serviceId, name, opts = {}) {
|
|
94
|
+
const outcome = await waitForRunning(fleetId, serviceId, opts);
|
|
95
|
+
if (outcome.state === 'running')
|
|
96
|
+
return;
|
|
97
|
+
if (outcome.state === 'failed') {
|
|
98
|
+
throw new CliError(`"${name}" did not start${outcome.reason ? `: ${outcome.reason}` : '.'} ` +
|
|
99
|
+
`\`fleet deployments ${name}\` has the reason.`, EXIT.healthCheckFailed);
|
|
100
|
+
}
|
|
101
|
+
const minutes = Math.round(outcome.elapsedMs / 60_000);
|
|
102
|
+
throw new CliError(`Stopped watching "${name}" after ${minutes} minute${minutes === 1 ? '' : 's'}; ` +
|
|
103
|
+
`it was last ${outcome.last ?? 'not reporting a status'}. The deploy may still be running — ` +
|
|
104
|
+
`\`fleet deployments ${name}\` has the current state.`, EXIT.healthCheckFailed);
|
|
105
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -77,6 +77,10 @@ const OPTIONS = [
|
|
|
77
77
|
// Documented because it silently did nothing on `up` and `deploy` for a long
|
|
78
78
|
// time, and a flag that validates but is ignored is worse than one that errors.
|
|
79
79
|
['--node <name>', 'Deploy onto this node, or say why the service cannot go there'],
|
|
80
|
+
// Needed since a service became (fleet, project, name): two projects may
|
|
81
|
+
// both have a "backend", and picking the first match silently acts on
|
|
82
|
+
// somebody else's.
|
|
83
|
+
['--project <name>', 'Disambiguate when two projects have a service of the same name'],
|
|
80
84
|
['--api <url>', 'Control plane URL (default: saved profile)'],
|
|
81
85
|
['--json', 'Machine-readable output on stdout'],
|
|
82
86
|
['--plan, --dry-run', 'Show the deploy placement plan without changing anything'],
|