gencow 0.1.216 → 0.1.218
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/lib/app-delete-operation.mjs +15 -1
- package/lib/deploy-package-runtime.mjs +50 -4
- package/lib/deploy-runtime.mjs +8 -0
- package/lib/deploy-static-fullstack-runtime.mjs +8 -1
- package/lib/deployment-operation-poll.mjs +25 -0
- package/lib/init-command.mjs +4 -1
- package/lib/project-dependency-lock-sync.mjs +33 -9
- package/lib/release-client.mjs +7 -8
- package/lib/static-deploy-command.mjs +8 -1
- package/package.json +2 -2
- package/runtime/server.mjs +501 -415
|
@@ -76,6 +76,10 @@ function invalidPollResponse(accepted, response = null) {
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function isRetryableStatusPollFailure(response) {
|
|
80
|
+
return response?.status === 503 || response?.status >= 500;
|
|
81
|
+
}
|
|
82
|
+
|
|
79
83
|
function isMatchingTerminalResponse(value, accepted, expectedAppId) {
|
|
80
84
|
return (
|
|
81
85
|
isExactAppDeleteResponse(value, expectedAppId) &&
|
|
@@ -125,7 +129,17 @@ export async function pollAppDeleteOperation({
|
|
|
125
129
|
operationId: accepted.operationId,
|
|
126
130
|
});
|
|
127
131
|
const statusData = await readJsonObjectResponse(statusRes);
|
|
128
|
-
|
|
132
|
+
// A lifecycle operation is durable. A transient 5xx while the status
|
|
133
|
+
// query is being served must not turn that durable receipt into a client
|
|
134
|
+
// contract failure or abandon the operation's correlation identity.
|
|
135
|
+
if (!statusRes?.ok || !statusData) {
|
|
136
|
+
if (isRetryableStatusPollFailure(statusRes)) {
|
|
137
|
+
transportFailures += 1;
|
|
138
|
+
if (attempt + 1 < pollAttempts) await sleepImpl(pollIntervalMs);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
return invalidPollResponse(accepted, statusRes);
|
|
142
|
+
}
|
|
129
143
|
successfulPollResponses += 1;
|
|
130
144
|
if (isMatchingTerminalResponse(statusData, accepted, name)) return statusData;
|
|
131
145
|
if (isMatchingTerminalFailureResponse(statusData, accepted, name)) {
|
|
@@ -41,6 +41,37 @@ import {
|
|
|
41
41
|
} from "./deploy-lockfile-selection.mjs";
|
|
42
42
|
import { detectProjectConfigFile, warnProjectConfigSelectionOnce } from "./project-config-selection.mjs";
|
|
43
43
|
import { writeCanonicalDeployBundle } from "./deploy-bundle-staging.mjs";
|
|
44
|
+
|
|
45
|
+
const INITIAL_DEPLOYMENT_ADMISSION_ATTEMPTS = 3;
|
|
46
|
+
|
|
47
|
+
function isRetryableInitialDeploymentAdmission(response) {
|
|
48
|
+
return Number.isInteger(response?.status) && response.status >= 500 && response.status <= 599;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A deploy claim is an idempotency key. Before the Platform returns the
|
|
53
|
+
* operation receipt, a transport/5xx outcome is intentionally ambiguous;
|
|
54
|
+
* resubmit only that same claim a bounded number of times rather than making
|
|
55
|
+
* the customer invent a second operation.
|
|
56
|
+
*/
|
|
57
|
+
export async function submitDeploymentWithIdempotentRetry({
|
|
58
|
+
submit,
|
|
59
|
+
sleepImpl = (ms) => new Promise((resolveDelay) => setTimeout(resolveDelay, ms)),
|
|
60
|
+
}) {
|
|
61
|
+
let response;
|
|
62
|
+
for (let attempt = 1; attempt <= INITIAL_DEPLOYMENT_ADMISSION_ATTEMPTS; attempt += 1) {
|
|
63
|
+
response = await submit();
|
|
64
|
+
if (
|
|
65
|
+
!isRetryableInitialDeploymentAdmission(response) ||
|
|
66
|
+
attempt === INITIAL_DEPLOYMENT_ADMISSION_ATTEMPTS
|
|
67
|
+
) {
|
|
68
|
+
return response;
|
|
69
|
+
}
|
|
70
|
+
await sleepImpl(250 * attempt);
|
|
71
|
+
}
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
74
|
+
|
|
44
75
|
async function loadProjectConfigDefault(options) {
|
|
45
76
|
const { loadConfig } = await import("./cli-project-runtime.mjs");
|
|
46
77
|
return loadConfig(options);
|
|
@@ -637,6 +668,15 @@ export function createDeployPackageRuntime({
|
|
|
637
668
|
platformFetch: platformFetchImpl,
|
|
638
669
|
setTimeoutImpl,
|
|
639
670
|
});
|
|
671
|
+
const submitDeployAdmission = () => {
|
|
672
|
+
// A legacy request without a deploy claim has no stable pre-receipt
|
|
673
|
+
// identity. Never retry that request automatically.
|
|
674
|
+
if (!activeDeployClaim) return deployOnce(targetAppId);
|
|
675
|
+
return submitDeploymentWithIdempotentRetry({
|
|
676
|
+
submit: () => deployOnce(targetAppId),
|
|
677
|
+
sleepImpl: (ms) => new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, ms)),
|
|
678
|
+
});
|
|
679
|
+
};
|
|
640
680
|
|
|
641
681
|
const showCrashLogs = async (errData, title, targetAppId) => {
|
|
642
682
|
let diagnosticLines = normalizeDiagnosticLogLines(errData?.crashLogs);
|
|
@@ -661,7 +701,7 @@ export function createDeployPackageRuntime({
|
|
|
661
701
|
const spinner = startSpinner("Deploying...");
|
|
662
702
|
let deployRes;
|
|
663
703
|
try {
|
|
664
|
-
deployRes = await
|
|
704
|
+
deployRes = await submitDeployAdmission();
|
|
665
705
|
if (deployRes.status === 202) {
|
|
666
706
|
deployRes = await pollAcceptedDeployment(deployRes);
|
|
667
707
|
}
|
|
@@ -698,7 +738,7 @@ export function createDeployPackageRuntime({
|
|
|
698
738
|
infoImpl("Retrying deploy...");
|
|
699
739
|
const retrySpinner = startSpinner("Deploying...");
|
|
700
740
|
try {
|
|
701
|
-
deployRes = await
|
|
741
|
+
deployRes = await submitDeployAdmission();
|
|
702
742
|
if (deployRes.status === 202) {
|
|
703
743
|
deployRes = await pollAcceptedDeployment(deployRes);
|
|
704
744
|
}
|
|
@@ -737,7 +777,9 @@ export function createDeployPackageRuntime({
|
|
|
737
777
|
|
|
738
778
|
const deployData = await readJsonObjectResponse(deployRes);
|
|
739
779
|
if (!deployData) {
|
|
740
|
-
errorImpl(
|
|
780
|
+
errorImpl(
|
|
781
|
+
`Deploy failed: no terminal deployment receipt was returned after packaging.\nCode: CLI_DEPLOY_TERMINAL_RECEIPT_MISSING\nStage: controller\nHTTP status: ${deployRes?.status ?? "unknown"}\nNext action: RETRY`,
|
|
782
|
+
);
|
|
741
783
|
exitImpl(1);
|
|
742
784
|
return null;
|
|
743
785
|
}
|
|
@@ -748,7 +790,11 @@ export function createDeployPackageRuntime({
|
|
|
748
790
|
successImpl(`Server build complete! (${deployElapsed}s)`);
|
|
749
791
|
|
|
750
792
|
if (deployData.url) {
|
|
751
|
-
const ready = await verifyAppReadyImpl(
|
|
793
|
+
const ready = await verifyAppReadyImpl(
|
|
794
|
+
releaseAttemptId
|
|
795
|
+
? { appUrl: deployData.url, appId: targetAppId, announceReady: false }
|
|
796
|
+
: { appUrl: deployData.url, appId: targetAppId },
|
|
797
|
+
);
|
|
752
798
|
if (!ready) {
|
|
753
799
|
errorImpl("Deploy failed: app did not become ready after deployment.");
|
|
754
800
|
await showCrashLogs(null, "Readiness failure diagnostics:", targetAppId);
|
package/lib/deploy-runtime.mjs
CHANGED
|
@@ -144,6 +144,13 @@ export function readDeployProjectContext({
|
|
|
144
144
|
return { appId, displayName, gencowJsonPath, prodAppId, projectDir };
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
// gencow.json deliberately keeps the first production deployment separate
|
|
148
|
+
// from the optional development app. Every production operation must resolve
|
|
149
|
+
// that persisted production identity before it can create or deploy anything.
|
|
150
|
+
export function resolveEnvironmentDeployTarget({ appId = null, envTarget = "dev", prodAppId = null } = {}) {
|
|
151
|
+
return envTarget === "prod" ? (prodAppId ?? appId) : appId;
|
|
152
|
+
}
|
|
153
|
+
|
|
147
154
|
function showUnknownDeployArgument(arg, { errorImpl = error, infoImpl = info, logImpl = log } = {}) {
|
|
148
155
|
errorImpl(`Unknown deploy argument: "${arg}"`);
|
|
149
156
|
logImpl("");
|
|
@@ -516,6 +523,7 @@ export function createDeployCommand({
|
|
|
516
523
|
resolvePathImpl,
|
|
517
524
|
});
|
|
518
525
|
let { appId, displayName, gencowJsonPath, prodAppId, projectDir } = context;
|
|
526
|
+
appId = resolveEnvironmentDeployTarget({ appId, envTarget: parsed.envTarget, prodAppId });
|
|
519
527
|
const backendProjectRoot = projectDir ?? projectRoot;
|
|
520
528
|
const projectConfig = loadConfigImpl
|
|
521
529
|
? await loadConfigImpl({
|
|
@@ -5,6 +5,11 @@ import { createDeployPackageRuntime, writeProjectMetadata } from "./deploy-packa
|
|
|
5
5
|
import { BOLD, CYAN, DIM, RESET, error, info, log, success } from "./output.mjs";
|
|
6
6
|
import { preflightStaticDeployArtifact } from "./static-deploy-command.mjs";
|
|
7
7
|
|
|
8
|
+
function shouldPreserveFinalizeAttempt(error) {
|
|
9
|
+
return error?.retryable === true ||
|
|
10
|
+
error?.message === "APP_SERVING_ROUTE_COMMIT_PENDING";
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export function createScopedDeployPackageRuntime({
|
|
9
14
|
buildEnvImpl,
|
|
10
15
|
buildDeployClaimFromArchiveImpl,
|
|
@@ -181,7 +186,9 @@ export async function deployBackendPackage({
|
|
|
181
186
|
try {
|
|
182
187
|
await finalizeReleaseAttempt({ appId: deployed.appId, attemptId: releaseAttempt.attemptId });
|
|
183
188
|
} catch (caught) {
|
|
184
|
-
|
|
189
|
+
if (!shouldPreserveFinalizeAttempt(caught)) {
|
|
190
|
+
await markAttemptFailed("finalize", "APP_RELEASE_FINALIZE_FAILED");
|
|
191
|
+
}
|
|
185
192
|
throw caught;
|
|
186
193
|
}
|
|
187
194
|
successImpl("App Ready! (release finalized)");
|
|
@@ -63,6 +63,15 @@ function isNonterminalResponse(response, body, identity) {
|
|
|
63
63
|
);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function isTransientPinnedOperationRead(response, expected) {
|
|
67
|
+
return (
|
|
68
|
+
expected.operationId !== null &&
|
|
69
|
+
Number.isInteger(response?.status) &&
|
|
70
|
+
response.status >= 500 &&
|
|
71
|
+
response.status <= 599
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
export async function pollAcceptedDeploymentOperation({
|
|
67
76
|
initialResponse,
|
|
68
77
|
creds,
|
|
@@ -153,6 +162,22 @@ export async function pollAcceptedDeploymentOperation({
|
|
|
153
162
|
response = polledResponse;
|
|
154
163
|
continue;
|
|
155
164
|
}
|
|
165
|
+
// Once the server has sealed the operation identity, a gateway/database
|
|
166
|
+
// read failure cannot safely be presented as that operation's terminal
|
|
167
|
+
// result. Keep polling the same receipt; never submit a second deploy.
|
|
168
|
+
if (isTransientPinnedOperationRead(response, expected)) {
|
|
169
|
+
if (nowImpl() >= deadlineMs) return pendingResponse();
|
|
170
|
+
const retryAfter = Number(response.headers?.get?.("Retry-After"));
|
|
171
|
+
const retryDelayMs =
|
|
172
|
+
Number.isFinite(retryAfter) && retryAfter >= 1 && retryAfter <= 10 ? retryAfter * 1000 : 1000;
|
|
173
|
+
const remainingMs = Math.max(0, deadlineMs - nowImpl());
|
|
174
|
+
await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, Math.min(retryDelayMs, remainingMs)));
|
|
175
|
+
if (nowImpl() >= deadlineMs) return pendingResponse();
|
|
176
|
+
const polledResponse = await fetchWithinDeadline();
|
|
177
|
+
if (!polledResponse) return pendingResponse();
|
|
178
|
+
response = polledResponse;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
156
181
|
if (
|
|
157
182
|
expected.operationId === null ||
|
|
158
183
|
!TERMINAL_STATES.has(identity.state) ||
|
package/lib/init-command.mjs
CHANGED
|
@@ -587,7 +587,10 @@ export function createInitCommand({
|
|
|
587
587
|
successImpl("Dependencies installed");
|
|
588
588
|
} catch (caught) {
|
|
589
589
|
installStatus = "failed";
|
|
590
|
-
|
|
590
|
+
const reason = typeof caught?.reason === "string" ? ` (${caught.reason})` : "";
|
|
591
|
+
warnImpl(
|
|
592
|
+
`${caught?.code ?? "CLI_INIT_DEPENDENCY_INSTALL_FAILED"}${reason}: Dependency setup failed.`,
|
|
593
|
+
);
|
|
591
594
|
exitImpl(1);
|
|
592
595
|
return;
|
|
593
596
|
}
|
|
@@ -15,6 +15,33 @@ function corepackCommand(platform) {
|
|
|
15
15
|
return platform === "win32" ? "corepack.cmd" : "corepack";
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function isCommandUnavailable(error) {
|
|
19
|
+
return error?.code === "ENOENT";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function installProjectDependencies(projectDir, deps) {
|
|
23
|
+
const pnpmArgs = [approvedPnpmSelector(), "install", "--ignore-workspace"];
|
|
24
|
+
try {
|
|
25
|
+
deps.execFileSyncImpl(corepackCommand(deps.platform), pnpmArgs, {
|
|
26
|
+
cwd: projectDir,
|
|
27
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
28
|
+
});
|
|
29
|
+
return { executor: "corepack" };
|
|
30
|
+
} catch (error) {
|
|
31
|
+
// Corepack is deliberately preferred because it is Node's package-manager
|
|
32
|
+
// authority. Some supported CLI hosts intentionally provide only Bun;
|
|
33
|
+
// boot the *same pinned pnpm* through Bun rather than switching lockfile
|
|
34
|
+
// authorities or requiring a machine-global install.
|
|
35
|
+
if (!isCommandUnavailable(error)) throw error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
deps.execFileSyncImpl(deps.bunRuntimePath, ["x", "--bun", ...pnpmArgs], {
|
|
39
|
+
cwd: projectDir,
|
|
40
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
41
|
+
});
|
|
42
|
+
return { executor: "bun-pnpm" };
|
|
43
|
+
}
|
|
44
|
+
|
|
18
45
|
function readProjectDependencyFiles(projectDir, deps) {
|
|
19
46
|
const packagePath = resolve(projectDir, PACKAGE_FILE);
|
|
20
47
|
const lockPath = resolve(projectDir, LOCK_FILE);
|
|
@@ -65,6 +92,7 @@ function restoreFile(path, previous, deps) {
|
|
|
65
92
|
|
|
66
93
|
function dependencyIoDeps(overrides = {}) {
|
|
67
94
|
return {
|
|
95
|
+
bunRuntimePath: overrides.bunRuntimePath ?? process.execPath,
|
|
68
96
|
execFileSyncImpl: overrides.execFileSyncImpl ?? execFileSync,
|
|
69
97
|
existsSyncImpl: overrides.existsSyncImpl ?? existsSync,
|
|
70
98
|
readFileSyncImpl: overrides.readFileSyncImpl ?? readFileSync,
|
|
@@ -77,17 +105,13 @@ function dependencyIoDeps(overrides = {}) {
|
|
|
77
105
|
export function installAndVerifyProjectDependencies(projectDir, overrides = {}) {
|
|
78
106
|
const deps = dependencyIoDeps(overrides);
|
|
79
107
|
try {
|
|
80
|
-
deps
|
|
81
|
-
|
|
82
|
-
[approvedPnpmSelector(), "install", "--ignore-workspace"],
|
|
83
|
-
{
|
|
84
|
-
cwd: projectDir,
|
|
85
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
86
|
-
},
|
|
87
|
-
);
|
|
88
|
-
} catch {
|
|
108
|
+
installProjectDependencies(projectDir, deps);
|
|
109
|
+
} catch (cause) {
|
|
89
110
|
const error = new Error("CLI_INIT_DEPENDENCY_INSTALL_FAILED");
|
|
90
111
|
error.code = "CLI_INIT_DEPENDENCY_INSTALL_FAILED";
|
|
112
|
+
error.reason = isCommandUnavailable(cause)
|
|
113
|
+
? "APPROVED_PNPM_EXECUTOR_UNAVAILABLE"
|
|
114
|
+
: "APPROVED_PNPM_INSTALL_FAILED";
|
|
91
115
|
throw error;
|
|
92
116
|
}
|
|
93
117
|
return assertDeployValidLock(projectDir, deps, "CLI_INIT_LOCK_POSTCONDITION_FAILED");
|
package/lib/release-client.mjs
CHANGED
|
@@ -24,6 +24,7 @@ const PUBLIC_RELEASE_ERROR_CODES = new Set([
|
|
|
24
24
|
"APP_RELEASE_ENVIRONMENT_INVALID",
|
|
25
25
|
"APP_RELEASE_ENVIRONMENT_MISMATCH",
|
|
26
26
|
"APP_RELEASE_ENV_KEYS_MISSING",
|
|
27
|
+
"APP_RELEASE_FINALIZATION_PENDING",
|
|
27
28
|
"APP_RELEASE_IDEMPOTENCY_CONFLICT",
|
|
28
29
|
"APP_RELEASE_METADATA_INVALID",
|
|
29
30
|
"APP_RELEASE_POLICY_READ_UNAVAILABLE",
|
|
@@ -113,7 +114,8 @@ export async function prepareReleaseAttempt({
|
|
|
113
114
|
capabilityTimeoutSetTimeoutImpl = setTimeout,
|
|
114
115
|
capabilityTimeoutClearTimeoutImpl = clearTimeout,
|
|
115
116
|
}) {
|
|
116
|
-
const capabilityRequest = (signal) =>
|
|
117
|
+
const capabilityRequest = (signal) =>
|
|
118
|
+
releaseCapability({ creds, appId, environment, platformFetchImpl, signal });
|
|
117
119
|
let capability;
|
|
118
120
|
if (Number.isFinite(capabilityTimeoutMs) && capabilityTimeoutMs > 0) {
|
|
119
121
|
let timeout;
|
|
@@ -122,13 +124,10 @@ export async function prepareReleaseAttempt({
|
|
|
122
124
|
capability = await Promise.race([
|
|
123
125
|
capabilityRequest(controller.signal),
|
|
124
126
|
new Promise((_, reject) => {
|
|
125
|
-
timeout = capabilityTimeoutSetTimeoutImpl(
|
|
126
|
-
()
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
},
|
|
130
|
-
capabilityTimeoutMs,
|
|
131
|
-
);
|
|
127
|
+
timeout = capabilityTimeoutSetTimeoutImpl(() => {
|
|
128
|
+
controller.abort();
|
|
129
|
+
reject(new Error("APP_RELEASE_CAPABILITY_TIMEOUT"));
|
|
130
|
+
}, capabilityTimeoutMs);
|
|
132
131
|
timeout?.unref?.();
|
|
133
132
|
}),
|
|
134
133
|
]);
|
|
@@ -20,6 +20,11 @@ import { getCliInvocationSelection } from "./project-context.mjs";
|
|
|
20
20
|
import { pollAcceptedDeploymentOperation } from "./deployment-operation-poll.mjs";
|
|
21
21
|
import { writeProjectMetadata } from "./deploy-project-metadata.mjs";
|
|
22
22
|
|
|
23
|
+
function shouldPreserveFinalizeAttempt(error) {
|
|
24
|
+
return error?.retryable === true ||
|
|
25
|
+
error?.message === "APP_SERVING_ROUTE_COMMIT_PENDING";
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
export function resolveStaticDeployDir({
|
|
24
29
|
staticDirArg,
|
|
25
30
|
cwd = process.cwd(),
|
|
@@ -447,7 +452,9 @@ export function createStaticDeployRuntime({
|
|
|
447
452
|
platformFetchImpl,
|
|
448
453
|
});
|
|
449
454
|
} catch (caught) {
|
|
450
|
-
|
|
455
|
+
if (!shouldPreserveFinalizeAttempt(caught)) {
|
|
456
|
+
await markAttemptFailed("finalize", "APP_RELEASE_FINALIZE_FAILED");
|
|
457
|
+
}
|
|
451
458
|
throw caught;
|
|
452
459
|
}
|
|
453
460
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gencow",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.218",
|
|
4
4
|
"description": "Gencow — AI Backend Engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^25.9.5",
|
|
35
35
|
"better-auth": "^1.6.23",
|
|
36
|
-
"@gencow/core": "0.1.43",
|
|
37
36
|
"@gencow/client": "0.2.7",
|
|
37
|
+
"@gencow/core": "0.1.43",
|
|
38
38
|
"@gencow/react": "0.2.7"
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|