@myapihq/cli 2.31.0 → 2.31.2
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/commands/container-deploy-failure.test.d.ts +1 -0
- package/dist/commands/container-deploy-failure.test.js +49 -0
- package/dist/commands/container.d.ts +1 -0
- package/dist/commands/container.js +34 -1
- package/dist/flags.d.ts +1 -0
- package/dist/flags.js +24 -0
- package/dist/flags.test.js +27 -1
- package/dist/index.js +2 -3
- package/dist/utils.d.ts +9 -2
- package/dist/utils.js +4 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// A failed deploy has to say which half failed.
|
|
2
|
+
//
|
|
3
|
+
// One fixed sentence covered every failure: "Build/deploy failed — check
|
|
4
|
+
// `myapi container logs <id>`". ImmoPilot read thirty lines of healthy nginx
|
|
5
|
+
// after a ROLLOUT failure — the image had built, served two 200s to our own
|
|
6
|
+
// startup probes, then taken a SIGTERM — and a second deploy of the same code
|
|
7
|
+
// succeeded unchanged. The sentence sent them looking for a defect in code that
|
|
8
|
+
// was fine.
|
|
9
|
+
//
|
|
10
|
+
// I hit the same message the day before, on a container the probe had already
|
|
11
|
+
// deleted, so the logs it named did not exist either.
|
|
12
|
+
//
|
|
13
|
+
// The platform now returns error, error_stage and error_retryable, so the CLI
|
|
14
|
+
// can stop guessing.
|
|
15
|
+
import { describe, it, expect } from 'vitest';
|
|
16
|
+
import { describeDeployFailure } from './container.js';
|
|
17
|
+
const ID = 'c1';
|
|
18
|
+
const base = { id: ID };
|
|
19
|
+
describe('describeDeployFailure', () => {
|
|
20
|
+
it('leads with what the platform said, not our paraphrase', () => {
|
|
21
|
+
const out = describeDeployFailure({ ...base, error: 'Deployment failed after the image built.', error_stage: 'deploy' }, ID);
|
|
22
|
+
expect(out.split('\n')[0]).toBe('Deployment failed after the image built.');
|
|
23
|
+
});
|
|
24
|
+
it('on a rollout failure, says their application logs are the wrong place', () => {
|
|
25
|
+
// The whole point. "Check the logs" is what cost a customer a deploy.
|
|
26
|
+
const out = describeDeployFailure({ ...base, error_stage: 'deploy' }, ID);
|
|
27
|
+
expect(out).toMatch(/not the place to look/i);
|
|
28
|
+
expect(out).not.toContain(`myapi container logs ${ID}`);
|
|
29
|
+
});
|
|
30
|
+
it('on a build failure, sends them to the build logs', () => {
|
|
31
|
+
const out = describeDeployFailure({ ...base, error_stage: 'build' }, ID);
|
|
32
|
+
expect(out).toContain(`myapi container build-logs ${ID}`);
|
|
33
|
+
});
|
|
34
|
+
it('says so when the failure is ours and retrying is reasonable', () => {
|
|
35
|
+
const out = describeDeployFailure({ ...base, error_stage: 'deploy', error_retryable: true }, ID);
|
|
36
|
+
expect(out).toMatch(/retrying is reasonable/i);
|
|
37
|
+
});
|
|
38
|
+
it('does not invite a retry when the failure is theirs', () => {
|
|
39
|
+
// Telling someone to retry a broken build wastes four minutes per attempt.
|
|
40
|
+
const out = describeDeployFailure({ ...base, error_stage: 'build', error_retryable: false }, ID);
|
|
41
|
+
expect(out).not.toMatch(/retrying is reasonable/i);
|
|
42
|
+
});
|
|
43
|
+
it('offers both logs when the platform did not say which stage', () => {
|
|
44
|
+
// Older backends, and any future status that predates the field.
|
|
45
|
+
const out = describeDeployFailure(base, ID);
|
|
46
|
+
expect(out).toContain(`build-logs ${ID}`);
|
|
47
|
+
expect(out).toContain(`logs ${ID}`);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -13,6 +13,7 @@ export declare function create(nameArg: string | undefined, flags: Flags): Promi
|
|
|
13
13
|
export declare function list(flags: Flags): Promise<void>;
|
|
14
14
|
export declare function get(id: string, flags: Flags): Promise<void>;
|
|
15
15
|
export declare function healthCheck(id: string, flags: Flags): Promise<void>;
|
|
16
|
+
export declare function describeDeployFailure(c: sdkContainer.Container, id: string): string;
|
|
16
17
|
export declare function del(id: string, flags: Flags): Promise<void>;
|
|
17
18
|
export declare function _checkProbePath(hc: string): string;
|
|
18
19
|
export declare function _isTarball(p: string): boolean;
|
|
@@ -277,6 +277,39 @@ export async function healthCheck(id, flags) {
|
|
|
277
277
|
? ` The runtime now waits for ${value} to answer before sending traffic to a new revision.`
|
|
278
278
|
: ' No startup probe: a new revision takes traffic as soon as the runtime reports it started.');
|
|
279
279
|
}
|
|
280
|
+
// What to say when a deploy fails, and where to send them.
|
|
281
|
+
//
|
|
282
|
+
// One fixed sentence used to cover every failure: "Build/deploy failed — check
|
|
283
|
+
// `myapi container logs <id>`". A customer read thirty lines of healthy nginx
|
|
284
|
+
// after a ROLLOUT failure — the image had built, served two 200s to our own
|
|
285
|
+
// startup probes, then taken a SIGTERM — and a second deploy of the same code
|
|
286
|
+
// succeeded unchanged. The message sent them to look for a defect in code that
|
|
287
|
+
// was fine.
|
|
288
|
+
//
|
|
289
|
+
// The platform now says which stage failed and whether it is worth retrying,
|
|
290
|
+
// so the CLI can stop guessing. `build` is their image; `deploy` is our rollout
|
|
291
|
+
// of an image that built, which is the case where their application logs are
|
|
292
|
+
// the wrong place to look.
|
|
293
|
+
export function describeDeployFailure(c, id) {
|
|
294
|
+
const lines = [c.error?.trim() || 'The deploy failed.'];
|
|
295
|
+
if (c.error_stage === 'build') {
|
|
296
|
+
lines.push(` Your image did not build: myapi container build-logs ${id}`);
|
|
297
|
+
}
|
|
298
|
+
else if (c.error_stage === 'deploy') {
|
|
299
|
+
// Said explicitly, because "check the logs" is what cost the customer a
|
|
300
|
+
// deploy: the image built and ran, so the application logs look healthy.
|
|
301
|
+
lines.push(' The image built — this failed while we rolled it out, so your');
|
|
302
|
+
lines.push(' application logs will look healthy and are not the place to look.');
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
lines.push(` Build logs: myapi container build-logs ${id}`);
|
|
306
|
+
lines.push(` Runtime logs: myapi container logs ${id}`);
|
|
307
|
+
}
|
|
308
|
+
if (c.error_retryable) {
|
|
309
|
+
lines.push(' This one is ours and retrying is reasonable: myapi container deploy ' + id + ' --source <dir>');
|
|
310
|
+
}
|
|
311
|
+
return lines.join('\n');
|
|
312
|
+
}
|
|
280
313
|
export async function del(id, flags) {
|
|
281
314
|
const config = requireConfig();
|
|
282
315
|
const orgId = requireOrg(flags, config, 'myapi container delete <id> [--yes] [--org <id>]');
|
|
@@ -421,7 +454,7 @@ export async function deploy(id, image, flags) {
|
|
|
421
454
|
check: () => sdkContainer.getContainer(config.api_key, orgId, id),
|
|
422
455
|
isDone: (c) => c.status === 'active',
|
|
423
456
|
isFailed: (c) => c.status === 'build_error' || c.status === 'deploy_error',
|
|
424
|
-
failedMessage:
|
|
457
|
+
failedMessage: (c) => describeDeployFailure(c, id),
|
|
425
458
|
timeoutMs: 600_000,
|
|
426
459
|
intervalMs: 5_000,
|
|
427
460
|
});
|
package/dist/flags.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export type FlagType = 'string' | 'boolean' | 'number' | 'list';
|
|
2
2
|
export type FlagSchema = Record<string, FlagType>;
|
|
3
3
|
export declare const GLOBAL_FLAGS: FlagSchema;
|
|
4
|
+
export declare const DISPATCH_FLAGS: ReadonlySet<string>;
|
|
4
5
|
export interface ParsedArgs {
|
|
5
6
|
args: string[];
|
|
6
7
|
flags: Record<string, string | boolean | number>;
|
package/dist/flags.js
CHANGED
|
@@ -5,13 +5,37 @@
|
|
|
5
5
|
// utils.ts — adding a new value flag without registering it would silently
|
|
6
6
|
// turn it into a boolean (e.g. `--html '<p>'` becoming `flags.html === true`).
|
|
7
7
|
// Flags every command understands.
|
|
8
|
+
//
|
|
9
|
+
// `org` belongs here rather than in each command's own schema. Ten commands
|
|
10
|
+
// declared it individually and the other nineteen did not, so the same flag
|
|
11
|
+
// refused a duplicate on `myapi pixel` and silently kept the last value on
|
|
12
|
+
// `myapi funnel push` — see the duplicate check in parseFlags for why last-wins
|
|
13
|
+
// is the one outcome you cannot recover from. For the tenant selector it picks
|
|
14
|
+
// the organisation your write lands in, which makes it the worst flag in the
|
|
15
|
+
// CLI to resolve silently.
|
|
8
16
|
export const GLOBAL_FLAGS = {
|
|
9
17
|
help: 'boolean',
|
|
10
18
|
json: 'boolean',
|
|
11
19
|
verbose: 'boolean',
|
|
12
20
|
yes: 'boolean',
|
|
13
21
|
y: 'boolean',
|
|
22
|
+
version: 'boolean',
|
|
23
|
+
org: 'string',
|
|
14
24
|
};
|
|
25
|
+
// Short aliases are consumed as `-h` / `-v` / `-V` before the `--flag` branch,
|
|
26
|
+
// so they never appear as keys in `merged` and need no type — but they do land
|
|
27
|
+
// in `flags` (as help/version), so the dispatcher must still tolerate them.
|
|
28
|
+
const SHORT_ALIASES = ['h', 'v', 'V'];
|
|
29
|
+
// What the dispatcher tolerates on any command, derived from the list above so
|
|
30
|
+
// the two cannot disagree. They previously drifted: `--org` was missing here
|
|
31
|
+
// while being honoured, so the CLI announced "unknown flag --org (took value
|
|
32
|
+
// …)" and then switched organisation anyway. A user who believed the warning
|
|
33
|
+
// and dropped the flag would have written to whatever org the config pointed
|
|
34
|
+
// at (ImmoPilot, 2026-08-22).
|
|
35
|
+
export const DISPATCH_FLAGS = new Set([
|
|
36
|
+
...Object.keys(GLOBAL_FLAGS),
|
|
37
|
+
...SHORT_ALIASES,
|
|
38
|
+
]);
|
|
15
39
|
// `quiet` suppresses the unknown-flag note. The dispatcher parses twice — once
|
|
16
40
|
// to learn the command, once under that command's own schema — and only the
|
|
17
41
|
// second pass knows which flags are genuinely foreign, so the first must stay
|
package/dist/flags.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseFlags } from './flags.js';
|
|
2
|
+
import { parseFlags, GLOBAL_FLAGS, DISPATCH_FLAGS } from './flags.js';
|
|
3
3
|
const SCHEMA = {
|
|
4
4
|
org: 'string',
|
|
5
5
|
name: 'string',
|
|
@@ -190,3 +190,29 @@ describe('parseFlags — repeated flags are never silently collapsed', () => {
|
|
|
190
190
|
expect(flags.name).toBe('prod');
|
|
191
191
|
});
|
|
192
192
|
});
|
|
193
|
+
// ImmoPilot, 2026-08-22: `--org` was honoured while being announced as an
|
|
194
|
+
// unknown flag, because the dispatcher's tolerated-flag list and the parser's
|
|
195
|
+
// typed-flag list were maintained separately and disagreed. DISPATCH_FLAGS is
|
|
196
|
+
// now derived from GLOBAL_FLAGS, so this asserts the derivation holds rather
|
|
197
|
+
// than re-checking a hand-written list.
|
|
198
|
+
describe('global flag lists cannot drift', () => {
|
|
199
|
+
it('every globally-typed flag is tolerated by the dispatcher', () => {
|
|
200
|
+
for (const key of Object.keys(GLOBAL_FLAGS)) {
|
|
201
|
+
expect(DISPATCH_FLAGS.has(key)).toBe(true);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
it('org is typed globally, so every command refuses a duplicate', () => {
|
|
205
|
+
// Ten commands declared `org` in their own schema and got this guard; the
|
|
206
|
+
// other nineteen silently kept the last value. Parse with an empty schema
|
|
207
|
+
// to stand in for a command that declares nothing of its own.
|
|
208
|
+
expect(() => parseFlags(['--org', 'ORG_A', '--org', 'ORG_B'], {}, true)).toThrow(/--org was given more than once/);
|
|
209
|
+
});
|
|
210
|
+
it('a single --org still parses to its value under an empty schema', () => {
|
|
211
|
+
const { flags } = parseFlags(['--org', 'ORG_A'], {}, true);
|
|
212
|
+
expect(flags.org).toBe('ORG_A');
|
|
213
|
+
});
|
|
214
|
+
it('--org is not reported as a foreign flag', () => {
|
|
215
|
+
const { unknownFlags } = parseFlags(['--org', 'ORG_A'], {}, true);
|
|
216
|
+
expect(unknownFlags ?? []).not.toContain('--org');
|
|
217
|
+
});
|
|
218
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { MyApiError, setUserAgent, hq } from '@myapihq/sdk';
|
|
|
5
5
|
import { friendlyError } from './errors.js';
|
|
6
6
|
import { currentOrg, adoptSoleOrg } from './helpers.js';
|
|
7
7
|
import * as fs from 'fs';
|
|
8
|
-
import { parseFlags } from './flags.js';
|
|
8
|
+
import { parseFlags, DISPATCH_FLAGS } from './flags.js';
|
|
9
9
|
const pkgPath = new URL('../package.json', import.meta.url);
|
|
10
10
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
11
11
|
// Identify the CLI (and its version) on every outbound request. Without this
|
|
@@ -100,8 +100,7 @@ const COMMAND_SCHEMAS = {
|
|
|
100
100
|
webhook: webhookCmd.SCHEMA,
|
|
101
101
|
workflow: workflowCmd.SCHEMA,
|
|
102
102
|
};
|
|
103
|
-
// Flags the dispatcher itself consumes
|
|
104
|
-
const DISPATCH_FLAGS = new Set(['help', 'h', 'json', 'verbose', 'yes', 'y', 'version', 'v', 'V', 'org']);
|
|
103
|
+
// Flags the dispatcher itself consumes are derived from GLOBAL_FLAGS in flags.ts.
|
|
105
104
|
function warnForeignFlags(command, flags) {
|
|
106
105
|
if (!command || process.env.MYAPI_QUIET_UNKNOWN_FLAGS)
|
|
107
106
|
return;
|
package/dist/utils.d.ts
CHANGED
|
@@ -20,8 +20,15 @@ export interface PollOptions<T> {
|
|
|
20
20
|
intervalMs?: number;
|
|
21
21
|
/** Message shown via error() on timeout. Defaults to a generic phrase. */
|
|
22
22
|
timeoutMessage?: string;
|
|
23
|
-
/**
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Message shown via error() when isFailed returns true.
|
|
25
|
+
*
|
|
26
|
+
* A function when the useful message depends on WHY it failed. `container
|
|
27
|
+
* deploy` printed one fixed sentence for every failure, so a rollout problem
|
|
28
|
+
* and a broken build read identically — a customer spent a deploy reading
|
|
29
|
+
* healthy nginx logs because the sentence sent them there.
|
|
30
|
+
*/
|
|
31
|
+
failedMessage?: string | ((state: T) => string);
|
|
25
32
|
}
|
|
26
33
|
/**
|
|
27
34
|
* Generic spinner+poll helper. Used wherever the CLI kicks off a long-running
|
package/dist/utils.js
CHANGED
|
@@ -46,7 +46,10 @@ export async function pollJob(opts) {
|
|
|
46
46
|
}
|
|
47
47
|
if (opts.isFailed && opts.isFailed(state)) {
|
|
48
48
|
clearLine();
|
|
49
|
-
|
|
49
|
+
const msg = typeof opts.failedMessage === 'function'
|
|
50
|
+
? opts.failedMessage(state)
|
|
51
|
+
: opts.failedMessage;
|
|
52
|
+
error(msg ?? `${opts.label} failed`);
|
|
50
53
|
}
|
|
51
54
|
spinnerWrite(`\r${opts.label} ${spinnerFrame(i++)}`);
|
|
52
55
|
await sleep(intervalMs);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2.31.
|
|
4
|
+
"version": "2.31.2",
|
|
5
5
|
"description": "MyAPI command-line interface",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"lint:skills:strict": "node scripts/copy-skills.js && node scripts/lint-skills.js --strict"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@myapihq/sdk": "^2.31.
|
|
50
|
+
"@myapihq/sdk": "^2.31.2"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@types/node": "^25.6.0",
|