@xano/cli 1.0.6-beta.7 → 1.0.6-beta.9
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.
|
@@ -16,10 +16,20 @@ export default class EphemeralPush extends BaseCommand {
|
|
|
16
16
|
sync: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
17
|
transaction: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
18
18
|
truncate: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
19
|
+
wait: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
20
|
+
'wait-timeout': import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
|
|
19
21
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
20
22
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
21
23
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
22
24
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
23
25
|
};
|
|
24
26
|
run(): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Poll GET .../tenant/{name}/microservice until auto microservices settle.
|
|
29
|
+
* Renders a one-line-per-microservice progress view that updates in place, then
|
|
30
|
+
* exits non-zero if any auto microservice failed or the wait timed out.
|
|
31
|
+
*/
|
|
32
|
+
private waitForDeploy;
|
|
33
|
+
/** Number of lines the last progress render wrote (for in-place TTY updates). */
|
|
34
|
+
private lastRenderLines;
|
|
25
35
|
}
|
|
@@ -2,6 +2,7 @@ import { Args, Flags } from '@oclif/core';
|
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
4
|
import BaseCommand from '../../../base-command.js';
|
|
5
|
+
import { isAwaited, waitForMicroservices, } from '../../../utils/microservice-wait.js';
|
|
5
6
|
import { executePush } from '../../../utils/multidoc-push.js';
|
|
6
7
|
export default class EphemeralPush extends BaseCommand {
|
|
7
8
|
static args = {
|
|
@@ -88,6 +89,17 @@ Skip preview and push immediately
|
|
|
88
89
|
description: '[CRITICAL] STOP and confirm with the user; this truncates live tables before importing.',
|
|
89
90
|
required: false,
|
|
90
91
|
}),
|
|
92
|
+
wait: Flags.boolean({
|
|
93
|
+
default: false,
|
|
94
|
+
description: 'After pushing, wait for auto-deployed microservices (tenant_deploy="auto") to become ready. Exits non-zero if any fails to deploy or the wait times out. Ignored with --dry-run.',
|
|
95
|
+
required: false,
|
|
96
|
+
}),
|
|
97
|
+
'wait-timeout': Flags.integer({
|
|
98
|
+
default: 300,
|
|
99
|
+
description: 'Seconds to wait for microservices to become ready when --wait is set (default 300).',
|
|
100
|
+
min: 1,
|
|
101
|
+
required: false,
|
|
102
|
+
}),
|
|
91
103
|
workspace: Flags.string({
|
|
92
104
|
char: 'w',
|
|
93
105
|
description: 'Workspace ID (uses profile workspace if not provided)',
|
|
@@ -140,5 +152,70 @@ Skip preview and push immediately
|
|
|
140
152
|
inputDir,
|
|
141
153
|
verboseFetch: this.verboseFetch.bind(this),
|
|
142
154
|
}, target, pushFlags);
|
|
155
|
+
// --wait: after a real push, poll the tenant's live microservice status until
|
|
156
|
+
// every auto-deployed microservice (tenant_deploy="auto") is ready or the
|
|
157
|
+
// wait times out. Skipped on --dry-run (nothing was deployed).
|
|
158
|
+
if (flags.wait && !flags['dry-run']) {
|
|
159
|
+
await this.waitForDeploy(baseUrl, profile.access_token, tenantName, flags['wait-timeout'], flags.verbose);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Poll GET .../tenant/{name}/microservice until auto microservices settle.
|
|
164
|
+
* Renders a one-line-per-microservice progress view that updates in place, then
|
|
165
|
+
* exits non-zero if any auto microservice failed or the wait timed out.
|
|
166
|
+
*/
|
|
167
|
+
async waitForDeploy(baseUrl, accessToken, tenantName, timeoutSeconds, verbose) {
|
|
168
|
+
this.log('');
|
|
169
|
+
this.log(`Waiting for microservices to deploy (timeout ${timeoutSeconds}s)…`);
|
|
170
|
+
const icon = (e) => {
|
|
171
|
+
if (!isAwaited(e))
|
|
172
|
+
return '➖';
|
|
173
|
+
switch (e.status) {
|
|
174
|
+
case 'error': {
|
|
175
|
+
return '❌';
|
|
176
|
+
}
|
|
177
|
+
case 'ok': {
|
|
178
|
+
return '✅';
|
|
179
|
+
}
|
|
180
|
+
default: {
|
|
181
|
+
return '⏳';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const line = (e) => {
|
|
186
|
+
const detail = isAwaited(e) ? e.detail || e.status : `skipped (${e.tenant_deploy ?? 'manual'})`;
|
|
187
|
+
return ` ${icon(e)} ${e.name.padEnd(24)} ${detail}`;
|
|
188
|
+
};
|
|
189
|
+
const result = await waitForMicroservices({
|
|
190
|
+
accessToken,
|
|
191
|
+
onPoll: (entries) => {
|
|
192
|
+
// Re-render the block: move the cursor up over the previous render (if any)
|
|
193
|
+
// and rewrite. Falls back to plain appends when not a TTY.
|
|
194
|
+
if (process.stdout.isTTY && this.lastRenderLines > 0) {
|
|
195
|
+
process.stdout.write(`[${this.lastRenderLines}A[0J`);
|
|
196
|
+
}
|
|
197
|
+
const lines = entries.map((e) => line(e));
|
|
198
|
+
for (const l of lines)
|
|
199
|
+
this.log(l);
|
|
200
|
+
this.lastRenderLines = process.stdout.isTTY ? lines.length : 0;
|
|
201
|
+
},
|
|
202
|
+
statusUrl: `${baseUrl}/microservice`,
|
|
203
|
+
timeoutMs: timeoutSeconds * 1000,
|
|
204
|
+
verbose,
|
|
205
|
+
verboseFetch: this.verboseFetch.bind(this),
|
|
206
|
+
});
|
|
207
|
+
const awaited = result.entries.filter((e) => isAwaited(e));
|
|
208
|
+
const ready = awaited.filter((e) => e.status === 'ok').length;
|
|
209
|
+
if (result.timedOut) {
|
|
210
|
+
this.error(`Timed out after ${timeoutSeconds}s waiting for microservices (${ready}/${awaited.length} ready). ` +
|
|
211
|
+
`Check status with the Xano dashboard or re-run with a larger --wait-timeout.`);
|
|
212
|
+
}
|
|
213
|
+
if (result.hadError) {
|
|
214
|
+
const failed = awaited.filter((e) => e.status === 'error').map((e) => `${e.name} (${e.detail || 'error'})`);
|
|
215
|
+
this.error(`Microservice deploy failed: ${failed.join(', ')}`);
|
|
216
|
+
}
|
|
217
|
+
this.log(`All microservices ready (${ready}/${awaited.length}).`);
|
|
143
218
|
}
|
|
219
|
+
/** Number of lines the last progress render wrote (for in-place TTY updates). */
|
|
220
|
+
lastRenderLines = 0;
|
|
144
221
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll a tenant's microservice deployment status until every auto-deployed
|
|
3
|
+
* microservice settles (ok/error) or a timeout elapses. Backs `ephemeral push
|
|
4
|
+
* --wait`: after a multidoc push auto-deploys microservices (tenant_deploy=
|
|
5
|
+
* "auto"), the CLI waits here for them to become Ready and reports the outcome.
|
|
6
|
+
*
|
|
7
|
+
* The backend route (GET .../tenant/{name}/microservice, statement
|
|
8
|
+
* mvp:microservice_status_all) returns one entry per microservice with a live
|
|
9
|
+
* k8s readiness read. `manual`/`disabled` microservices are reported but not
|
|
10
|
+
* waited on (they were never deployed by the push).
|
|
11
|
+
*/
|
|
12
|
+
/** One microservice's status as returned by the tenant status route. */
|
|
13
|
+
export interface MicroserviceStatusEntry {
|
|
14
|
+
desired?: number;
|
|
15
|
+
detail?: string;
|
|
16
|
+
microservice_id: number;
|
|
17
|
+
name: string;
|
|
18
|
+
ready?: number;
|
|
19
|
+
status: string;
|
|
20
|
+
tenant_deploy?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface WaitOptions {
|
|
23
|
+
/** Bearer token for the authed status GET. */
|
|
24
|
+
accessToken: string;
|
|
25
|
+
/** Called once per poll with the latest entries, for progress rendering. */
|
|
26
|
+
onPoll?: (entries: MicroserviceStatusEntry[], elapsedMs: number) => void;
|
|
27
|
+
/** Poll cadence in ms (default 2000). */
|
|
28
|
+
pollIntervalMs?: number;
|
|
29
|
+
/** Absolute URL of the tenant microservice status route. */
|
|
30
|
+
statusUrl: string;
|
|
31
|
+
/** Overall wait budget in ms (default 300000 = 300s). */
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
/** Authed fetch (command.verboseFetch bound), so TLS/dispatcher settings apply. */
|
|
34
|
+
verboseFetch: (url: string, options: RequestInit, verbose: boolean, authToken?: string) => Promise<Response>;
|
|
35
|
+
/** Verbose request logging passthrough. */
|
|
36
|
+
verbose?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface WaitResult {
|
|
39
|
+
/** The final status entries observed. */
|
|
40
|
+
entries: MicroserviceStatusEntry[];
|
|
41
|
+
/** True if any awaited (auto) microservice ended in "error". */
|
|
42
|
+
hadError: boolean;
|
|
43
|
+
/** True if the wait budget elapsed before all awaited microservices settled. */
|
|
44
|
+
timedOut: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** An awaited microservice is one that was auto-deployed and is not skipped/disabled. */
|
|
47
|
+
export declare function isAwaited(entry: MicroserviceStatusEntry): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Poll until all awaited microservices settle or the timeout elapses. Transient
|
|
50
|
+
* poll failures (network blips, a brief 5xx while a pod restarts) are tolerated —
|
|
51
|
+
* they don't abort the wait; only the overall timeout does.
|
|
52
|
+
*/
|
|
53
|
+
export declare function waitForMicroservices(opts: WaitOptions): Promise<WaitResult>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll a tenant's microservice deployment status until every auto-deployed
|
|
3
|
+
* microservice settles (ok/error) or a timeout elapses. Backs `ephemeral push
|
|
4
|
+
* --wait`: after a multidoc push auto-deploys microservices (tenant_deploy=
|
|
5
|
+
* "auto"), the CLI waits here for them to become Ready and reports the outcome.
|
|
6
|
+
*
|
|
7
|
+
* The backend route (GET .../tenant/{name}/microservice, statement
|
|
8
|
+
* mvp:microservice_status_all) returns one entry per microservice with a live
|
|
9
|
+
* k8s readiness read. `manual`/`disabled` microservices are reported but not
|
|
10
|
+
* waited on (they were never deployed by the push).
|
|
11
|
+
*/
|
|
12
|
+
/** Statuses that mean an awaited microservice has settled (no longer in flight). */
|
|
13
|
+
const SETTLED = new Set(['ok', 'error']);
|
|
14
|
+
/** Statuses that mean we don't wait on this microservice at all. */
|
|
15
|
+
const NOT_AWAITED = new Set(['skipped', 'disabled']);
|
|
16
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
17
|
+
/** An awaited microservice is one that was auto-deployed and is not skipped/disabled. */
|
|
18
|
+
export function isAwaited(entry) {
|
|
19
|
+
return !NOT_AWAITED.has(entry.status) && (entry.tenant_deploy ?? 'auto') !== 'manual';
|
|
20
|
+
}
|
|
21
|
+
/** True once every awaited microservice has settled (ok/error). */
|
|
22
|
+
function allSettled(entries) {
|
|
23
|
+
const awaited = entries.filter((e) => isAwaited(e));
|
|
24
|
+
// No auto microservices at all → nothing to wait for.
|
|
25
|
+
if (awaited.length === 0)
|
|
26
|
+
return true;
|
|
27
|
+
return awaited.every((e) => SETTLED.has(e.status));
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Fetch the status route once. Throws on a non-2xx so the caller can decide
|
|
31
|
+
* whether a transient failure is fatal (it isn't — we keep polling).
|
|
32
|
+
*/
|
|
33
|
+
async function fetchStatus(opts) {
|
|
34
|
+
const res = await opts.verboseFetch(opts.statusUrl, { headers: { Authorization: `Bearer ${opts.accessToken}` }, method: 'GET' }, opts.verbose ?? false, opts.accessToken);
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
throw new Error(`status ${res.status} ${res.statusText}`);
|
|
37
|
+
}
|
|
38
|
+
const body = (await res.json());
|
|
39
|
+
if (!Array.isArray(body)) {
|
|
40
|
+
throw new Error('unexpected status response shape');
|
|
41
|
+
}
|
|
42
|
+
return body;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Poll until all awaited microservices settle or the timeout elapses. Transient
|
|
46
|
+
* poll failures (network blips, a brief 5xx while a pod restarts) are tolerated —
|
|
47
|
+
* they don't abort the wait; only the overall timeout does.
|
|
48
|
+
*/
|
|
49
|
+
export async function waitForMicroservices(opts) {
|
|
50
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 2000;
|
|
51
|
+
const timeoutMs = opts.timeoutMs ?? 300000;
|
|
52
|
+
const start = Date.now();
|
|
53
|
+
let entries = [];
|
|
54
|
+
let consecutiveErrors = 0;
|
|
55
|
+
// Poll loop. We always do at least one fetch so `--wait` reports even when the
|
|
56
|
+
// deployment already settled between the push returning and the first poll.
|
|
57
|
+
for (;;) {
|
|
58
|
+
const elapsed = Date.now() - start;
|
|
59
|
+
try {
|
|
60
|
+
entries = await fetchStatus(opts);
|
|
61
|
+
consecutiveErrors = 0;
|
|
62
|
+
opts.onPoll?.(entries, elapsed);
|
|
63
|
+
if (allSettled(entries)) {
|
|
64
|
+
return { entries, hadError: entries.some((e) => isAwaited(e) && e.status === 'error'), timedOut: false };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// A transient status read failure is non-fatal — keep polling until the
|
|
69
|
+
// overall timeout. (Mirrors the frontend poller's tolerate-with-cap.) We
|
|
70
|
+
// don't surface each blip; the timeout is the real failure signal.
|
|
71
|
+
consecutiveErrors++;
|
|
72
|
+
}
|
|
73
|
+
if (Date.now() - start + pollIntervalMs > timeoutMs) {
|
|
74
|
+
// Out of budget. Report whatever we last saw as timed out.
|
|
75
|
+
return {
|
|
76
|
+
entries,
|
|
77
|
+
hadError: entries.some((e) => isAwaited(e) && e.status === 'error'),
|
|
78
|
+
timedOut: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
await sleep(pollIntervalMs);
|
|
82
|
+
}
|
|
83
|
+
}
|