@percher/core 0.4.2 → 0.4.3
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/cache.d.ts +23 -0
- package/dist/commands/cache.d.ts.map +1 -0
- package/dist/commands/cache.js +24 -0
- package/dist/commands/cache.js.map +1 -0
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +1 -1
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/dashboard.d.ts.map +1 -1
- package/dist/commands/dashboard.js +12 -1
- package/dist/commands/dashboard.js.map +1 -1
- package/dist/commands/doctor.d.ts +126 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +445 -313
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/preview-branch.d.ts +31 -0
- package/dist/commands/preview-branch.d.ts.map +1 -0
- package/dist/commands/preview-branch.js +55 -0
- package/dist/commands/preview-branch.js.map +1 -0
- package/dist/commands/publish-api-error.d.ts +15 -0
- package/dist/commands/publish-api-error.d.ts.map +1 -1
- package/dist/commands/publish-api-error.js +155 -6
- package/dist/commands/publish-api-error.js.map +1 -1
- package/dist/commands/publish-failure.d.ts +3 -1
- package/dist/commands/publish-failure.d.ts.map +1 -1
- package/dist/commands/publish-failure.js +11 -7
- package/dist/commands/publish-failure.js.map +1 -1
- package/dist/commands/publish.d.ts.map +1 -1
- package/dist/commands/publish.js +62 -328
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/push.d.ts.map +1 -1
- package/dist/commands/push.js +4 -4
- package/dist/commands/push.js.map +1 -1
- package/dist/commands/redeploy.js +3 -3
- package/dist/commands/redeploy.js.map +1 -1
- package/dist/commands/rename.d.ts +7 -0
- package/dist/commands/rename.d.ts.map +1 -1
- package/dist/commands/rename.js +32 -1
- package/dist/commands/rename.js.map +1 -1
- package/dist/commands/wait-deploy.d.ts +12 -2
- package/dist/commands/wait-deploy.d.ts.map +1 -1
- package/dist/commands/wait-deploy.js +115 -46
- package/dist/commands/wait-deploy.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/poll-deployment.d.ts +3 -3
- package/dist/poll-deployment.d.ts.map +1 -1
- package/dist/poll-deployment.js +5 -4
- package/dist/poll-deployment.js.map +1 -1
- package/dist/publish-retry.d.ts.map +1 -1
- package/dist/publish-retry.js +2 -3
- package/dist/publish-retry.js.map +1 -1
- package/package.json +4 -4
- package/dist/commands/continue.d.ts +0 -48
- package/dist/commands/continue.d.ts.map +0 -1
- package/dist/commands/continue.js +0 -121
- package/dist/commands/continue.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish-failure.js","sourceRoot":"","sources":["../../src/commands/publish-failure.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"publish-failure.js","sourceRoot":"","sources":["../../src/commands/publish-failure.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../src/commands/publish.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,GAAG,EACR,KAAK,YAAY,EAEjB,KAAK,UAAU,EAIhB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"publish.d.ts","sourceRoot":"","sources":["../../src/commands/publish.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/publish.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { isDeployAlreadyInProgress, PercherApiError, PercherClient, } from "@percher/client";
|
|
4
|
+
import { MAX_TARBALL_BYTES } from "@percher/shared";
|
|
4
5
|
import { TIMEOUTS } from "@percher/shared/timeouts";
|
|
5
6
|
import { PercherTomlError, parseFile } from "@percher/toml";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { renderDeployEvent } from "../event-renderer";
|
|
8
9
|
import { pollDeployment } from "../poll-deployment";
|
|
9
10
|
import { runDeployWithRetry } from "../publish-retry";
|
|
10
|
-
import { RECOVERY_NEEDS_LOGIN, RECOVERY_NONE, recoveryAsk, recoveryDoctor,
|
|
11
|
+
import { RECOVERY_NEEDS_LOGIN, RECOVERY_NONE, recoveryAsk, recoveryDoctor, recoveryFixConfig, recoveryWait, } from "../recovery";
|
|
11
12
|
import { createTarball } from "../tarball";
|
|
12
13
|
import { scanForMissingBuildEnvRefs } from "./env-scan";
|
|
13
14
|
import { init } from "./init";
|
|
14
15
|
import { login } from "./login";
|
|
15
|
-
import { classifyPublishApiError } from "./publish-api-error";
|
|
16
|
+
import { classifyPublishApiError, DEPLOY_GATE_CODES } from "./publish-api-error";
|
|
16
17
|
import { buildFailureResult } from "./publish-failure";
|
|
17
18
|
import { resolveNodeVersion } from "./publish-node";
|
|
18
19
|
import { resolveReplaced } from "./wait-deploy";
|
|
@@ -36,7 +37,6 @@ export const publishInputSchema = z.object({
|
|
|
36
37
|
.optional()
|
|
37
38
|
.describe("If true (default), publish blocks until the deploy is live or failed. If false, publish returns as soon as the deploy is queued and the agent can resume with percher_wait_for_deploy. Recommended for AI agents — gives back control quickly with a deployId so they can decide between waiting, asking the user, or working on something else."),
|
|
38
39
|
});
|
|
39
|
-
const MAX_BYTES = 500 * 1024 * 1024;
|
|
40
40
|
const SUSPICIOUS_BYTES = 50 * 1024 * 1024;
|
|
41
41
|
const SUSPICIOUS_FILES = 10_000;
|
|
42
42
|
function tomlPathFor(cwd) {
|
|
@@ -99,39 +99,6 @@ function buildAlreadyInProgressResult(opts) {
|
|
|
99
99
|
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
-
/**
|
|
103
|
-
* FUTURE12 Phase 6d — server rejected this publish because the user
|
|
104
|
-
* already used their one auto-retry within the 10-minute window after
|
|
105
|
-
* an `infra_unavailable` failure. Build an `ask_user` recovery so the
|
|
106
|
-
* agent surfaces it to the human instead of looping.
|
|
107
|
-
*/
|
|
108
|
-
function buildRetryLimitReachedResult(opts) {
|
|
109
|
-
const { err, app, tarball, cwd } = opts;
|
|
110
|
-
const extra = err.extra ?? {};
|
|
111
|
-
const resetAtStr = extra.resetAt ?? "";
|
|
112
|
-
const resetAtForPrompt = resetAtStr || "the cooldown window expires";
|
|
113
|
-
const prompt = `Percher already retried once after an infrastructure failure for ${app.name}. Wait until ${resetAtForPrompt} or surface to the user.`;
|
|
114
|
-
return {
|
|
115
|
-
status: "failed",
|
|
116
|
-
app,
|
|
117
|
-
fileCount: tarball.fileCount,
|
|
118
|
-
bytes: tarball.bytes,
|
|
119
|
-
error: {
|
|
120
|
-
title: "Retry limit reached",
|
|
121
|
-
explanation: "Percher already auto-retried once after an `infra_unavailable` failure for this app within the last 10 minutes. We won't auto-retry again — the next attempt should be a deliberate user decision.",
|
|
122
|
-
suggestion: `Wait until ${resetAtForPrompt}, then re-run \`percher publish\`. If the failure persists past that, the underlying issue is probably not transient — run \`percher doctor\` to diagnose.`,
|
|
123
|
-
},
|
|
124
|
-
recovery: recoveryAsk({
|
|
125
|
-
prompt,
|
|
126
|
-
options: ["wait before retrying", "inspect status"],
|
|
127
|
-
reasonCode: "retry_limit_reached",
|
|
128
|
-
retryable: false,
|
|
129
|
-
}),
|
|
130
|
-
summary: `Retry limit reached for ${app.name}${resetAtStr ? ` — wait until ${resetAtStr}` : ""}.`,
|
|
131
|
-
configPath: tomlPathFor(cwd),
|
|
132
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
102
|
/**
|
|
136
103
|
* Build the one-line summary echoed back to humans. Includes app name,
|
|
137
104
|
* framework when known, total seconds, and final URL — covers the
|
|
@@ -326,7 +293,7 @@ async function publishInner(ctx, input) {
|
|
|
326
293
|
ctx.status("[2/4] Packaging files...");
|
|
327
294
|
const tarball = await createTarball({ cwd: ctx.cwd, config });
|
|
328
295
|
const packageMs = Date.now() - t0;
|
|
329
|
-
if (tarball.bytes >
|
|
296
|
+
if (tarball.bytes > MAX_TARBALL_BYTES) {
|
|
330
297
|
return {
|
|
331
298
|
status: "failed",
|
|
332
299
|
fileCount: tarball.fileCount,
|
|
@@ -447,7 +414,29 @@ async function publishInner(ctx, input) {
|
|
|
447
414
|
// proceeds and lets the retry loop (Phase 7.2) handle whatever
|
|
448
415
|
// surfaces during the actual upload.
|
|
449
416
|
}
|
|
450
|
-
// ── 4. Upload
|
|
417
|
+
// ── 4. Upload → poll → result ─────────────────────────────────────
|
|
418
|
+
return executeDeploy({
|
|
419
|
+
ctx,
|
|
420
|
+
config,
|
|
421
|
+
input,
|
|
422
|
+
app,
|
|
423
|
+
firstDeploy,
|
|
424
|
+
tarball,
|
|
425
|
+
t0,
|
|
426
|
+
packageMs,
|
|
427
|
+
missingBuildEnvKeys,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Post-upload deploy pipeline shared by the primary publish path and
|
|
432
|
+
* the post-login retry: buffer + upload the tarball (cache probe,
|
|
433
|
+
* idempotent retry loop), poll to a terminal status via pollDeployment
|
|
434
|
+
* (which retries transient network failures), and shape the
|
|
435
|
+
* PublishResult. Both entry paths must route through here so the
|
|
436
|
+
* re-auth path can't drift from the primary one.
|
|
437
|
+
*/
|
|
438
|
+
async function executeDeploy(opts) {
|
|
439
|
+
const { ctx, config, input, app, firstDeploy, tarball, t0, packageMs, missingBuildEnvKeys } = opts;
|
|
451
440
|
const uploadStart = Date.now();
|
|
452
441
|
ctx.status("[3/4] Uploading...");
|
|
453
442
|
// Buffer the tarball so the retry loop (Phase 7.2) can resend the
|
|
@@ -471,7 +460,11 @@ async function publishInner(ctx, input) {
|
|
|
471
460
|
// Codex P2, 2026-05-08. Sent as `X-Tarball-Hash` on the upload so
|
|
472
461
|
// the API stores it on `deployments.tarball_hash` for the probe.
|
|
473
462
|
const contentHash = tarball.contentHash;
|
|
474
|
-
|
|
463
|
+
// Preview publishes never take the probe fast-path: the probe's hit
|
|
464
|
+
// branch reruns the previous LIVE deploy (traffic swap included),
|
|
465
|
+
// which would silently turn `--preview` into a live redeploy. The
|
|
466
|
+
// server-side image cache still skips the rebuild on upload.
|
|
467
|
+
if (!input.noCache && !input.preview) {
|
|
475
468
|
try {
|
|
476
469
|
const probe = await ctx.client.platform.cacheProbe({
|
|
477
470
|
appId: app.id,
|
|
@@ -534,118 +527,18 @@ async function publishInner(ctx, input) {
|
|
|
534
527
|
deployment = deployResponse;
|
|
535
528
|
}
|
|
536
529
|
catch (err) {
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
return buildRetryLimitReachedResult({
|
|
544
|
-
err,
|
|
545
|
-
app,
|
|
546
|
-
tarball: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
547
|
-
cwd: ctx.cwd,
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
// Daily-quota gate (Fas 4) returns 429 DAILY_QUOTA_EXCEEDED with
|
|
551
|
-
// structured `extra` fields (kind/used/limit/resetAt). Surface
|
|
552
|
-
// those to the agent as ask_user — retry only makes sense after
|
|
553
|
-
// resetAt, which the message includes verbatim.
|
|
554
|
-
// FUTURE12 Phase 6c — pre-queue env gate. Two codes, both 422:
|
|
555
|
-
// REQUIRED_ENV_MISSING → recoveryEnv (set the keys, re-publish)
|
|
556
|
-
// ENV_KEY_UNDECLARED → recoveryFixConfig (declare in [env])
|
|
557
|
-
// Distinct from build-failure missing_env: the gate fires BEFORE
|
|
558
|
-
// the deploy queues, so there's no deployId/buildLog to point at —
|
|
559
|
-
// the only artifact is the structured `extra` payload.
|
|
560
|
-
if (err instanceof PercherApiError && err.code === "REQUIRED_ENV_MISSING") {
|
|
561
|
-
const extra = err.extra ?? {};
|
|
562
|
-
const keys = extra.keys ?? [];
|
|
563
|
-
const source = extra.source ?? "contract";
|
|
564
|
-
const sourceText = source === "contract"
|
|
565
|
-
? "declared in your [env].required"
|
|
566
|
-
: source === "discovered"
|
|
567
|
-
? "learned from a previous build failure"
|
|
568
|
-
: "both declared and learned from a previous build failure";
|
|
569
|
-
return {
|
|
570
|
-
status: "failed",
|
|
571
|
-
app,
|
|
572
|
-
fileCount: tarball.fileCount,
|
|
573
|
-
bytes: tarball.bytes,
|
|
574
|
-
error: {
|
|
575
|
-
title: "Required env keys not set",
|
|
576
|
-
explanation: `${keys.length} env ${keys.length === 1 ? "key is" : "keys are"} ${sourceText} but missing from the env store: ${keys.join(", ")}.`,
|
|
577
|
-
suggestion: `Run \`bunx percher env set ${keys[0] ?? "KEY"}=value\` (and similarly for the rest) and re-publish.`,
|
|
578
|
-
errorClass: "missing_env",
|
|
579
|
-
phase: "config",
|
|
580
|
-
missingEnvVars: keys,
|
|
581
|
-
},
|
|
582
|
-
recovery: recoveryEnv({
|
|
583
|
-
app: app.name,
|
|
584
|
-
keys,
|
|
585
|
-
reasonCode: "missing_env",
|
|
586
|
-
}),
|
|
587
|
-
summary: `Missing required env ${keys.length === 1 ? "key" : "keys"}: ${keys.join(", ")}.`,
|
|
530
|
+
// Pre-queue deploy gates (retry-limit guardrail, env contract,
|
|
531
|
+
// daily quota) classify via the shared table with app + bundle
|
|
532
|
+
// context. Everything else rethrows so the top-level catch-all
|
|
533
|
+
// keeps its existing rendering (no app, zero bundle).
|
|
534
|
+
if (err instanceof PercherApiError && DEPLOY_GATE_CODES.has(err.code)) {
|
|
535
|
+
const classified = classifyPublishApiError(err, {
|
|
588
536
|
configPath: tomlPathFor(ctx.cwd),
|
|
589
537
|
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
590
|
-
};
|
|
591
|
-
}
|
|
592
|
-
if (err instanceof PercherApiError && err.code === "ENV_KEY_UNDECLARED") {
|
|
593
|
-
const extra = err.extra ?? {};
|
|
594
|
-
const apiProblems = extra.problems ?? [];
|
|
595
|
-
const problems = apiProblems.map((p) => ({
|
|
596
|
-
file: p.file,
|
|
597
|
-
line: p.line,
|
|
598
|
-
message: `Source references env key '${p.key}' which is not declared in [env]. Add it to [env].required, [env].optional, or [env].ignore in percher.toml. Context: ${p.context ?? "(unavailable)"}`,
|
|
599
|
-
}));
|
|
600
|
-
const uniqueKeys = [...new Set(apiProblems.map((p) => p.key))];
|
|
601
|
-
return {
|
|
602
|
-
status: "failed",
|
|
603
538
|
app,
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
title: "Undeclared env key",
|
|
608
|
-
explanation: `Source references ${uniqueKeys.length} env ${uniqueKeys.length === 1 ? "key" : "keys"} that aren't classified in percher.toml's [env] table: ${uniqueKeys.join(", ")}.`,
|
|
609
|
-
suggestion: "Add each key to [env].required (must exist before deploy), [env].optional (may be referenced), or [env].ignore (intentionally unset). See https://percher.app/docs/env for the contract.",
|
|
610
|
-
errorClass: "config_invalid",
|
|
611
|
-
phase: "config",
|
|
612
|
-
relevantFiles: ["percher.toml"],
|
|
613
|
-
},
|
|
614
|
-
recovery: recoveryFixConfig({
|
|
615
|
-
problems,
|
|
616
|
-
reasonCode: "env_key_undeclared",
|
|
617
|
-
}),
|
|
618
|
-
summary: `${uniqueKeys.length} undeclared env ${uniqueKeys.length === 1 ? "key" : "keys"} in source: ${uniqueKeys.join(", ")}.`,
|
|
619
|
-
configPath: tomlPathFor(ctx.cwd),
|
|
620
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
if (err instanceof PercherApiError && err.code === "DAILY_QUOTA_EXCEEDED") {
|
|
624
|
-
const extra = err.extra ?? {};
|
|
625
|
-
const kind = extra.kind ?? "live";
|
|
626
|
-
const used = extra.used ?? 0;
|
|
627
|
-
const limit = extra.limit ?? 0;
|
|
628
|
-
const resetAt = extra.resetAt ?? "";
|
|
629
|
-
const otherKind = kind === "live" ? "preview" : "live";
|
|
630
|
-
return {
|
|
631
|
-
status: "failed",
|
|
632
|
-
app,
|
|
633
|
-
fileCount: tarball.fileCount,
|
|
634
|
-
bytes: tarball.bytes,
|
|
635
|
-
error: {
|
|
636
|
-
title: "Daily deploy quota reached",
|
|
637
|
-
explanation: `You've used ${used} of ${limit} ${kind} deploys today on this account. Counter resets at ${resetAt}.`,
|
|
638
|
-
suggestion: `Wait until the counter resets, deploy a ${otherKind} instead, or upgrade your plan at https://percher.app/settings to raise this cap.`,
|
|
639
|
-
},
|
|
640
|
-
recovery: recoveryAsk({
|
|
641
|
-
prompt: `Daily ${kind}-deploy quota reached (${used}/${limit}). Counter resets at ${resetAt}. Wait until then, deploy a ${otherKind} instead, or upgrade at https://percher.app/settings.`,
|
|
642
|
-
options: ["wait", `deploy ${otherKind}`, "upgrade"],
|
|
643
|
-
reasonCode: "quota_exceeded",
|
|
644
|
-
}),
|
|
645
|
-
summary: `Daily ${kind}-deploy quota reached (${used}/${limit}). Resets ${resetAt}.`,
|
|
646
|
-
configPath: tomlPathFor(ctx.cwd),
|
|
647
|
-
bundle: { fileCount: tarball.fileCount, bytes: tarball.bytes },
|
|
648
|
-
};
|
|
539
|
+
});
|
|
540
|
+
if (classified)
|
|
541
|
+
return classified;
|
|
649
542
|
}
|
|
650
543
|
throw err;
|
|
651
544
|
}
|
|
@@ -908,7 +801,10 @@ async function handleUnauthorized(ctx, config, tarball, input) {
|
|
|
908
801
|
};
|
|
909
802
|
}
|
|
910
803
|
}
|
|
911
|
-
// Interactive CLI — trigger login flow
|
|
804
|
+
// Interactive CLI — trigger login flow, rebind the client to the new
|
|
805
|
+
// token, rebuild the inputs the failed attempt consumed (the tarball
|
|
806
|
+
// stream is spent, the app lookup ran against the dead token), then
|
|
807
|
+
// hand off to the same pipeline as the primary path.
|
|
912
808
|
ctx.status("Not logged in — starting login...");
|
|
913
809
|
const newToken = await login(ctx);
|
|
914
810
|
ctx.client = new PercherClient({
|
|
@@ -916,187 +812,25 @@ async function handleUnauthorized(ctx, config, tarball, input) {
|
|
|
916
812
|
token: newToken,
|
|
917
813
|
sessionId: ctx.client.sessionId,
|
|
918
814
|
});
|
|
919
|
-
//
|
|
815
|
+
// Timing restarts here so the interactive login wait doesn't inflate
|
|
816
|
+
// totalSeconds in the result.
|
|
817
|
+
const t0 = Date.now();
|
|
920
818
|
const freshTarball = await createTarball({ cwd: ctx.cwd, config });
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
const freshIdempotencyKey = crypto.randomUUID();
|
|
930
|
-
let deployment;
|
|
931
|
-
try {
|
|
932
|
-
const { result: deployResponse } = await runDeployWithRetry({
|
|
933
|
-
call: (attempt) => ctx.client.apps.deploy(app.id, {
|
|
934
|
-
tarball: freshTarballBytes,
|
|
935
|
-
// Preserve preview/note semantics across the post-login retry.
|
|
936
|
-
// Without these, an interactive `percher publish --preview -m "x"`
|
|
937
|
-
// that hits a 401 would silently downgrade to a live deploy with
|
|
938
|
-
// no note after the user logs in. Codex P1, 2026-04-29.
|
|
939
|
-
type: input.preview ? "preview" : undefined,
|
|
940
|
-
note: input.message,
|
|
941
|
-
noCache: input.noCache,
|
|
942
|
-
idempotencyKey: freshIdempotencyKey,
|
|
943
|
-
// Phase 7.5 — same X-Publish-Attempts claim as the main
|
|
944
|
-
// publish path; ensures the post-login retry chain still
|
|
945
|
-
// yields a `retry_recovered` outcome when it eventually wins.
|
|
946
|
-
retriedAttempts: attempt - 1,
|
|
947
|
-
}),
|
|
948
|
-
onRetry: ({ attempt, decision }) => {
|
|
949
|
-
ctx.status(`Retrying after ${Math.round(decision.delayMs / 1000)}s — ${decision.reason} (attempt ${attempt + 1}/4)…`);
|
|
950
|
-
},
|
|
951
|
-
});
|
|
952
|
-
// FUTURE12 Phase 6d — same already_in_progress short-circuit as
|
|
953
|
-
// the main publishInner path. Without this branch, a user who
|
|
954
|
-
// hits a 401 mid-publish and re-auths could land on the
|
|
955
|
-
// post-login deploy step and have it silently fall through into
|
|
956
|
-
// a 5-minute polling loop against the existing deploy's row.
|
|
957
|
-
if (isDeployAlreadyInProgress(deployResponse)) {
|
|
958
|
-
return buildAlreadyInProgressResult({
|
|
959
|
-
active: deployResponse,
|
|
960
|
-
app,
|
|
961
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
962
|
-
cwd: ctx.cwd,
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
deployment = deployResponse;
|
|
966
|
-
}
|
|
967
|
-
catch (err) {
|
|
968
|
-
if (err instanceof PercherApiError && err.code === "RETRY_LIMIT_REACHED") {
|
|
969
|
-
return buildRetryLimitReachedResult({
|
|
970
|
-
err,
|
|
971
|
-
app,
|
|
972
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
973
|
-
cwd: ctx.cwd,
|
|
974
|
-
});
|
|
975
|
-
}
|
|
976
|
-
throw err;
|
|
977
|
-
}
|
|
978
|
-
const replacedPreview = deployment.replacedPreview === true;
|
|
979
|
-
// FUTURE11 Phase 1 — same async opt-in as the main publishInner path.
|
|
980
|
-
// Without this, a caller that passed `waitForLive: false` and hit a
|
|
981
|
-
// 401 (interactive login flow) would silently fall through to the
|
|
982
|
-
// 5-minute polling loop after login, breaking the contract that
|
|
983
|
-
// waitForLive=false always returns as soon as the deploy is queued.
|
|
984
|
-
if (input.waitForLive === false) {
|
|
985
|
-
ctx.status(`Queued (${deployment.id}) — resume with percher_wait_for_deploy.`);
|
|
986
|
-
return {
|
|
987
|
-
status: "queued",
|
|
988
|
-
app,
|
|
989
|
-
deployment,
|
|
990
|
-
fileCount: freshTarball.fileCount,
|
|
991
|
-
bytes: freshTarball.bytes,
|
|
992
|
-
replacedPreview: replacedPreview || undefined,
|
|
993
|
-
firstDeploy: firstDeployRetry || undefined,
|
|
994
|
-
recovery: recoveryWait({
|
|
995
|
-
app: app.name,
|
|
996
|
-
deployId: deployment.id,
|
|
997
|
-
reasonCode: "deploy_queued",
|
|
998
|
-
}),
|
|
999
|
-
summary: `${app.name} deploy ${deployment.id} queued — resume with percher_wait_for_deploy.`,
|
|
1000
|
-
configPath: tomlPathFor(ctx.cwd),
|
|
1001
|
-
bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
1002
|
-
};
|
|
1003
|
-
}
|
|
1004
|
-
const timeoutMs = TIMEOUTS.clientPublishPoll;
|
|
1005
|
-
const start = Date.now();
|
|
1006
|
-
while (deployment.status !== "live" &&
|
|
1007
|
-
deployment.status !== "failed" &&
|
|
1008
|
-
deployment.status !== "replaced") {
|
|
1009
|
-
if (Date.now() - start > timeoutMs) {
|
|
1010
|
-
return {
|
|
1011
|
-
status: "failed",
|
|
1012
|
-
app,
|
|
1013
|
-
deployment,
|
|
1014
|
-
fileCount: freshTarball.fileCount,
|
|
1015
|
-
bytes: freshTarball.bytes,
|
|
1016
|
-
error: {
|
|
1017
|
-
title: "Deploy timed out",
|
|
1018
|
-
explanation: "The deployment did not complete within 5 minutes.",
|
|
1019
|
-
suggestion: `Run \`percher doctor --app ${app.name}\` (CLI) or the percher_doctor tool (MCP) to diagnose the stall. Doctor will read deploy state + build log and return a concrete next step.`,
|
|
1020
|
-
},
|
|
1021
|
-
// FUTURE12 Phase 4: ambiguous stall — let doctor classify
|
|
1022
|
-
// before the agent dives into the raw log.
|
|
1023
|
-
recovery: recoveryDoctor({
|
|
1024
|
-
app: app.name,
|
|
1025
|
-
deployId: deployment.id,
|
|
1026
|
-
mode: "deploy",
|
|
1027
|
-
reasonCode: "deploy_stalled",
|
|
1028
|
-
}),
|
|
1029
|
-
summary: `Deploy timed out after 5 minutes (${deployment.id}).`,
|
|
1030
|
-
configPath: tomlPathFor(ctx.cwd),
|
|
1031
|
-
bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
ctx.status(`${deployment.status}...`);
|
|
1035
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
1036
|
-
deployment = await ctx.client.apps.getDeployment(app.id, deployment.id);
|
|
1037
|
-
}
|
|
1038
|
-
if (deployment.status === "replaced") {
|
|
1039
|
-
return await buildReplacedResult({
|
|
1040
|
-
ctx,
|
|
1041
|
-
app,
|
|
1042
|
-
deployment,
|
|
1043
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
1044
|
-
cwd: ctx.cwd,
|
|
1045
|
-
});
|
|
1046
|
-
}
|
|
1047
|
-
if (deployment.status === "failed") {
|
|
1048
|
-
return buildFailureResult({
|
|
1049
|
-
ctx,
|
|
1050
|
-
app,
|
|
1051
|
-
deployment,
|
|
1052
|
-
tarball: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
1053
|
-
input,
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
const totalSeconds = (Date.now() - start) / 1000;
|
|
1057
|
-
const url = deployment.previewUrl ?? deployment.url ?? app.url;
|
|
1058
|
-
ctx.status(`Live at ${url} (${totalSeconds.toFixed(0)}s)`);
|
|
1059
|
-
return {
|
|
1060
|
-
status: "live",
|
|
1061
|
-
url,
|
|
819
|
+
const packageMs = Date.now() - t0;
|
|
820
|
+
const { app, firstDeploy } = await ensureApp(ctx, config);
|
|
821
|
+
// The advisory build-env scan doesn't run on this path (the 401 fired
|
|
822
|
+
// before publishInner reached it); empty keeps the field absent.
|
|
823
|
+
return executeDeploy({
|
|
824
|
+
ctx,
|
|
825
|
+
config,
|
|
826
|
+
input,
|
|
1062
827
|
app,
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
},
|
|
1070
|
-
fileCount: freshTarball.fileCount,
|
|
1071
|
-
bytes: freshTarball.bytes,
|
|
1072
|
-
replacedPreview,
|
|
1073
|
-
firstDeploy: firstDeployRetry || undefined,
|
|
1074
|
-
recovery: RECOVERY_NONE,
|
|
1075
|
-
cacheHit: deployment.cacheHit,
|
|
1076
|
-
// Phase 7.9 — operator-facing trace key, same shape the primary
|
|
1077
|
-
// success path returns. Codex P2 follow-up on daf063f: the
|
|
1078
|
-
// post-login retry path was returning a PublishResult without
|
|
1079
|
-
// `traceId`, so a user who started unauthenticated, logged in,
|
|
1080
|
-
// and then succeeded got no `Trace: dep_xxx` line from the CLI
|
|
1081
|
-
// and the correlation key was missing for that publish.
|
|
1082
|
-
traceId: deployment.id,
|
|
1083
|
-
summary: buildLiveSummary({
|
|
1084
|
-
appName: app.name,
|
|
1085
|
-
url: url ?? app.url,
|
|
1086
|
-
totalSeconds: Math.round(totalSeconds),
|
|
1087
|
-
framework: config.app.framework,
|
|
1088
|
-
preview: input.preview === true,
|
|
1089
|
-
replacedPreview,
|
|
1090
|
-
// Read directly off the polled deployment row — same source of
|
|
1091
|
-
// truth as the primary publish path. Codex P2 follow-up on
|
|
1092
|
-
// 9e5ceee: the previous hardcoded `false` here meant the user
|
|
1093
|
-
// who hit the post-login retry branch always saw "Build cache:
|
|
1094
|
-
// miss" regardless of whether the build actually used the cache.
|
|
1095
|
-
cacheHit: deployment.cacheHit ?? false,
|
|
1096
|
-
}),
|
|
1097
|
-
configPath: tomlPathFor(ctx.cwd),
|
|
1098
|
-
bundle: { fileCount: freshTarball.fileCount, bytes: freshTarball.bytes },
|
|
1099
|
-
};
|
|
828
|
+
firstDeploy,
|
|
829
|
+
tarball: freshTarball,
|
|
830
|
+
t0,
|
|
831
|
+
packageMs,
|
|
832
|
+
missingBuildEnvKeys: [],
|
|
833
|
+
});
|
|
1100
834
|
}
|
|
1101
835
|
// ── Helpers ────────────────────────────────────────────────────────────
|
|
1102
836
|
async function loadOrGenerate(ctx) {
|