@myapihq/cli 2.8.0 → 2.10.0
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.d.ts +1 -0
- package/dist/commands/container.js +91 -48
- package/dist/commands/feedback.js +5 -4
- package/dist/commands/flag-reachability.test.js +48 -25
- package/dist/errors.js +6 -0
- package/dist/skills/my-container-api/SKILL.md +24 -18
- package/dist/skills/my-storage-api/SKILL.md +12 -6
- package/package.json +2 -2
|
@@ -17,6 +17,7 @@ export declare function deploy(id: string, image: string, flags: Flags): Promise
|
|
|
17
17
|
export declare function _parseSmoke(raw: string): sdkContainer.SmokeCheck;
|
|
18
18
|
export declare function revisions(id: string, flags: Flags): Promise<void>;
|
|
19
19
|
export declare function promote(id: string, revision: string | undefined, flags: Flags): Promise<void>;
|
|
20
|
+
export declare function rollback(id: string, flags: Flags): Promise<void>;
|
|
20
21
|
export declare function logs(id: string, flags: Flags): Promise<void>;
|
|
21
22
|
export declare function domain(id: string, domainArg: string | undefined, flags: Flags): Promise<void>;
|
|
22
23
|
export declare function buildLogs(id: string | undefined, flags: Flags): Promise<void>;
|
|
@@ -239,18 +239,6 @@ export async function deploy(id, image, flags) {
|
|
|
239
239
|
// write their own; a guard that silently passes makes them stop. Restore
|
|
240
240
|
// these the moment the upstream fix lands — see
|
|
241
241
|
// docs/cross-repo-prompts/backend-consolidated-2026-07-28.md.
|
|
242
|
-
if (flags['no-promote'] === true || typeof flags.smoke === 'string') {
|
|
243
|
-
const which = flags['no-promote'] === true ? '--no-promote' : '--smoke';
|
|
244
|
-
error(`${which} is not honoured yet, so this CLI refuses it rather than letting you believe a deploy was guarded.\n\n` +
|
|
245
|
-
'It shipped in 2.7.0 and does not work end to end on either deploy path:\n' +
|
|
246
|
-
' --source the API accepts only the tarball on that path; the flag never reaches it\n' +
|
|
247
|
-
' <image-ref> the API accepts the flag, then fails while moving traffic\n\n' +
|
|
248
|
-
'Until it lands, the safe sequence is:\n' +
|
|
249
|
-
` 1. deploy to a non-production container first\n` +
|
|
250
|
-
` 2. check it yourself (curl for a string only a real build emits)\n` +
|
|
251
|
-
` 3. deploy the same image to production\n\n` +
|
|
252
|
-
'Reported upstream; this message goes away when the flag works.');
|
|
253
|
-
}
|
|
254
242
|
const source = typeof flags.source === 'string' ? flags.source : undefined;
|
|
255
243
|
// --image is an alias for the positional image ref.
|
|
256
244
|
if (!image && typeof flags.image === 'string')
|
|
@@ -258,6 +246,33 @@ export async function deploy(id, image, flags) {
|
|
|
258
246
|
if (source && image) {
|
|
259
247
|
error('Pass either an image ref or --source, not both.');
|
|
260
248
|
}
|
|
249
|
+
// Built once, passed to BOTH branches. The original bug was building these
|
|
250
|
+
// inside the image branch only, so a --source deploy dropped them silently.
|
|
251
|
+
const deployOpts = {};
|
|
252
|
+
if (flags['no-promote'] === true)
|
|
253
|
+
deployOpts.promote = false;
|
|
254
|
+
if (typeof flags.smoke === 'string')
|
|
255
|
+
deployOpts.smoke = _parseSmoke(flags.smoke);
|
|
256
|
+
// On a container's FIRST deploy there is no earlier revision to hold
|
|
257
|
+
// traffic, so the platform runs the assertion against the LIVE url — a
|
|
258
|
+
// failing build is already serving when you are told it failed. Say so
|
|
259
|
+
// before the deploy rather than after, because the remedy differs: on any
|
|
260
|
+
// later deploy a failure is contained, and here it is not.
|
|
261
|
+
if (deployOpts.smoke || deployOpts.promote === false) {
|
|
262
|
+
try {
|
|
263
|
+
const existing = await sdkContainer.listRevisions(config.api_key, orgId, id);
|
|
264
|
+
if (existing.length === 0) {
|
|
265
|
+
info('Note: this container has no earlier revision, so there is nothing to hold');
|
|
266
|
+
info('traffic while the new one is checked. A failing build WILL be serving.');
|
|
267
|
+
info('Deploy to a non-production container first if that matters.');
|
|
268
|
+
info('');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch { /* advisory only — never block a deploy on it */ }
|
|
272
|
+
}
|
|
273
|
+
if (deployOpts.promote === false && deployOpts.smoke) {
|
|
274
|
+
error('--smoke already withholds traffic until the assertion passes, then promotes.\nUse one or the other: --smoke to verify-and-promote, --no-promote to hold the revision back.');
|
|
275
|
+
}
|
|
261
276
|
// ── Source-build path (async) ───────────────────────────────────────────
|
|
262
277
|
if (source) {
|
|
263
278
|
let tarball;
|
|
@@ -281,7 +296,7 @@ export async function deploy(id, image, flags) {
|
|
|
281
296
|
else {
|
|
282
297
|
error(`--source must be a directory or a .tar/.tar.gz/.tgz archive — got ${source}`);
|
|
283
298
|
}
|
|
284
|
-
const start = await sdkContainer.deployContainerSource(config.api_key, orgId, id, tarball, filename);
|
|
299
|
+
const start = await sdkContainer.deployContainerSource(config.api_key, orgId, id, tarball, filename, deployOpts);
|
|
285
300
|
if (flags.json && start.status !== 'building') {
|
|
286
301
|
printJson(start);
|
|
287
302
|
return;
|
|
@@ -320,10 +335,7 @@ export async function deploy(id, image, flags) {
|
|
|
320
335
|
// ── Pre-built image path (sync) ─────────────────────────────────────────
|
|
321
336
|
if (!image)
|
|
322
337
|
error('Missing image ref.\nUsage: myapi container deploy <id> <image-ref>\n or: myapi container deploy <id> --source <dir|tar>\n\n→ <image-ref> is a pre-built container image (e.g. a registry path).');
|
|
323
|
-
|
|
324
|
-
// until the upstream fix lands. The SDK still carries them so the wiring is
|
|
325
|
-
// one commit away, and sdk-container.test.ts keeps them covered.
|
|
326
|
-
const result = await sdkContainer.deployContainer(config.api_key, orgId, id, image);
|
|
338
|
+
const result = await sdkContainer.deployContainer(config.api_key, orgId, id, image, deployOpts);
|
|
327
339
|
if (flags.json) {
|
|
328
340
|
printJson(result);
|
|
329
341
|
return;
|
|
@@ -331,9 +343,18 @@ export async function deploy(id, image, flags) {
|
|
|
331
343
|
// An unpromoted revision must NOT read like a completed deploy. A response
|
|
332
344
|
// that looked the same either way is how an agent concludes it has shipped
|
|
333
345
|
// when it has not — the original outage in miniature.
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
//
|
|
346
|
+
// Read `promoted` rather than assuming --no-promote was honoured. On a
|
|
347
|
+
// container's FIRST deploy there is nothing already serving to hold traffic,
|
|
348
|
+
// so the platform promotes anyway and says so — reporting "not serving"
|
|
349
|
+
// there would be the lie this flag exists to prevent.
|
|
350
|
+
if (flags['no-promote'] === true && result.promoted !== false) {
|
|
351
|
+
success(`Deployed container ${id} (revision ${result.revision_id})`);
|
|
352
|
+
info('');
|
|
353
|
+
info('Note: --no-promote was NOT applied. This container had nothing already');
|
|
354
|
+
info('serving, so withholding traffic would have left it answering nothing.');
|
|
355
|
+
info(`URL: ${result.url}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
337
358
|
if (result.promoted === false) {
|
|
338
359
|
success(`Revision ${result.revision_id} built — NOT serving traffic`);
|
|
339
360
|
info(`Test it: ${result.revision_url ?? '(no revision URL returned)'}`);
|
|
@@ -417,13 +438,14 @@ export async function revisions(id, flags) {
|
|
|
417
438
|
// state.
|
|
418
439
|
//
|
|
419
440
|
// Say so rather than render a table that reads as "nothing is live".
|
|
441
|
+
// The 0%-everywhere reporting bug was fixed upstream on 2026-07-28 (a v2
|
|
442
|
+
// traffic target of type LATEST carries no revision name, so 100% was filed
|
|
443
|
+
// under ""). Keeping a narrower check: all-zero on an active container is
|
|
444
|
+
// still worth flagging, it is just no longer expected.
|
|
420
445
|
if (revs.length > 0 && revs.every(r => !r.serving && !r.traffic_percent) && container?.status === 'active') {
|
|
421
446
|
info('');
|
|
422
|
-
info('Note:
|
|
423
|
-
info('
|
|
424
|
-
info('today, so this is not limited to older ones — an earlier version of this message');
|
|
425
|
-
info('said redeploying fixes it, which was wrong.');
|
|
426
|
-
info('promote depends on this data and currently fails. Reported upstream.');
|
|
447
|
+
info('Note: no revision reports any traffic while this container is active.');
|
|
448
|
+
info('That should not happen — report it rather than trusting the column.');
|
|
427
449
|
}
|
|
428
450
|
}
|
|
429
451
|
// promote moves all traffic to one revision. Omitting the revision rolls back
|
|
@@ -447,16 +469,25 @@ export async function promote(id, revision, flags) {
|
|
|
447
469
|
if (res.message)
|
|
448
470
|
info(res.message);
|
|
449
471
|
}
|
|
450
|
-
//
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
'
|
|
459
|
-
|
|
472
|
+
// rollback moves traffic to the previous ready revision. The API models it as
|
|
473
|
+
// `promote` with no revision named — one operation, two targets — but
|
|
474
|
+
// `rollback` is the word someone types during an incident, so it is a verb
|
|
475
|
+
// here even though the SDK has a single function.
|
|
476
|
+
export async function rollback(id, flags) {
|
|
477
|
+
const config = requireConfig();
|
|
478
|
+
const orgId = requireOrg(flags, config, 'myapi container rollback <id> [--org <id>]');
|
|
479
|
+
if (!id)
|
|
480
|
+
error('Missing id.\nUsage: myapi container rollback <id>');
|
|
481
|
+
const res = await sdkContainer.promoteRevision(config.api_key, orgId, id);
|
|
482
|
+
if (flags.json) {
|
|
483
|
+
printJson(res);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
success(`Rolled back ${id} to the previous ready revision`);
|
|
487
|
+
if (res.serving)
|
|
488
|
+
info(`Now serving: ${res.serving}`);
|
|
489
|
+
if (res.message)
|
|
490
|
+
info(res.message);
|
|
460
491
|
}
|
|
461
492
|
// logs prints the container's recent runtime logs, newest first. By default
|
|
462
493
|
// this is the container's own stdout/stderr; --scope all adds the platform
|
|
@@ -565,19 +596,33 @@ Two ways to deploy:
|
|
|
565
596
|
built server-side (typically ~4 minutes), then deployed.
|
|
566
597
|
Asynchronous — the CLI polls until it's live.
|
|
567
598
|
|
|
568
|
-
|
|
599
|
+
Options:
|
|
600
|
+
--no-promote Build the revision without giving it traffic, on either
|
|
601
|
+
deploy path. Prints a revision URL to test at, then:
|
|
602
|
+
myapi container promote <id> <revision>
|
|
569
603
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
A guard that silently passes is worse than no guard.
|
|
604
|
+
On a container's FIRST deploy there is nothing already
|
|
605
|
+
serving, so traffic is NOT withheld and the output says so.
|
|
573
606
|
|
|
574
|
-
|
|
575
|
-
|
|
607
|
+
--smoke '<assertion>'
|
|
608
|
+
Deploy, assert against the new revision, and promote it ONLY
|
|
609
|
+
if the assertion holds. A failure leaves the previous
|
|
610
|
+
revision serving and returns SMOKE_FAILED with a revision URL
|
|
611
|
+
to inspect. Works on both deploy paths.
|
|
612
|
+
|
|
613
|
+
Grammar: [GET|HEAD] [/path] [status N] [contains TEXT]
|
|
614
|
+
Assert on CONTENT — "returns 200" is true of a placeholder
|
|
615
|
+
page too, which is how one reached production and stayed.
|
|
576
616
|
|
|
577
617
|
Examples:
|
|
578
618
|
myapi container deploy <id> registry.example.com/my-app:v2
|
|
579
619
|
myapi container deploy <id> --source ./my-app
|
|
580
|
-
myapi container deploy <id> --
|
|
620
|
+
myapi container deploy <id> <image> --no-promote
|
|
621
|
+
myapi container deploy <id> --source ./app --smoke 'GET / contains assets/'`,
|
|
622
|
+
'rollback': `myapi container rollback <id> [--org <id>] [--json]
|
|
623
|
+
|
|
624
|
+
Move traffic back to the previous ready revision. Seconds, no rebuild.
|
|
625
|
+
Refuses rather than guessing when it cannot tell what is serving.`,
|
|
581
626
|
'revisions': `myapi container revisions <id> [--org <id>] [--json]
|
|
582
627
|
|
|
583
628
|
Every revision the runtime currently holds, newest first, with the traffic
|
|
@@ -621,14 +666,17 @@ dependencies and long execution.
|
|
|
621
666
|
Subcommands:
|
|
622
667
|
build-logs <id> Why the last --source build failed (--tail N; default 100)
|
|
623
668
|
create Register a container and get its scoped API key (returned once)
|
|
669
|
+
(--health-check /livez for an HTTP startup probe)
|
|
624
670
|
delete <id> Soft-delete and revoke its scoped API key
|
|
625
671
|
deploy <id> <image> Ship a pre-built image (or --source <dir|tar> to build) and go live
|
|
672
|
+
(--smoke to verify before promoting; --no-promote to hold it back)
|
|
626
673
|
domain <id> <domain> Bind a custom domain (--remove to unbind)
|
|
627
674
|
get <id> Inspect a container
|
|
628
675
|
list List containers in your org
|
|
629
676
|
logs <id> Show recent runtime logs (--tail <n>, --scope all) — see build-logs for build failures
|
|
630
677
|
promote <id> <rev> Move all traffic to a revision (seconds, no rebuild)
|
|
631
678
|
revisions <id> List revisions and the traffic each takes
|
|
679
|
+
rollback <id> Move traffic back to the previous ready revision
|
|
632
680
|
|
|
633
681
|
All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
634
682
|
return;
|
|
@@ -652,12 +700,7 @@ All commands accept --org <id> (or set default: myapi config set-org <id>).`);
|
|
|
652
700
|
case 'promote': return promote(args[0], args[1], flags);
|
|
653
701
|
case 'domain': return domain(args[0], args[1], flags);
|
|
654
702
|
case 'delete': return del(args[0], flags);
|
|
655
|
-
|
|
656
|
-
// NOT a verb here — the API models rollback as `promote` with no revision.
|
|
657
|
-
// A bare "unknown subcommand" would cost minutes at the worst possible
|
|
658
|
-
// moment, so say what to do instead, and be honest that the underlying
|
|
659
|
-
// call is currently broken rather than let someone discover that live.
|
|
660
|
-
case 'rollback': return rollbackGuidance(args[0]);
|
|
703
|
+
case 'rollback': return rollback(args[0], flags);
|
|
661
704
|
default: error(`Unknown subcommand: ${subcommand}. Run "myapi container --help" for a list of valid subcommands.`);
|
|
662
705
|
}
|
|
663
706
|
}
|
|
@@ -24,7 +24,8 @@ export const SCHEMA = {
|
|
|
24
24
|
route: 'string',
|
|
25
25
|
origins: 'string',
|
|
26
26
|
};
|
|
27
|
-
|
|
27
|
+
// The platform's enum, checked against the schema rather than invented.
|
|
28
|
+
const KINDS = ['bug', 'issue', 'suggestion'];
|
|
28
29
|
export async function list(flags) {
|
|
29
30
|
const config = requireConfig();
|
|
30
31
|
const orgId = requireOrg(flags, config, 'myapi feedback list [--kind <k>] [--status open|resolved] [--org <id>]');
|
|
@@ -64,7 +65,7 @@ export async function create(bodyArg, flags) {
|
|
|
64
65
|
const orgId = requireOrg(flags, config, 'myapi feedback create "<text>" --kind <k> [--org <id>]');
|
|
65
66
|
const body = bodyArg ?? flags.body;
|
|
66
67
|
requireArg(body, 'text', 'myapi feedback create "<text>" --kind bug');
|
|
67
|
-
const kind = flags.kind ?? '
|
|
68
|
+
const kind = flags.kind ?? 'issue';
|
|
68
69
|
if (!KINDS.includes(kind)) {
|
|
69
70
|
error(`Invalid --kind "${kind}". Use one of: ${KINDS.join(', ')}.\n\n→ Kind is what the PERSON says it is. "bug" is a claim that the product is broken; do not infer it from the wording.`);
|
|
70
71
|
}
|
|
@@ -127,7 +128,7 @@ export async function widget(sub, arg, flags) {
|
|
|
127
128
|
error('Usage: myapi feedback widget create <name> [--origins <list>]\n myapi feedback widget revoke <id>');
|
|
128
129
|
}
|
|
129
130
|
const SUBCOMMAND_USAGE = {
|
|
130
|
-
'list': `myapi feedback list [--kind bug|
|
|
131
|
+
'list': `myapi feedback list [--kind bug|issue|suggestion] [--status open|resolved]
|
|
131
132
|
[--limit N] [--offset N] [--org <id>] [--json]
|
|
132
133
|
|
|
133
134
|
Newest first. \`total\` is the number of matches, not the page size.`,
|
|
@@ -149,7 +150,7 @@ Collect feedback from the people using what you built. A widget key lets a
|
|
|
149
150
|
page submit without a credential; you list, filter and resolve the results.
|
|
150
151
|
|
|
151
152
|
Subcommands:
|
|
152
|
-
create "<text>" Record one piece of feedback (--kind bug|
|
|
153
|
+
create "<text>" Record one piece of feedback (--kind bug|issue|suggestion)
|
|
153
154
|
list List feedback, newest first (--kind, --status, --limit, --offset)
|
|
154
155
|
resolve <id> Close a piece of feedback
|
|
155
156
|
widget create <name> Mint a PUBLIC widget key for a site (--origins to restrict)
|
|
@@ -83,38 +83,61 @@ async function run(fn) {
|
|
|
83
83
|
throw e;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
describe('container deploy —
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
86
|
+
describe('container deploy — --no-promote must REACH both paths', () => {
|
|
87
|
+
// This is the original bug in its final form. --no-promote was wired to the
|
|
88
|
+
// image branch only, so a --source deploy accepted it and dropped it, and
|
|
89
|
+
// the build took 100% of traffic. The platform has since fixed its side and
|
|
90
|
+
// the flag works on both paths — so the assertion flips from "is refused" to
|
|
91
|
+
// "arrives", and the bug it guards is the same one either way.
|
|
92
|
+
const DEPLOYED = {
|
|
93
|
+
container_id: 'c1', revision_id: 'r1', url: 'https://x',
|
|
94
|
+
status: 'active', scoped_api_key: 'k', promoted: false,
|
|
95
|
+
revision_url: 'https://rev---x.run.app',
|
|
96
|
+
};
|
|
97
|
+
it('sends promote:false on the image path', async () => {
|
|
98
|
+
sdk.container.deployContainer.mockResolvedValue(DEPLOYED);
|
|
91
99
|
const { deploy } = await import('./container.js');
|
|
92
|
-
await run(() => deploy('c1', '', {
|
|
93
|
-
expect(
|
|
94
|
-
expect(sdk.container.deployContainerSource).not.toHaveBeenCalled();
|
|
95
|
-
expect(sdk.container.deployContainer).not.toHaveBeenCalled();
|
|
100
|
+
await run(() => deploy('c1', 'img:v1', { 'no-promote': true, org: ORG }));
|
|
101
|
+
expect(sdk.container.deployContainer).toHaveBeenCalledWith('hq_live_test', ORG, 'c1', 'img:v1', { promote: false });
|
|
96
102
|
});
|
|
97
|
-
|
|
103
|
+
// The branch that shipped broken. It reads a real tarball, so the assertion
|
|
104
|
+
// is that the options object reaches the call — argument 6.
|
|
105
|
+
it('sends promote:false on the --source path', async () => {
|
|
106
|
+
sdk.container.deployContainerSource.mockResolvedValue({ container_id: 'c1', revision_id: 'r1', status: 'building' });
|
|
107
|
+
sdk.container.getContainer.mockResolvedValue({ id: 'c1', status: 'active', url: 'https://x' });
|
|
108
|
+
const { _isTarball } = await import('./container.js');
|
|
109
|
+
void _isTarball;
|
|
98
110
|
const { deploy } = await import('./container.js');
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
const fsp = await import('node:fs/promises');
|
|
112
|
+
const tmp = `${process.env.TMPDIR ?? '/tmp'}/reach-${Date.now()}.tar.gz`;
|
|
113
|
+
await fsp.writeFile(tmp, 'not-a-real-tarball');
|
|
114
|
+
await run(() => deploy('c1', '', { source: tmp, 'no-promote': true, org: ORG }));
|
|
115
|
+
await fsp.rm(tmp, { force: true });
|
|
116
|
+
expect(sdk.container.deployContainerSource).toHaveBeenCalled();
|
|
117
|
+
expect(sdk.container.deployContainerSource.mock.calls[0][5]).toEqual({ promote: false });
|
|
102
118
|
});
|
|
103
|
-
it('
|
|
119
|
+
it('sends no options at all when the flag is absent', async () => {
|
|
120
|
+
sdk.container.deployContainer.mockResolvedValue({ ...DEPLOYED, promoted: true });
|
|
104
121
|
const { deploy } = await import('./container.js');
|
|
105
|
-
await run(() => deploy('c1', 'img:v1', {
|
|
106
|
-
expect(
|
|
107
|
-
expect(sdk.container.deployContainer).not.toHaveBeenCalled();
|
|
122
|
+
await run(() => deploy('c1', 'img:v1', { org: ORG }));
|
|
123
|
+
expect(sdk.container.deployContainer).toHaveBeenCalledWith('hq_live_test', ORG, 'c1', 'img:v1', {});
|
|
108
124
|
});
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
});
|
|
125
|
+
// --smoke was refused while the platform ignored it, and is honoured now.
|
|
126
|
+
// The assertion has flipped twice; what has not changed is that it must
|
|
127
|
+
// REACH the call, on whichever path.
|
|
128
|
+
it('sends the parsed assertion on the image path', async () => {
|
|
129
|
+
sdk.container.deployContainer.mockResolvedValue({ ...DEPLOYED, promoted: true });
|
|
114
130
|
const { deploy } = await import('./container.js');
|
|
115
|
-
await run(() => deploy('c1', 'img:v1', { org: ORG }));
|
|
116
|
-
expect(
|
|
117
|
-
|
|
131
|
+
await run(() => deploy('c1', 'img:v1', { smoke: 'GET / contains assets/', org: ORG }));
|
|
132
|
+
expect(sdk.container.deployContainer).toHaveBeenCalledWith('hq_live_test', ORG, 'c1', 'img:v1', { smoke: { method: 'GET', path: '/', contains: 'assets/' } });
|
|
133
|
+
});
|
|
134
|
+
// Combining them is a contradiction: --smoke promotes on success, and
|
|
135
|
+
// --no-promote withholds regardless. Refused rather than silently picking.
|
|
136
|
+
it('refuses --smoke together with --no-promote', async () => {
|
|
137
|
+
const { deploy } = await import('./container.js');
|
|
138
|
+
await run(() => deploy('c1', 'img:v1', { smoke: 'GET / contains x', 'no-promote': true, org: ORG }));
|
|
139
|
+
expect(exitError).toMatch(/one or the other/);
|
|
140
|
+
expect(sdk.container.deployContainer).not.toHaveBeenCalled();
|
|
118
141
|
});
|
|
119
142
|
});
|
|
120
143
|
describe('crm pagination — --offset must reach the SDK', () => {
|
package/dist/errors.js
CHANGED
|
@@ -45,6 +45,12 @@ export const ERROR_MESSAGES = {
|
|
|
45
45
|
// on a backend deploy we do not control the timing of, and a CLI that only
|
|
46
46
|
// knows the new names would print a bare code for anyone on the old build.
|
|
47
47
|
// The old five can go once that deploy is everywhere.
|
|
48
|
+
// Deploy-guard validation, added when the platform started refusing bad
|
|
49
|
+
// assertions BEFORE building — a source build costs minutes you pay for, so
|
|
50
|
+
// catching a typo up front matters more than it looks.
|
|
51
|
+
INVALID_SMOKE: 'The --smoke assertion is not usable. It must assert something: add `contains <text>` or `status <code>`. A check that only requests a path passes on any response, including the placeholder page the flag exists to catch.',
|
|
52
|
+
SMOKE_FAILED: 'The smoke assertion did not pass, so the new revision was NOT promoted. On a container with an earlier revision that one is still serving; on a FIRST deploy there was nothing to hold traffic and the failing build is live. Inspect the revision URL in the error.',
|
|
53
|
+
INVALID_HEALTH_CHECK: 'That health-check path is not usable. /healthz is refused specifically: the runtime answers it before your container does, so a probe on it passes even when your app is down. Use /livez or any other path.',
|
|
48
54
|
DNS_UNAVAILABLE: 'The DNS provider is unavailable. This is platform-side and usually transient — retry shortly rather than changing your request.',
|
|
49
55
|
DNS_ZONE_NOT_FOUND: 'No DNS zone for this domain. Register or import it first: myapi domain register <domain>.',
|
|
50
56
|
DNS_ZONE_UNAVAILABLE: 'The DNS zone exists but could not be reached. Platform-side and transient.',
|
|
@@ -4,7 +4,7 @@ version: 1.0.0
|
|
|
4
4
|
description: >
|
|
5
5
|
Run containers on demand — long-running services, background workers, and scheduled jobs. The heavier-duty sibling of edge functions, for native deps and long execution.
|
|
6
6
|
triggers: [container, cloud run, dynamic app, custom domain app, service, worker, scheduled job, deploy container, docker image]
|
|
7
|
-
checksum: sha256-
|
|
7
|
+
checksum: sha256-0e68a4596027c9ccccf0cc86423deffbe71307d265aa973d3cd6261db2695152
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MyContainerAPI
|
|
@@ -23,26 +23,33 @@ The lifecycle is **create → deploy → (optionally) bind a custom domain**.
|
|
|
23
23
|
can deploy; you do not need to be able to run one.
|
|
24
24
|
- `domain` puts the container on a **custom domain** — how you serve a dynamic app at `app.yourbrand.com`.
|
|
25
25
|
|
|
26
|
-
### Deploying safely
|
|
26
|
+
### Deploying safely
|
|
27
27
|
|
|
28
|
-
A deploy takes 100% of traffic the moment it lands.
|
|
29
|
-
definition of correct beyond "something is listening on the port", and no way
|
|
30
|
-
back. Plan for that.
|
|
28
|
+
A plain deploy takes 100% of traffic the moment it lands.
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
not appear on the container afterwards, so do not rely on it either.
|
|
30
|
+
**`--smoke '<assertion>'`** deploys the revision with no traffic, checks the
|
|
31
|
+
assertion, and promotes only if it holds. A failure returns `SMOKE_FAILED`,
|
|
32
|
+
leaves the previous revision serving, and returns a revision URL to inspect.
|
|
36
33
|
|
|
37
|
-
|
|
34
|
+
```bash
|
|
35
|
+
myapi container deploy <id> --source ./app --smoke 'GET / contains assets/'
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Assert on content, not status.** A build whose frontend never bundled still
|
|
39
|
+
binds its port and answers `200` — that is how a placeholder page reached
|
|
40
|
+
production and served a dead page for fifteen minutes.
|
|
41
|
+
|
|
42
|
+
**`--no-promote`** holds the revision back and prints a URL to test yourself,
|
|
43
|
+
then `myapi container promote <id> <revision>`. Use it when the check is more
|
|
44
|
+
than one assertion. On a container's *first* deploy nothing is serving yet, so
|
|
45
|
+
traffic is not withheld and the output says so.
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
a 200, because a broken build returns 200 too
|
|
42
|
-
3. deploy the same image to production
|
|
47
|
+
**`myapi container rollback <id>`** returns traffic to the previous ready
|
|
48
|
+
revision in seconds, no rebuild.
|
|
43
49
|
|
|
44
|
-
`
|
|
45
|
-
|
|
50
|
+
`--health-check /livez` at create makes the startup probe an HTTP request
|
|
51
|
+
rather than a bare TCP connect. `/healthz` is refused — the runtime intercepts
|
|
52
|
+
it, so the probe would never reach your container.
|
|
46
53
|
|
|
47
54
|
### Custom domains (dynamic apps)
|
|
48
55
|
|
|
@@ -121,8 +128,7 @@ way that looks like an application bug.
|
|
|
121
128
|
error envelope will not reach the caller. Return a 4xx if the reason has to
|
|
122
129
|
survive.
|
|
123
130
|
- **`--env`, `--cpu`, `--memory`, `--max-instances` and `--cron` are set at
|
|
124
|
-
`create` and cannot be changed by `deploy`.**
|
|
125
|
-
nothing. Recreate the container to change them.
|
|
131
|
+
`create` and cannot be changed by `deploy`.** Recreate to change them.
|
|
126
132
|
|
|
127
133
|
### Keeping a service warm
|
|
128
134
|
|
|
@@ -4,7 +4,7 @@ version: 1.0.0
|
|
|
4
4
|
description: >
|
|
5
5
|
Edge-hosted asset storage. Upload a local file of any content type directly, or have the server fetch from a public URL. Each asset gets a stable public CDN URL.
|
|
6
6
|
triggers: [storage, upload, ingest, asset, cdn, image hosting, file upload, get-url, download, public url]
|
|
7
|
-
checksum: sha256-
|
|
7
|
+
checksum: sha256-1f3e7f6de3435e0334a808b9d2faa978c9cb70982509a376abc0e539cb44e1ed
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
# MyStorageAPI
|
|
@@ -93,11 +93,17 @@ Both produce identical asset records — `list` doesn't distinguish.
|
|
|
93
93
|
fetch it; there is no private mode, no signed URL, and no revocation. The id
|
|
94
94
|
being long and random is **not** access control — treat the URL as public the
|
|
95
95
|
moment it exists.
|
|
96
|
-
- **
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
96
|
+
- **Private assets exist as of 2026-07-28** — upload with `visibility: private`,
|
|
97
|
+
or `PATCH` an existing asset to close an exposure, which takes effect
|
|
98
|
+
immediately. Fetch it with a signed URL (15 minutes default, 24 hours max);
|
|
99
|
+
`revoke-links` kills every link already handed out, including unexpired ones.
|
|
100
|
+
A private upload deliberately returns **no plain `url`**, because that URL
|
|
101
|
+
does not serve the file and would look like the answer.
|
|
102
|
+
- **Encrypting before upload is still worth doing for the strictest cases.**
|
|
103
|
+
Private assets protect against the internet; client-side encryption protects
|
|
104
|
+
against the platform, and those are different threat models. A team shipping
|
|
105
|
+
Swiss lease documents used AES-256-GCM envelope encryption with a
|
|
106
|
+
per-document data key wrapped under a master key in function secrets.
|
|
101
107
|
- Delete is immediate and unrecoverable — run `myapi storage list` first to confirm the asset, pass `--org` explicitly, and pass `--yes` in non-interactive runs.
|
|
102
108
|
- The URL is permanent until you `myapi storage delete <id>` — embed it freely.
|
|
103
109
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myapihq/cli",
|
|
3
3
|
"license": "Apache-2.0",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.10.0",
|
|
5
5
|
"description": "MyAPI command-line interface",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"lint:skills:strict": "node scripts/copy-skills.js && node scripts/lint-skills.js --strict"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@myapihq/sdk": "^2.
|
|
49
|
+
"@myapihq/sdk": "^2.10.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@types/node": "^25.6.0",
|