@myapihq/cli 2.9.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.
|
@@ -239,17 +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
|
-
// --smoke is still not honoured; --no-promote was fixed upstream on
|
|
243
|
-
// 2026-07-28 and works on both deploy paths. Refusing what does not work
|
|
244
|
-
// beats accepting it, and un-refusing what does beats obstructing.
|
|
245
|
-
if (typeof flags.smoke === 'string') {
|
|
246
|
-
error('--smoke is not honoured yet, so this CLI refuses it rather than letting you believe a deploy was checked.\n\n' +
|
|
247
|
-
'Use --no-promote instead, which now works:\n' +
|
|
248
|
-
' 1. myapi container deploy <id> <image> --no-promote\n' +
|
|
249
|
-
' 2. curl the revision URL it prints, for a string only a real build emits\n' +
|
|
250
|
-
' 3. myapi container promote <id> <revision>\n\n' +
|
|
251
|
-
'That is the same verify-then-promote, done by you rather than the platform.');
|
|
252
|
-
}
|
|
253
242
|
const source = typeof flags.source === 'string' ? flags.source : undefined;
|
|
254
243
|
// --image is an alias for the positional image ref.
|
|
255
244
|
if (!image && typeof flags.image === 'string')
|
|
@@ -262,6 +251,28 @@ export async function deploy(id, image, flags) {
|
|
|
262
251
|
const deployOpts = {};
|
|
263
252
|
if (flags['no-promote'] === true)
|
|
264
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
|
+
}
|
|
265
276
|
// ── Source-build path (async) ───────────────────────────────────────────
|
|
266
277
|
if (source) {
|
|
267
278
|
let tarball;
|
|
@@ -593,13 +604,21 @@ Options:
|
|
|
593
604
|
On a container's FIRST deploy there is nothing already
|
|
594
605
|
serving, so traffic is NOT withheld and the output says so.
|
|
595
606
|
|
|
596
|
-
--smoke
|
|
597
|
-
|
|
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.
|
|
598
616
|
|
|
599
617
|
Examples:
|
|
600
618
|
myapi container deploy <id> registry.example.com/my-app:v2
|
|
601
619
|
myapi container deploy <id> --source ./my-app
|
|
602
|
-
myapi container deploy <id> <image> --no-promote
|
|
620
|
+
myapi container deploy <id> <image> --no-promote
|
|
621
|
+
myapi container deploy <id> --source ./app --smoke 'GET / contains assets/'`,
|
|
603
622
|
'rollback': `myapi container rollback <id> [--org <id>] [--json]
|
|
604
623
|
|
|
605
624
|
Move traffic back to the previous ready revision. Seconds, no rebuild.
|
|
@@ -647,8 +666,10 @@ dependencies and long execution.
|
|
|
647
666
|
Subcommands:
|
|
648
667
|
build-logs <id> Why the last --source build failed (--tail N; default 100)
|
|
649
668
|
create Register a container and get its scoped API key (returned once)
|
|
669
|
+
(--health-check /livez for an HTTP startup probe)
|
|
650
670
|
delete <id> Soft-delete and revoke its scoped API key
|
|
651
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)
|
|
652
673
|
domain <id> <domain> Bind a custom domain (--remove to unbind)
|
|
653
674
|
get <id> Inspect a container
|
|
654
675
|
list List containers in your org
|
|
@@ -122,11 +122,21 @@ describe('container deploy — --no-promote must REACH both paths', () => {
|
|
|
122
122
|
await run(() => deploy('c1', 'img:v1', { org: ORG }));
|
|
123
123
|
expect(sdk.container.deployContainer).toHaveBeenCalledWith('hq_live_test', ORG, 'c1', 'img:v1', {});
|
|
124
124
|
});
|
|
125
|
-
// --smoke
|
|
126
|
-
|
|
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 });
|
|
130
|
+
const { deploy } = await import('./container.js');
|
|
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 () => {
|
|
127
137
|
const { deploy } = await import('./container.js');
|
|
128
|
-
await run(() => deploy('c1', 'img:v1', { smoke: 'GET / contains x', org: ORG }));
|
|
129
|
-
expect(exitError).toMatch(
|
|
138
|
+
await run(() => deploy('c1', 'img:v1', { smoke: 'GET / contains x', 'no-promote': true, org: ORG }));
|
|
139
|
+
expect(exitError).toMatch(/one or the other/);
|
|
130
140
|
expect(sdk.container.deployContainer).not.toHaveBeenCalled();
|
|
131
141
|
});
|
|
132
142
|
});
|
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
|
|
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",
|