@run402/sdk 1.69.7 → 1.70.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/README.md +7 -5
- package/dist/namespaces/auth.d.ts.map +1 -1
- package/dist/namespaces/auth.js +60 -6
- package/dist/namespaces/auth.js.map +1 -1
- package/dist/namespaces/billing.d.ts.map +1 -1
- package/dist/namespaces/billing.js +42 -14
- package/dist/namespaces/billing.js.map +1 -1
- package/dist/namespaces/deploy.d.ts.map +1 -1
- package/dist/namespaces/deploy.js +452 -12
- package/dist/namespaces/deploy.js.map +1 -1
- package/dist/namespaces/deploy.types.d.ts +11 -0
- package/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/dist/namespaces/deploy.types.js.map +1 -1
- package/dist/namespaces/email.d.ts.map +1 -1
- package/dist/namespaces/email.js +55 -12
- package/dist/namespaces/email.js.map +1 -1
- package/dist/namespaces/projects.d.ts.map +1 -1
- package/dist/namespaces/projects.js +7 -2
- package/dist/namespaces/projects.js.map +1 -1
- package/dist/namespaces/tier.d.ts +16 -0
- package/dist/namespaces/tier.d.ts.map +1 -1
- package/dist/namespaces/tier.js.map +1 -1
- package/dist/node/deploy-manifest.d.ts +2 -0
- package/dist/node/deploy-manifest.d.ts.map +1 -1
- package/dist/node/deploy-manifest.js +24 -1
- package/dist/node/deploy-manifest.js.map +1 -1
- package/dist/validation.d.ts +6 -3
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +30 -0
- package/dist/validation.js.map +1 -1
- package/package.json +1 -1
|
@@ -37,6 +37,38 @@ const SECRET_KEY_RE = /^[A-Z_][A-Z0-9_]{0,127}$/;
|
|
|
37
37
|
const APPLY_SAFE_RETRY_CODES = new Set([
|
|
38
38
|
"BASE_RELEASE_CONFLICT",
|
|
39
39
|
]);
|
|
40
|
+
const STATIC_ACTIVATION_FAILURE_CODES = new Set([
|
|
41
|
+
"BAD_FIELD",
|
|
42
|
+
"INVALID_SPEC",
|
|
43
|
+
"FUNCTION_ACTIVATE_FAILED",
|
|
44
|
+
"FUNCTION_CONFIG_INVALID",
|
|
45
|
+
"FUNCTION_TIMEOUT_EXCEEDS_TIER",
|
|
46
|
+
"FUNCTION_MEMORY_EXCEEDS_TIER",
|
|
47
|
+
"FUNCTION_SCHEDULE_EXCEEDS_TIER",
|
|
48
|
+
"FUNCTION_SCHEDULE_INTERVAL_TOO_SHORT",
|
|
49
|
+
"FUNCTION_SCHEDULE_LIMIT_EXCEEDED",
|
|
50
|
+
"TIER_LIMIT_EXCEEDED",
|
|
51
|
+
]);
|
|
52
|
+
const STATIC_FUNCTION_LIMITS_BY_TIER = {
|
|
53
|
+
prototype: {
|
|
54
|
+
maxTimeoutSeconds: 10,
|
|
55
|
+
maxMemoryMb: 128,
|
|
56
|
+
maxScheduledFunctions: 1,
|
|
57
|
+
minCronIntervalMinutes: 15,
|
|
58
|
+
},
|
|
59
|
+
hobby: {
|
|
60
|
+
maxTimeoutSeconds: 30,
|
|
61
|
+
maxMemoryMb: 256,
|
|
62
|
+
maxScheduledFunctions: 3,
|
|
63
|
+
minCronIntervalMinutes: 5,
|
|
64
|
+
},
|
|
65
|
+
team: {
|
|
66
|
+
maxTimeoutSeconds: 60,
|
|
67
|
+
maxMemoryMb: 512,
|
|
68
|
+
maxScheduledFunctions: 10,
|
|
69
|
+
minCronIntervalMinutes: 1,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
40
72
|
const MANIFEST_CONTENT_TYPE = "application/vnd.run402.deploy-manifest+json";
|
|
41
73
|
const TERMINAL_STATUSES = [
|
|
42
74
|
"ready",
|
|
@@ -331,11 +363,12 @@ function requireNonEmptyStringQueryOption(value, label, context) {
|
|
|
331
363
|
}
|
|
332
364
|
// ─── Internal pipeline ───────────────────────────────────────────────────────
|
|
333
365
|
async function applyOnce(client, spec, opts, emit) {
|
|
366
|
+
const allowWarningCodes = normalizeAllowWarningCodes(opts.allowWarningCodes);
|
|
334
367
|
emit({ type: "plan.started" });
|
|
335
368
|
const { plan, byteReaders } = await planInternal(client, spec, opts.idempotencyKey);
|
|
336
369
|
emit({ type: "plan.diff", diff: plan.diff });
|
|
337
370
|
emitPlanWarnings(plan, emit);
|
|
338
|
-
abortOnConfirmationWarnings(plan, opts);
|
|
371
|
+
abortOnConfirmationWarnings(plan, opts, allowWarningCodes);
|
|
339
372
|
if (plan.payment_required) {
|
|
340
373
|
emit({
|
|
341
374
|
type: "payment.required",
|
|
@@ -420,6 +453,7 @@ async function planInternal(client, spec, idempotencyKey, dryRun = false) {
|
|
|
420
453
|
if (ciCredentials)
|
|
421
454
|
assertCiDeployableSpec(spec);
|
|
422
455
|
const { normalized, byteReaders } = await normalizeReleaseSpec(client, spec);
|
|
456
|
+
await preflightTierFunctionLimits(client, normalized, ciCredentials);
|
|
423
457
|
// The gateway expects { spec, manifest_ref?, idempotency_key? } with
|
|
424
458
|
// ReleaseSpec.project (singular). For oversized specs the SDK uploads
|
|
425
459
|
// the manifest JSON to CAS first and references it; the gateway still
|
|
@@ -474,6 +508,338 @@ async function planInternal(client, spec, idempotencyKey, dryRun = false) {
|
|
|
474
508
|
}
|
|
475
509
|
return { plan, byteReaders };
|
|
476
510
|
}
|
|
511
|
+
async function preflightTierFunctionLimits(client, spec, ciCredentials) {
|
|
512
|
+
if (ciCredentials)
|
|
513
|
+
return;
|
|
514
|
+
if (!hasFunctionTierPreflightInputs(spec.functions))
|
|
515
|
+
return;
|
|
516
|
+
const limits = await readTierFunctionLimits(client);
|
|
517
|
+
if (!limits)
|
|
518
|
+
return;
|
|
519
|
+
const entries = collectFunctionPreflightEntries(spec.functions);
|
|
520
|
+
for (const entry of entries) {
|
|
521
|
+
const timeout = entry.fn.config?.timeoutSeconds;
|
|
522
|
+
if (timeout !== undefined &&
|
|
523
|
+
limits.maxTimeoutSeconds &&
|
|
524
|
+
timeout > limits.maxTimeoutSeconds.value) {
|
|
525
|
+
throw tierLimitError(`Function ${entry.name} timeoutSeconds ${timeout} exceeds the ${limits.tier} tier maximum of ${limits.maxTimeoutSeconds.value}.`, `${entry.fieldPrefix}.config.timeoutSeconds`, timeout, limits, limits.maxTimeoutSeconds, {
|
|
526
|
+
tier_max: limits.maxTimeoutSeconds.value,
|
|
527
|
+
max_function_timeout_seconds: limits.maxTimeoutSeconds.value,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const memory = entry.fn.config?.memoryMb;
|
|
531
|
+
if (memory !== undefined &&
|
|
532
|
+
limits.maxMemoryMb &&
|
|
533
|
+
memory > limits.maxMemoryMb.value) {
|
|
534
|
+
throw tierLimitError(`Function ${entry.name} memoryMb ${memory} exceeds the ${limits.tier} tier maximum of ${limits.maxMemoryMb.value}.`, `${entry.fieldPrefix}.config.memoryMb`, memory, limits, limits.maxMemoryMb, {
|
|
535
|
+
tier_max: limits.maxMemoryMb.value,
|
|
536
|
+
max_function_memory_mb: limits.maxMemoryMb.value,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
if (isScheduledCron(entry.fn.schedule) && limits.minCronIntervalMinutes) {
|
|
540
|
+
const intervalMinutes = estimateCronMinimumIntervalMinutes(entry.fn.schedule);
|
|
541
|
+
if (intervalMinutes !== null &&
|
|
542
|
+
intervalMinutes < limits.minCronIntervalMinutes.value) {
|
|
543
|
+
throw tierLimitError(`Function ${entry.name} schedule runs every ${intervalMinutes} minute(s), below the ${limits.tier} tier minimum interval of ${limits.minCronIntervalMinutes.value} minutes.`, `${entry.fieldPrefix}.schedule`, entry.fn.schedule, limits, limits.minCronIntervalMinutes, {
|
|
544
|
+
interval_minutes: intervalMinutes,
|
|
545
|
+
min_interval_minutes: limits.minCronIntervalMinutes.value,
|
|
546
|
+
min_cron_interval_minutes: limits.minCronIntervalMinutes.value,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (limits.maxScheduledFunctions) {
|
|
552
|
+
const lowerBound = countScheduledFunctionsInSetEntries(spec.functions);
|
|
553
|
+
if (lowerBound > limits.maxScheduledFunctions.value) {
|
|
554
|
+
throw scheduledCountTierLimitError(lowerBound, limits, limits.maxScheduledFunctions, "manifest");
|
|
555
|
+
}
|
|
556
|
+
const desired = await computeDesiredScheduledFunctionCount(client, spec);
|
|
557
|
+
if (desired && desired.count > limits.maxScheduledFunctions.value) {
|
|
558
|
+
throw scheduledCountTierLimitError(desired.count, limits, limits.maxScheduledFunctions, desired.source);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
function hasFunctionTierPreflightInputs(functions) {
|
|
563
|
+
if (!functions)
|
|
564
|
+
return false;
|
|
565
|
+
return collectFunctionPreflightEntries(functions).some((entry) => (entry.fn.config?.timeoutSeconds !== undefined ||
|
|
566
|
+
entry.fn.config?.memoryMb !== undefined ||
|
|
567
|
+
isScheduledCron(entry.fn.schedule)));
|
|
568
|
+
}
|
|
569
|
+
function collectFunctionPreflightEntries(functions) {
|
|
570
|
+
if (!functions)
|
|
571
|
+
return [];
|
|
572
|
+
const entries = [];
|
|
573
|
+
for (const [name, fn] of Object.entries(functions.replace ?? {})) {
|
|
574
|
+
entries.push({ name, fn, fieldPrefix: `functions.${name}` });
|
|
575
|
+
}
|
|
576
|
+
for (const [name, fn] of Object.entries(functions.patch?.set ?? {})) {
|
|
577
|
+
entries.push({ name, fn, fieldPrefix: `functions.${name}` });
|
|
578
|
+
}
|
|
579
|
+
return entries;
|
|
580
|
+
}
|
|
581
|
+
async function readTierFunctionLimits(client) {
|
|
582
|
+
let status;
|
|
583
|
+
try {
|
|
584
|
+
status = await client.request("/tiers/v1/status", {
|
|
585
|
+
context: "checking tier status for deploy preflight",
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
catch {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
if (typeof status.tier !== "string" || status.tier.length === 0) {
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
const tierKey = status.tier.toLowerCase();
|
|
595
|
+
const fallback = STATIC_FUNCTION_LIMITS_BY_TIER[tierKey];
|
|
596
|
+
const limits = { tier: status.tier };
|
|
597
|
+
limits.maxTimeoutSeconds = tierStatusLimitOrFallback(status, [
|
|
598
|
+
"max_function_timeout_seconds",
|
|
599
|
+
"max_timeout_seconds",
|
|
600
|
+
"timeout_seconds_max",
|
|
601
|
+
"function_timeout_seconds_max",
|
|
602
|
+
], fallback?.maxTimeoutSeconds);
|
|
603
|
+
limits.maxMemoryMb = tierStatusLimitOrFallback(status, [
|
|
604
|
+
"max_function_memory_mb",
|
|
605
|
+
"max_memory_mb",
|
|
606
|
+
"memory_mb_max",
|
|
607
|
+
"function_memory_mb_max",
|
|
608
|
+
], fallback?.maxMemoryMb);
|
|
609
|
+
limits.maxScheduledFunctions = tierStatusLimitOrFallback(status, [
|
|
610
|
+
"max_scheduled_functions",
|
|
611
|
+
"scheduled_functions_limit",
|
|
612
|
+
"scheduled_function_limit",
|
|
613
|
+
"max_function_schedules",
|
|
614
|
+
], fallback?.maxScheduledFunctions);
|
|
615
|
+
limits.minCronIntervalMinutes = tierStatusLimitOrFallback(status, [
|
|
616
|
+
"min_cron_interval_minutes",
|
|
617
|
+
"minimum_cron_interval_minutes",
|
|
618
|
+
"min_schedule_interval_minutes",
|
|
619
|
+
"min_scheduled_function_interval_minutes",
|
|
620
|
+
], fallback?.minCronIntervalMinutes);
|
|
621
|
+
limits.currentScheduledFunctions = tierStatusLimit(status, [
|
|
622
|
+
"current_scheduled_functions",
|
|
623
|
+
"current_scheduled_function_count",
|
|
624
|
+
"scheduled_function_count",
|
|
625
|
+
"scheduled_functions",
|
|
626
|
+
]);
|
|
627
|
+
return hasAnyTierFunctionLimit(limits) ? limits : null;
|
|
628
|
+
}
|
|
629
|
+
function tierStatusLimitOrFallback(status, keys, fallback) {
|
|
630
|
+
return tierStatusLimit(status, keys) ?? (fallback === undefined
|
|
631
|
+
? undefined
|
|
632
|
+
: { value: fallback, source: "local_static_fallback" });
|
|
633
|
+
}
|
|
634
|
+
function tierStatusLimit(status, keys) {
|
|
635
|
+
const limits = objectField(status, "limits");
|
|
636
|
+
const containers = [
|
|
637
|
+
objectField(status, "function_limits"),
|
|
638
|
+
objectField(limits, "functions"),
|
|
639
|
+
objectField(limits, "function_limits"),
|
|
640
|
+
objectField(status, "pool_usage"),
|
|
641
|
+
status,
|
|
642
|
+
];
|
|
643
|
+
for (const container of containers) {
|
|
644
|
+
if (!container || typeof container !== "object" || Array.isArray(container))
|
|
645
|
+
continue;
|
|
646
|
+
const obj = container;
|
|
647
|
+
for (const key of keys) {
|
|
648
|
+
const value = obj[key];
|
|
649
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
|
|
650
|
+
return { value, source: "tier_status" };
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return undefined;
|
|
655
|
+
}
|
|
656
|
+
function hasAnyTierFunctionLimit(limits) {
|
|
657
|
+
return Boolean(limits.maxTimeoutSeconds ||
|
|
658
|
+
limits.maxMemoryMb ||
|
|
659
|
+
limits.maxScheduledFunctions ||
|
|
660
|
+
limits.minCronIntervalMinutes ||
|
|
661
|
+
limits.currentScheduledFunctions);
|
|
662
|
+
}
|
|
663
|
+
async function computeDesiredScheduledFunctionCount(client, spec) {
|
|
664
|
+
const functions = spec.functions;
|
|
665
|
+
if (!functions)
|
|
666
|
+
return null;
|
|
667
|
+
const replaceScheduled = scheduledFunctionNames(functions.replace);
|
|
668
|
+
if (replaceScheduled) {
|
|
669
|
+
applyScheduledFunctionPatch(replaceScheduled, functions.patch);
|
|
670
|
+
return { count: replaceScheduled.size, source: "manifest" };
|
|
671
|
+
}
|
|
672
|
+
if (!functions.patch)
|
|
673
|
+
return null;
|
|
674
|
+
const activeScheduled = await readActiveScheduledFunctionNames(client, spec.project);
|
|
675
|
+
if (!activeScheduled)
|
|
676
|
+
return null;
|
|
677
|
+
applyScheduledFunctionPatch(activeScheduled, functions.patch);
|
|
678
|
+
return { count: activeScheduled.size, source: "active_release_inventory" };
|
|
679
|
+
}
|
|
680
|
+
function countScheduledFunctionsInSetEntries(functions) {
|
|
681
|
+
if (!functions)
|
|
682
|
+
return 0;
|
|
683
|
+
const names = new Set();
|
|
684
|
+
for (const [name, fn] of Object.entries(functions.replace ?? {})) {
|
|
685
|
+
if (isScheduledCron(fn.schedule))
|
|
686
|
+
names.add(name);
|
|
687
|
+
}
|
|
688
|
+
for (const [name, fn] of Object.entries(functions.patch?.set ?? {})) {
|
|
689
|
+
if (isScheduledCron(fn.schedule))
|
|
690
|
+
names.add(name);
|
|
691
|
+
}
|
|
692
|
+
return names.size;
|
|
693
|
+
}
|
|
694
|
+
function scheduledFunctionNames(functions) {
|
|
695
|
+
if (!functions)
|
|
696
|
+
return null;
|
|
697
|
+
const scheduled = new Set();
|
|
698
|
+
for (const [name, fn] of Object.entries(functions)) {
|
|
699
|
+
if (isScheduledCron(fn.schedule))
|
|
700
|
+
scheduled.add(name);
|
|
701
|
+
}
|
|
702
|
+
return scheduled;
|
|
703
|
+
}
|
|
704
|
+
function applyScheduledFunctionPatch(scheduled, patch) {
|
|
705
|
+
for (const name of patch?.delete ?? []) {
|
|
706
|
+
scheduled.delete(name);
|
|
707
|
+
}
|
|
708
|
+
for (const [name, fn] of Object.entries(patch?.set ?? {})) {
|
|
709
|
+
if (fn.schedule === null)
|
|
710
|
+
scheduled.delete(name);
|
|
711
|
+
else if (isScheduledCron(fn.schedule))
|
|
712
|
+
scheduled.add(name);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
async function readActiveScheduledFunctionNames(client, projectId) {
|
|
716
|
+
let inventory;
|
|
717
|
+
try {
|
|
718
|
+
inventory = await client.request(appendQuery("/deploy/v2/releases/active", { site_limit: 1 }), {
|
|
719
|
+
headers: await apikeyHeaders(client, projectId),
|
|
720
|
+
context: "fetching active release inventory for deploy preflight",
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
const scheduled = new Set();
|
|
727
|
+
for (const fn of inventory.functions ?? []) {
|
|
728
|
+
if (isScheduledCron(fn.schedule))
|
|
729
|
+
scheduled.add(fn.name);
|
|
730
|
+
}
|
|
731
|
+
return scheduled;
|
|
732
|
+
}
|
|
733
|
+
function scheduledCountTierLimitError(count, limits, limit, countSource) {
|
|
734
|
+
return tierLimitError(`Deploy would have ${count} scheduled function(s), exceeding the ${limits.tier} tier maximum of ${limit.value}.`, "functions.scheduled_count", count, limits, limit, {
|
|
735
|
+
tier_max: limit.value,
|
|
736
|
+
max_scheduled_functions: limit.value,
|
|
737
|
+
count_source: countSource,
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
function tierLimitError(message, field, value, limits, limit, extraDetails) {
|
|
741
|
+
const hint = limit.source === "local_static_fallback"
|
|
742
|
+
? "Tier limits came from the SDK's static fallback because /tiers/v1/status did not expose function caps. Run `run402 tier status` to refresh, lower the function setting, upgrade the tier, or retry and let gateway validation decide if this seems stale."
|
|
743
|
+
: "Lower the function setting or upgrade the tier before deploying.";
|
|
744
|
+
const details = {
|
|
745
|
+
field,
|
|
746
|
+
value,
|
|
747
|
+
tier: limits.tier,
|
|
748
|
+
limit_source: limit.source,
|
|
749
|
+
...extraDetails,
|
|
750
|
+
};
|
|
751
|
+
const body = {
|
|
752
|
+
code: "BAD_FIELD",
|
|
753
|
+
category: "deploy",
|
|
754
|
+
message,
|
|
755
|
+
retryable: false,
|
|
756
|
+
details,
|
|
757
|
+
hint,
|
|
758
|
+
};
|
|
759
|
+
return new Run402DeployError(message, {
|
|
760
|
+
code: "BAD_FIELD",
|
|
761
|
+
phase: "validate",
|
|
762
|
+
resource: field,
|
|
763
|
+
retryable: false,
|
|
764
|
+
body,
|
|
765
|
+
context: "validating deploy tier limits",
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
function isScheduledCron(value) {
|
|
769
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
770
|
+
}
|
|
771
|
+
function estimateCronMinimumIntervalMinutes(expression) {
|
|
772
|
+
const parts = expression.trim().split(/\s+/);
|
|
773
|
+
if (parts.length !== 5)
|
|
774
|
+
return null;
|
|
775
|
+
const minutes = expandCronNumberField(parts[0], 0, 59);
|
|
776
|
+
const hours = expandCronNumberField(parts[1], 0, 23);
|
|
777
|
+
if (!minutes || !hours || minutes.length === 0 || hours.length === 0)
|
|
778
|
+
return null;
|
|
779
|
+
const occurrences = [];
|
|
780
|
+
for (const hour of hours) {
|
|
781
|
+
for (const minute of minutes) {
|
|
782
|
+
occurrences.push(hour * 60 + minute);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
occurrences.sort((a, b) => a - b);
|
|
786
|
+
if (occurrences.length <= 1)
|
|
787
|
+
return 24 * 60;
|
|
788
|
+
let minGap = Number.POSITIVE_INFINITY;
|
|
789
|
+
for (let i = 1; i < occurrences.length; i += 1) {
|
|
790
|
+
minGap = Math.min(minGap, occurrences[i] - occurrences[i - 1]);
|
|
791
|
+
}
|
|
792
|
+
minGap = Math.min(minGap, 24 * 60 - occurrences[occurrences.length - 1] + occurrences[0]);
|
|
793
|
+
return Number.isFinite(minGap) ? minGap : null;
|
|
794
|
+
}
|
|
795
|
+
function expandCronNumberField(field, min, max) {
|
|
796
|
+
const values = new Set();
|
|
797
|
+
for (const part of field.split(",")) {
|
|
798
|
+
const expanded = expandCronNumberPart(part.trim(), min, max);
|
|
799
|
+
if (!expanded)
|
|
800
|
+
return null;
|
|
801
|
+
for (const value of expanded)
|
|
802
|
+
values.add(value);
|
|
803
|
+
}
|
|
804
|
+
return [...values].sort((a, b) => a - b);
|
|
805
|
+
}
|
|
806
|
+
function expandCronNumberPart(part, min, max) {
|
|
807
|
+
if (!part)
|
|
808
|
+
return null;
|
|
809
|
+
const [rangePart, stepPart] = part.split("/");
|
|
810
|
+
if (part.split("/").length > 2)
|
|
811
|
+
return null;
|
|
812
|
+
const step = stepPart === undefined ? 1 : Number(stepPart);
|
|
813
|
+
if (!Number.isSafeInteger(step) || step < 1)
|
|
814
|
+
return null;
|
|
815
|
+
let start;
|
|
816
|
+
let end;
|
|
817
|
+
if (rangePart === "*") {
|
|
818
|
+
start = min;
|
|
819
|
+
end = max;
|
|
820
|
+
}
|
|
821
|
+
else if (rangePart?.includes("-")) {
|
|
822
|
+
const [rawStart, rawEnd] = rangePart.split("-");
|
|
823
|
+
start = Number(rawStart);
|
|
824
|
+
end = Number(rawEnd);
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
start = Number(rangePart);
|
|
828
|
+
end = stepPart === undefined ? start : max;
|
|
829
|
+
}
|
|
830
|
+
if (!Number.isSafeInteger(start) ||
|
|
831
|
+
!Number.isSafeInteger(end) ||
|
|
832
|
+
start < min ||
|
|
833
|
+
end > max ||
|
|
834
|
+
start > end) {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
const values = [];
|
|
838
|
+
for (let value = start; value <= end; value += step) {
|
|
839
|
+
values.push(value);
|
|
840
|
+
}
|
|
841
|
+
return values;
|
|
842
|
+
}
|
|
477
843
|
async function commitInternal(client, planId, idempotencyKey) {
|
|
478
844
|
try {
|
|
479
845
|
return await client.request(`/deploy/v2/plans/${encodeURIComponent(planId)}/commit`, {
|
|
@@ -781,6 +1147,11 @@ async function pollSnapshotUntilReady(client, initial, diff, warnings, emit, pro
|
|
|
781
1147
|
closePreviousPhase(undefined, "failed");
|
|
782
1148
|
throw translateGatewayError(snapshot.error, snapshot.status, snapshot.plan_id, snapshot.operation_id);
|
|
783
1149
|
}
|
|
1150
|
+
if (snapshot.status === "activation_pending" &&
|
|
1151
|
+
isTerminalStaticActivationError(snapshot.error)) {
|
|
1152
|
+
closePreviousPhase(undefined, "failed");
|
|
1153
|
+
throw translateGatewayError(snapshot.error, "activate", snapshot.plan_id, snapshot.operation_id);
|
|
1154
|
+
}
|
|
784
1155
|
if (Date.now() - start > COMMIT_POLL_TIMEOUT_MS) {
|
|
785
1156
|
throw new Run402DeployError(`Timed out waiting for operation ${snapshot.operation_id} to reach ready`, {
|
|
786
1157
|
code: "INTERNAL_ERROR",
|
|
@@ -798,8 +1169,18 @@ async function pollSnapshotUntilReady(client, initial, diff, warnings, emit, pro
|
|
|
798
1169
|
snapshot = await client.request(`/deploy/v2/operations/${encodeURIComponent(snapshot.operation_id)}`, { headers: opHeaders, context: "polling deploy operation" });
|
|
799
1170
|
}
|
|
800
1171
|
}
|
|
1172
|
+
function isTerminalStaticActivationError(error) {
|
|
1173
|
+
if (!error?.code)
|
|
1174
|
+
return false;
|
|
1175
|
+
if (error.retryable === false)
|
|
1176
|
+
return true;
|
|
1177
|
+
if (error.safe_to_retry === false)
|
|
1178
|
+
return true;
|
|
1179
|
+
return STATIC_ACTIVATION_FAILURE_CODES.has(error.code.toUpperCase());
|
|
1180
|
+
}
|
|
801
1181
|
// ─── start() implementation ──────────────────────────────────────────────────
|
|
802
1182
|
async function startInternal(client, spec, opts) {
|
|
1183
|
+
const allowWarningCodes = normalizeAllowWarningCodes(opts.allowWarningCodes);
|
|
803
1184
|
const buffered = [];
|
|
804
1185
|
const subscribers = [];
|
|
805
1186
|
const emit = (event) => {
|
|
@@ -825,7 +1206,7 @@ async function startInternal(client, spec, opts) {
|
|
|
825
1206
|
const { plan, byteReaders } = await planInternal(client, spec, opts.idempotencyKey);
|
|
826
1207
|
emit({ type: "plan.diff", diff: plan.diff });
|
|
827
1208
|
emitPlanWarnings(plan, emit);
|
|
828
|
-
abortOnConfirmationWarnings(plan, opts);
|
|
1209
|
+
abortOnConfirmationWarnings(plan, opts, allowWarningCodes);
|
|
829
1210
|
if (plan.payment_required) {
|
|
830
1211
|
emit({
|
|
831
1212
|
type: "payment.required",
|
|
@@ -937,6 +1318,7 @@ async function startInternal(client, spec, opts) {
|
|
|
937
1318
|
};
|
|
938
1319
|
}
|
|
939
1320
|
const RELEASE_SPEC_FIELDS = new Set([
|
|
1321
|
+
"$schema",
|
|
940
1322
|
"project",
|
|
941
1323
|
"base",
|
|
942
1324
|
"database",
|
|
@@ -976,7 +1358,7 @@ const SITE_PUBLIC_PATHS_FIELDS = new Set(["mode", "replace"]);
|
|
|
976
1358
|
const PUBLIC_STATIC_PATH_FIELDS = new Set(["asset", "cache_class"]);
|
|
977
1359
|
const SUBDOMAINS_SPEC_FIELDS = new Set(["set", "add", "remove"]);
|
|
978
1360
|
const ROUTES_SPEC_FIELDS = new Set(["replace"]);
|
|
979
|
-
const ROUTE_ENTRY_FIELDS = new Set(["pattern", "methods", "target"]);
|
|
1361
|
+
const ROUTE_ENTRY_FIELDS = new Set(["pattern", "methods", "target", "acknowledge_readonly"]);
|
|
980
1362
|
const FUNCTION_ROUTE_TARGET_FIELDS = new Set(["type", "name"]);
|
|
981
1363
|
const STATIC_ROUTE_TARGET_FIELDS = new Set(["type", "file"]);
|
|
982
1364
|
const ROUTE_METHOD_SET = new Set(ROUTE_HTTP_METHODS);
|
|
@@ -1223,10 +1605,24 @@ function validateRouteEntry(route, resource) {
|
|
|
1223
1605
|
}
|
|
1224
1606
|
}
|
|
1225
1607
|
const targetType = validateRouteTarget(entry.target, `${resource}.target`);
|
|
1608
|
+
validateRouteReadOnlyAcknowledgement(entry, targetType, resource);
|
|
1226
1609
|
if (targetType === "static") {
|
|
1227
1610
|
validateStaticRouteEntry(entry, resource);
|
|
1228
1611
|
}
|
|
1229
1612
|
}
|
|
1613
|
+
function validateRouteReadOnlyAcknowledgement(entry, targetType, resource) {
|
|
1614
|
+
if (entry.acknowledge_readonly === undefined)
|
|
1615
|
+
return;
|
|
1616
|
+
if (entry.acknowledge_readonly !== true) {
|
|
1617
|
+
throw invalidRouteSpec(`ReleaseSpec.${resource}.acknowledge_readonly must be true when present`, `${resource}.acknowledge_readonly`);
|
|
1618
|
+
}
|
|
1619
|
+
if (targetType !== "function" ||
|
|
1620
|
+
typeof entry.pattern !== "string" ||
|
|
1621
|
+
!isFinalWildcardRoutePattern(entry.pattern) ||
|
|
1622
|
+
!isReadOnlyRouteMethods(entry.methods)) {
|
|
1623
|
+
throw invalidRouteSpec(`ReleaseSpec.${resource}.acknowledge_readonly applies only to GET/HEAD final-wildcard function routes`, `${resource}.acknowledge_readonly`);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1230
1626
|
function validateRouteTarget(target, resource) {
|
|
1231
1627
|
const obj = requireObject(target, resource);
|
|
1232
1628
|
if ((hasOwn(obj, "function") || hasOwn(obj, "static")) && !hasOwn(obj, "type")) {
|
|
@@ -1482,13 +1878,13 @@ function clientRoutePlanWarnings(spec) {
|
|
|
1482
1878
|
.filter((route) => {
|
|
1483
1879
|
if (route.target.type !== "function")
|
|
1484
1880
|
return false;
|
|
1485
|
-
if (!route.pattern
|
|
1881
|
+
if (!isFinalWildcardRoutePattern(route.pattern))
|
|
1486
1882
|
return false;
|
|
1487
1883
|
if (!route.methods)
|
|
1488
1884
|
return false;
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1885
|
+
if (route.acknowledge_readonly === true)
|
|
1886
|
+
return false;
|
|
1887
|
+
return isReadOnlyRouteMethods(route.methods);
|
|
1492
1888
|
})
|
|
1493
1889
|
.map((route) => route.pattern)
|
|
1494
1890
|
.sort();
|
|
@@ -1509,21 +1905,65 @@ function clientRoutePlanWarnings(spec) {
|
|
|
1509
1905
|
},
|
|
1510
1906
|
];
|
|
1511
1907
|
}
|
|
1512
|
-
function
|
|
1908
|
+
function isFinalWildcardRoutePattern(pattern) {
|
|
1909
|
+
return pattern.endsWith("/*");
|
|
1910
|
+
}
|
|
1911
|
+
function isReadOnlyRouteMethods(methods) {
|
|
1912
|
+
if (!Array.isArray(methods) || methods.length === 0)
|
|
1913
|
+
return false;
|
|
1914
|
+
return methods.every((method) => method === "GET" || method === "HEAD");
|
|
1915
|
+
}
|
|
1916
|
+
function normalizeAllowWarningCodes(value) {
|
|
1917
|
+
if (value === undefined)
|
|
1918
|
+
return new Set();
|
|
1919
|
+
if (!Array.isArray(value)) {
|
|
1920
|
+
throw new Run402DeployError("ApplyOptions.allowWarningCodes must be an array of warning-code strings", {
|
|
1921
|
+
code: "INVALID_SPEC",
|
|
1922
|
+
phase: "validate",
|
|
1923
|
+
resource: "allowWarningCodes",
|
|
1924
|
+
retryable: false,
|
|
1925
|
+
context: "validating deploy warning options",
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
const codes = new Set();
|
|
1929
|
+
for (const code of value) {
|
|
1930
|
+
if (typeof code !== "string" || code.length === 0) {
|
|
1931
|
+
throw new Run402DeployError("ApplyOptions.allowWarningCodes entries must be non-empty strings", {
|
|
1932
|
+
code: "INVALID_SPEC",
|
|
1933
|
+
phase: "validate",
|
|
1934
|
+
resource: "allowWarningCodes",
|
|
1935
|
+
retryable: false,
|
|
1936
|
+
context: "validating deploy warning options",
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
codes.add(code);
|
|
1940
|
+
}
|
|
1941
|
+
return codes;
|
|
1942
|
+
}
|
|
1943
|
+
function abortOnConfirmationWarnings(plan, opts, allowWarningCodes) {
|
|
1513
1944
|
if (opts.allowWarnings)
|
|
1514
1945
|
return;
|
|
1515
1946
|
const blocking = plan.warnings.filter((w) => w.requires_confirmation || w.code === "MISSING_REQUIRED_SECRET");
|
|
1516
1947
|
if (blocking.length === 0)
|
|
1517
1948
|
return;
|
|
1518
|
-
const
|
|
1519
|
-
|
|
1520
|
-
|
|
1949
|
+
const unacknowledged = blocking.filter((w) => !allowWarningCodes.has(w.code));
|
|
1950
|
+
if (unacknowledged.length === 0)
|
|
1951
|
+
return;
|
|
1952
|
+
const missing = unacknowledged.find((w) => w.code === "MISSING_REQUIRED_SECRET");
|
|
1953
|
+
const first = missing ?? unacknowledged[0];
|
|
1954
|
+
const unacknowledgedCodes = Array.from(new Set(unacknowledged.map((w) => w.code))).sort();
|
|
1955
|
+
throw new Run402DeployError(`Deploy plan returned unacknowledged warning ${first.code}; resolve it, retry with allowWarningCodes for reviewed warning codes, or retry with allowWarnings after explicit review.`, {
|
|
1521
1956
|
code: first.code || "DEPLOY_WARNING_REQUIRES_CONFIRMATION",
|
|
1522
1957
|
phase: "plan",
|
|
1523
1958
|
resource: "warnings",
|
|
1524
1959
|
retryable: false,
|
|
1525
1960
|
fix: { action: "review_warnings", path: "warnings" },
|
|
1526
|
-
body: {
|
|
1961
|
+
body: {
|
|
1962
|
+
warnings: blocking,
|
|
1963
|
+
unacknowledged_warnings: unacknowledged,
|
|
1964
|
+
unacknowledged_warning_codes: unacknowledgedCodes,
|
|
1965
|
+
allowed_warning_codes: Array.from(allowWarningCodes).sort(),
|
|
1966
|
+
},
|
|
1527
1967
|
context: "planning deploy",
|
|
1528
1968
|
});
|
|
1529
1969
|
}
|