run402 1.60.2 → 1.61.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/lib/deploy-v2.mjs +116 -16
- package/lib/deploy.mjs +35 -2
- package/package.json +1 -1
- package/sdk/dist/index.d.ts +1 -0
- package/sdk/dist/index.d.ts.map +1 -1
- package/sdk/dist/index.js +1 -0
- package/sdk/dist/index.js.map +1 -1
- package/sdk/dist/namespaces/ci.d.ts.map +1 -1
- package/sdk/dist/namespaces/ci.js +3 -0
- package/sdk/dist/namespaces/ci.js.map +1 -1
- package/sdk/dist/namespaces/deploy.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.js +352 -3
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/namespaces/deploy.types.d.ts +56 -11
- package/sdk/dist/namespaces/deploy.types.d.ts.map +1 -1
- package/sdk/dist/namespaces/deploy.types.js +9 -1
- package/sdk/dist/namespaces/deploy.types.js.map +1 -1
- package/sdk/dist/node/deploy-manifest.d.ts.map +1 -1
- package/sdk/dist/node/deploy-manifest.js +139 -8
- package/sdk/dist/node/deploy-manifest.js.map +1 -1
- package/sdk/dist/node/index.d.ts +1 -1
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +1 -1
- package/sdk/dist/node/index.js.map +1 -1
- package/sdk/dist/type-contract.d.ts.map +1 -1
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { isCiSessionCredentials } from "../ci-credentials.js";
|
|
22
22
|
import { assertCiDeployableSpec } from "./ci.js";
|
|
23
|
+
import { ROUTE_HTTP_METHODS } from "./deploy.types.js";
|
|
23
24
|
import { ApiError, LocalError, NetworkError, PaymentRequired, Run402DeployError, Unauthorized, } from "../errors.js";
|
|
24
25
|
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
25
26
|
const PLAN_BODY_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
@@ -764,6 +765,47 @@ async function startInternal(client, spec, opts) {
|
|
|
764
765
|
},
|
|
765
766
|
};
|
|
766
767
|
}
|
|
768
|
+
const RELEASE_SPEC_FIELDS = new Set([
|
|
769
|
+
"project",
|
|
770
|
+
"base",
|
|
771
|
+
"database",
|
|
772
|
+
"secrets",
|
|
773
|
+
"functions",
|
|
774
|
+
"site",
|
|
775
|
+
"subdomains",
|
|
776
|
+
"routes",
|
|
777
|
+
"checks",
|
|
778
|
+
]);
|
|
779
|
+
const DEPLOYABLE_SPEC_FIELDS = [
|
|
780
|
+
"database",
|
|
781
|
+
"site",
|
|
782
|
+
"functions",
|
|
783
|
+
"secrets",
|
|
784
|
+
"subdomains",
|
|
785
|
+
"routes",
|
|
786
|
+
"checks",
|
|
787
|
+
];
|
|
788
|
+
const BASE_SPEC_FIELDS = new Set(["release", "release_id"]);
|
|
789
|
+
const DATABASE_SPEC_FIELDS = new Set(["migrations", "expose", "zero_downtime"]);
|
|
790
|
+
const MIGRATION_SPEC_FIELDS = new Set(["id", "checksum", "sql", "sql_ref", "transaction"]);
|
|
791
|
+
const FUNCTIONS_SPEC_FIELDS = new Set(["replace", "patch"]);
|
|
792
|
+
const FUNCTIONS_PATCH_FIELDS = new Set(["set", "delete"]);
|
|
793
|
+
const FUNCTION_SPEC_FIELDS = new Set([
|
|
794
|
+
"runtime",
|
|
795
|
+
"source",
|
|
796
|
+
"files",
|
|
797
|
+
"entrypoint",
|
|
798
|
+
"config",
|
|
799
|
+
"schedule",
|
|
800
|
+
]);
|
|
801
|
+
const FUNCTION_CONFIG_FIELDS = new Set(["timeoutSeconds", "memoryMb"]);
|
|
802
|
+
const SITE_SPEC_FIELDS = new Set(["replace", "patch"]);
|
|
803
|
+
const SITE_PATCH_FIELDS = new Set(["put", "delete"]);
|
|
804
|
+
const SUBDOMAINS_SPEC_FIELDS = new Set(["set", "add", "remove"]);
|
|
805
|
+
const ROUTES_SPEC_FIELDS = new Set(["replace"]);
|
|
806
|
+
const ROUTE_ENTRY_FIELDS = new Set(["pattern", "methods", "target"]);
|
|
807
|
+
const ROUTE_TARGET_FIELDS = new Set(["type", "name"]);
|
|
808
|
+
const ROUTE_METHOD_SET = new Set(ROUTE_HTTP_METHODS);
|
|
767
809
|
function validateSpec(spec) {
|
|
768
810
|
if (!spec || typeof spec !== "object") {
|
|
769
811
|
throw new Run402DeployError("ReleaseSpec must be an object", {
|
|
@@ -775,6 +817,11 @@ function validateSpec(spec) {
|
|
|
775
817
|
context: "validating spec",
|
|
776
818
|
});
|
|
777
819
|
}
|
|
820
|
+
const raw = spec;
|
|
821
|
+
validateKnownFields(raw, "spec", RELEASE_SPEC_FIELDS, {
|
|
822
|
+
project_id: "Use `project` in ReleaseSpec, or call `loadDeployManifest()` / `normalizeDeployManifest()` for MCP/CLI-style manifests.",
|
|
823
|
+
subdomain: "Use `subdomains: { set: [name] }`.",
|
|
824
|
+
});
|
|
778
825
|
if (!spec.project || typeof spec.project !== "string") {
|
|
779
826
|
throw new Run402DeployError("ReleaseSpec.project is required", {
|
|
780
827
|
code: "INVALID_SPEC",
|
|
@@ -785,7 +832,17 @@ function validateSpec(spec) {
|
|
|
785
832
|
context: "validating spec",
|
|
786
833
|
});
|
|
787
834
|
}
|
|
788
|
-
|
|
835
|
+
validateBaseSpec(raw.base);
|
|
836
|
+
validateDatabaseSpec(raw.database);
|
|
837
|
+
validateFunctionsSpec(raw.functions);
|
|
838
|
+
validateSiteSpec(raw.site);
|
|
839
|
+
validateSubdomainsSpec(raw.subdomains);
|
|
840
|
+
validateRoutesSpec(raw.routes);
|
|
841
|
+
validateChecksSpec(raw.checks);
|
|
842
|
+
validateSecretsSpec(raw.secrets);
|
|
843
|
+
const subdomains = raw.subdomains;
|
|
844
|
+
const set = subdomains?.set;
|
|
845
|
+
if (set && set.length > 1) {
|
|
789
846
|
throw new Run402DeployError("subdomains.set accepts at most one subdomain per project; multi-subdomain support is not yet available", {
|
|
790
847
|
code: "SUBDOMAIN_MULTI_NOT_SUPPORTED",
|
|
791
848
|
phase: "validate",
|
|
@@ -795,7 +852,297 @@ function validateSpec(spec) {
|
|
|
795
852
|
context: "validating spec",
|
|
796
853
|
});
|
|
797
854
|
}
|
|
798
|
-
|
|
855
|
+
if (!hasDeployableContent(raw)) {
|
|
856
|
+
throw new Run402DeployError(`ReleaseSpec contains no deployable sections. Expected at least one non-empty section: ${DEPLOYABLE_SPEC_FIELDS.join(", ")}`, {
|
|
857
|
+
code: "MANIFEST_EMPTY",
|
|
858
|
+
phase: "validate",
|
|
859
|
+
resource: "spec",
|
|
860
|
+
retryable: false,
|
|
861
|
+
fix: { action: "set_field", path: "site.replace" },
|
|
862
|
+
body: { deployable_fields: DEPLOYABLE_SPEC_FIELDS },
|
|
863
|
+
context: "validating spec",
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
function validateBaseSpec(base) {
|
|
868
|
+
if (base === undefined)
|
|
869
|
+
return;
|
|
870
|
+
const obj = requireObject(base, "base");
|
|
871
|
+
validateKnownFields(obj, "base", BASE_SPEC_FIELDS);
|
|
872
|
+
if (hasOwn(obj, "release") && hasOwn(obj, "release_id")) {
|
|
873
|
+
throw invalidSpec("ReleaseSpec.base must use either release or release_id, not both", "base");
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
function validateDatabaseSpec(database) {
|
|
877
|
+
if (database === undefined)
|
|
878
|
+
return;
|
|
879
|
+
const obj = requireObject(database, "database");
|
|
880
|
+
validateKnownFields(obj, "database", DATABASE_SPEC_FIELDS);
|
|
881
|
+
if (obj.migrations !== undefined) {
|
|
882
|
+
if (!Array.isArray(obj.migrations)) {
|
|
883
|
+
throw invalidSpec("ReleaseSpec.database.migrations must be an array", "database.migrations");
|
|
884
|
+
}
|
|
885
|
+
for (const [index, migration] of obj.migrations.entries()) {
|
|
886
|
+
const m = requireObject(migration, `database.migrations.${index}`);
|
|
887
|
+
validateKnownFields(m, `database.migrations.${index}`, MIGRATION_SPEC_FIELDS);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
if (obj.expose !== undefined) {
|
|
891
|
+
requireObject(obj.expose, "database.expose");
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function validateFunctionsSpec(functions) {
|
|
895
|
+
if (functions === undefined)
|
|
896
|
+
return;
|
|
897
|
+
const obj = requireObject(functions, "functions");
|
|
898
|
+
validateKnownFields(obj, "functions", FUNCTIONS_SPEC_FIELDS);
|
|
899
|
+
if (obj.replace !== undefined) {
|
|
900
|
+
validateFunctionMap(obj.replace, "functions.replace");
|
|
901
|
+
}
|
|
902
|
+
if (obj.patch !== undefined) {
|
|
903
|
+
const patch = requireObject(obj.patch, "functions.patch");
|
|
904
|
+
validateKnownFields(patch, "functions.patch", FUNCTIONS_PATCH_FIELDS);
|
|
905
|
+
if (patch.set !== undefined)
|
|
906
|
+
validateFunctionMap(patch.set, "functions.patch.set");
|
|
907
|
+
if (patch.delete !== undefined)
|
|
908
|
+
validateStringArray(patch.delete, "functions.patch.delete");
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
function validateFunctionMap(value, resource) {
|
|
912
|
+
const map = requireObject(value, resource);
|
|
913
|
+
for (const [name, fn] of Object.entries(map)) {
|
|
914
|
+
const entry = requireObject(fn, `${resource}.${name}`);
|
|
915
|
+
validateKnownFields(entry, `${resource}.${name}`, FUNCTION_SPEC_FIELDS);
|
|
916
|
+
if (entry.config !== undefined) {
|
|
917
|
+
const config = requireObject(entry.config, `${resource}.${name}.config`);
|
|
918
|
+
validateKnownFields(config, `${resource}.${name}.config`, FUNCTION_CONFIG_FIELDS);
|
|
919
|
+
}
|
|
920
|
+
if (entry.files !== undefined) {
|
|
921
|
+
requireObject(entry.files, `${resource}.${name}.files`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
function validateSiteSpec(site) {
|
|
926
|
+
if (site === undefined)
|
|
927
|
+
return;
|
|
928
|
+
const obj = requireObject(site, "site");
|
|
929
|
+
validateKnownFields(obj, "site", SITE_SPEC_FIELDS, {
|
|
930
|
+
file: "Use `site.replace` or `site.patch.put` with a path-keyed file map.",
|
|
931
|
+
files: "Use `site.replace` or `site.patch.put` with a path-keyed file map.",
|
|
932
|
+
});
|
|
933
|
+
if (hasOwn(obj, "replace") && hasOwn(obj, "patch")) {
|
|
934
|
+
throw invalidSpec("ReleaseSpec.site must use either replace or patch, not both", "site");
|
|
935
|
+
}
|
|
936
|
+
if (obj.replace !== undefined) {
|
|
937
|
+
requireObject(obj.replace, "site.replace");
|
|
938
|
+
}
|
|
939
|
+
if (obj.patch !== undefined) {
|
|
940
|
+
const patch = requireObject(obj.patch, "site.patch");
|
|
941
|
+
validateKnownFields(patch, "site.patch", SITE_PATCH_FIELDS);
|
|
942
|
+
if (patch.put !== undefined)
|
|
943
|
+
requireObject(patch.put, "site.patch.put");
|
|
944
|
+
if (patch.delete !== undefined)
|
|
945
|
+
validateStringArray(patch.delete, "site.patch.delete");
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
function validateSubdomainsSpec(subdomains) {
|
|
949
|
+
if (subdomains === undefined)
|
|
950
|
+
return;
|
|
951
|
+
const obj = requireObject(subdomains, "subdomains");
|
|
952
|
+
validateKnownFields(obj, "subdomains", SUBDOMAINS_SPEC_FIELDS);
|
|
953
|
+
if (obj.set !== undefined)
|
|
954
|
+
validateStringArray(obj.set, "subdomains.set");
|
|
955
|
+
if (obj.add !== undefined)
|
|
956
|
+
validateStringArray(obj.add, "subdomains.add");
|
|
957
|
+
if (obj.remove !== undefined)
|
|
958
|
+
validateStringArray(obj.remove, "subdomains.remove");
|
|
959
|
+
}
|
|
960
|
+
function validateRoutesSpec(routes) {
|
|
961
|
+
if (routes === undefined)
|
|
962
|
+
return;
|
|
963
|
+
if (routes === null)
|
|
964
|
+
return;
|
|
965
|
+
const obj = requireObject(routes, "routes");
|
|
966
|
+
for (const key of Object.keys(obj)) {
|
|
967
|
+
if (key.startsWith("/")) {
|
|
968
|
+
throw invalidRouteSpec(`Unknown ReleaseSpec field: routes.${key}. ${routeShapeHints(obj)[key]}`, `routes.${key}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
validateKnownFields(obj, "routes", ROUTES_SPEC_FIELDS, routeShapeHints(obj));
|
|
972
|
+
if (!hasOwn(obj, "replace")) {
|
|
973
|
+
throw invalidRouteSpec("ReleaseSpec.routes must be null or { replace: [{ pattern, target: { type: \"function\", name } }] }. Path-keyed route maps are not supported.", "routes");
|
|
974
|
+
}
|
|
975
|
+
if (!Array.isArray(obj.replace)) {
|
|
976
|
+
throw invalidRouteSpec("ReleaseSpec.routes.replace must be an array of route entries", "routes.replace");
|
|
977
|
+
}
|
|
978
|
+
for (const [index, route] of obj.replace.entries()) {
|
|
979
|
+
validateRouteEntry(route, `routes.replace.${index}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
function routeShapeHints(obj) {
|
|
983
|
+
const hints = {};
|
|
984
|
+
for (const key of Object.keys(obj)) {
|
|
985
|
+
if (key.startsWith("/")) {
|
|
986
|
+
hints[key] = "Use `routes.replace[]` entries like `{ pattern, target: { type: \"function\", name } }` instead of a path-keyed route map.";
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
return hints;
|
|
990
|
+
}
|
|
991
|
+
function validateRouteEntry(route, resource) {
|
|
992
|
+
const entry = requireObject(route, resource);
|
|
993
|
+
validateKnownFields(entry, resource, ROUTE_ENTRY_FIELDS);
|
|
994
|
+
if (typeof entry.pattern !== "string" || entry.pattern.length === 0) {
|
|
995
|
+
throw invalidRouteSpec(`ReleaseSpec.${resource}.pattern must be a non-empty string`, `${resource}.pattern`);
|
|
996
|
+
}
|
|
997
|
+
if (entry.methods !== undefined) {
|
|
998
|
+
if (!Array.isArray(entry.methods)) {
|
|
999
|
+
throw invalidRouteSpec(`ReleaseSpec.${resource}.methods must be an array of HTTP methods`, `${resource}.methods`);
|
|
1000
|
+
}
|
|
1001
|
+
if (entry.methods.length === 0) {
|
|
1002
|
+
throw invalidRouteSpec(`ReleaseSpec.${resource}.methods must not be empty; omit methods to allow all supported methods`, `${resource}.methods`);
|
|
1003
|
+
}
|
|
1004
|
+
for (const method of entry.methods) {
|
|
1005
|
+
if (typeof method !== "string" || !ROUTE_METHOD_SET.has(method)) {
|
|
1006
|
+
throw invalidRouteSpec(`Unsupported route method ${JSON.stringify(method)} at ReleaseSpec.${resource}.methods. Supported methods: ${ROUTE_HTTP_METHODS.join(", ")}`, `${resource}.methods`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
validateRouteTarget(entry.target, `${resource}.target`);
|
|
1011
|
+
}
|
|
1012
|
+
function validateRouteTarget(target, resource) {
|
|
1013
|
+
const obj = requireObject(target, resource);
|
|
1014
|
+
if (hasOwn(obj, "function") && !hasOwn(obj, "type")) {
|
|
1015
|
+
throw invalidRouteSpec(`ReleaseSpec.${resource} uses an unsupported target shorthand. Use { type: "function", name: "api" }.`, resource);
|
|
1016
|
+
}
|
|
1017
|
+
validateKnownFields(obj, resource, ROUTE_TARGET_FIELDS);
|
|
1018
|
+
if (obj.type === undefined) {
|
|
1019
|
+
throw invalidRouteSpec(`ReleaseSpec.${resource}.type is required; use "function"`, `${resource}.type`);
|
|
1020
|
+
}
|
|
1021
|
+
if (obj.type !== "function") {
|
|
1022
|
+
throw invalidRouteSpec(`Unsupported route target type ${JSON.stringify(obj.type)} at ReleaseSpec.${resource}.type; Phase 1 routes support only "function" targets`, `${resource}.type`);
|
|
1023
|
+
}
|
|
1024
|
+
if (typeof obj.name !== "string" || obj.name.length === 0) {
|
|
1025
|
+
throw invalidRouteSpec(`ReleaseSpec.${resource}.name is required for function route targets`, `${resource}.name`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
function validateChecksSpec(checks) {
|
|
1029
|
+
if (checks === undefined)
|
|
1030
|
+
return;
|
|
1031
|
+
if (!Array.isArray(checks)) {
|
|
1032
|
+
throw invalidSpec("ReleaseSpec.checks must be an array", "checks");
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
function validateKnownFields(obj, resource, allowed, hints = {}) {
|
|
1036
|
+
for (const key of Object.keys(obj)) {
|
|
1037
|
+
if (allowed.has(key))
|
|
1038
|
+
continue;
|
|
1039
|
+
const field = resource === "spec" ? `spec.${key}` : `${resource}.${key}`;
|
|
1040
|
+
const hint = hints[key] ? ` ${hints[key]}` : "";
|
|
1041
|
+
throw invalidSpec(`Unknown ReleaseSpec field: ${field}.${hint}`, field);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function requireObject(value, resource) {
|
|
1045
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1046
|
+
throw invalidSpec(`ReleaseSpec.${resource} must be an object`, resource);
|
|
1047
|
+
}
|
|
1048
|
+
return value;
|
|
1049
|
+
}
|
|
1050
|
+
function validateStringArray(value, resource) {
|
|
1051
|
+
if (!Array.isArray(value)) {
|
|
1052
|
+
throw invalidSpec(`ReleaseSpec.${resource} must be an array`, resource);
|
|
1053
|
+
}
|
|
1054
|
+
if (value.some((entry) => typeof entry !== "string")) {
|
|
1055
|
+
throw invalidSpec(`ReleaseSpec.${resource} entries must be strings`, resource);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function hasDeployableContent(spec) {
|
|
1059
|
+
return (hasDatabaseContent(spec.database) ||
|
|
1060
|
+
hasSiteContent(spec.site) ||
|
|
1061
|
+
hasFunctionsContent(spec.functions) ||
|
|
1062
|
+
hasSecretsContent(spec.secrets) ||
|
|
1063
|
+
hasSubdomainsContent(spec.subdomains) ||
|
|
1064
|
+
hasRecordEntries(spec.routes) ||
|
|
1065
|
+
hasArrayEntries(spec.checks));
|
|
1066
|
+
}
|
|
1067
|
+
function hasDatabaseContent(database) {
|
|
1068
|
+
if (!isRecord(database))
|
|
1069
|
+
return false;
|
|
1070
|
+
return hasArrayEntries(database.migrations) || hasRecordEntries(database.expose);
|
|
1071
|
+
}
|
|
1072
|
+
function hasSiteContent(site) {
|
|
1073
|
+
if (!isRecord(site))
|
|
1074
|
+
return false;
|
|
1075
|
+
if (hasRecordEntries(site.replace))
|
|
1076
|
+
return true;
|
|
1077
|
+
if (!isRecord(site.patch))
|
|
1078
|
+
return false;
|
|
1079
|
+
return hasRecordEntries(site.patch.put) || hasArrayEntries(site.patch.delete);
|
|
1080
|
+
}
|
|
1081
|
+
function hasFunctionsContent(functions) {
|
|
1082
|
+
if (!isRecord(functions))
|
|
1083
|
+
return false;
|
|
1084
|
+
if (hasRecordEntries(functions.replace))
|
|
1085
|
+
return true;
|
|
1086
|
+
if (!isRecord(functions.patch))
|
|
1087
|
+
return false;
|
|
1088
|
+
return hasRecordEntries(functions.patch.set) || hasArrayEntries(functions.patch.delete);
|
|
1089
|
+
}
|
|
1090
|
+
function hasSecretsContent(secrets) {
|
|
1091
|
+
if (!isRecord(secrets))
|
|
1092
|
+
return false;
|
|
1093
|
+
return hasArrayEntries(secrets.require) || hasArrayEntries(secrets.delete);
|
|
1094
|
+
}
|
|
1095
|
+
function hasSubdomainsContent(subdomains) {
|
|
1096
|
+
if (!isRecord(subdomains))
|
|
1097
|
+
return false;
|
|
1098
|
+
return (hasArrayEntries(subdomains.set) ||
|
|
1099
|
+
hasArrayEntries(subdomains.add) ||
|
|
1100
|
+
hasArrayEntries(subdomains.remove));
|
|
1101
|
+
}
|
|
1102
|
+
function hasRecordEntries(value) {
|
|
1103
|
+
return isRecord(value) && Object.keys(value).length > 0;
|
|
1104
|
+
}
|
|
1105
|
+
function hasArrayEntries(value) {
|
|
1106
|
+
return Array.isArray(value) && value.length > 0;
|
|
1107
|
+
}
|
|
1108
|
+
function hasOwn(obj, key) {
|
|
1109
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
1110
|
+
}
|
|
1111
|
+
function isRecord(value) {
|
|
1112
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1113
|
+
}
|
|
1114
|
+
function invalidSpec(message, resource) {
|
|
1115
|
+
return new Run402DeployError(message, {
|
|
1116
|
+
code: "INVALID_SPEC",
|
|
1117
|
+
phase: "validate",
|
|
1118
|
+
resource,
|
|
1119
|
+
retryable: false,
|
|
1120
|
+
fix: { action: "set_field", path: resource.replace(/^spec\./, "") },
|
|
1121
|
+
context: "validating spec",
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
function invalidRouteSpec(message, resource) {
|
|
1125
|
+
return new Run402DeployError(message, {
|
|
1126
|
+
code: "INVALID_SPEC",
|
|
1127
|
+
phase: "validate",
|
|
1128
|
+
resource,
|
|
1129
|
+
retryable: false,
|
|
1130
|
+
fix: {
|
|
1131
|
+
action: "set_field",
|
|
1132
|
+
path: "routes.replace",
|
|
1133
|
+
example: {
|
|
1134
|
+
routes: {
|
|
1135
|
+
replace: [
|
|
1136
|
+
{
|
|
1137
|
+
pattern: "/api/*",
|
|
1138
|
+
target: { type: "function", name: "api" },
|
|
1139
|
+
},
|
|
1140
|
+
],
|
|
1141
|
+
},
|
|
1142
|
+
},
|
|
1143
|
+
},
|
|
1144
|
+
context: "validating spec",
|
|
1145
|
+
});
|
|
799
1146
|
}
|
|
800
1147
|
function normalizePlanResponse(plan) {
|
|
801
1148
|
const raw = plan;
|
|
@@ -810,6 +1157,7 @@ function normalizePlanResponse(plan) {
|
|
|
810
1157
|
functions: raw.functions,
|
|
811
1158
|
secrets: raw.secrets,
|
|
812
1159
|
subdomains: raw.subdomains,
|
|
1160
|
+
routes: raw.routes,
|
|
813
1161
|
};
|
|
814
1162
|
return {
|
|
815
1163
|
...plan,
|
|
@@ -939,8 +1287,9 @@ async function normalizeReleaseSpec(client, spec) {
|
|
|
939
1287
|
normalized.base = spec.base;
|
|
940
1288
|
if (spec.subdomains)
|
|
941
1289
|
normalized.subdomains = spec.subdomains;
|
|
942
|
-
if (spec
|
|
1290
|
+
if (hasOwn(spec, "routes")) {
|
|
943
1291
|
normalized.routes = spec.routes;
|
|
1292
|
+
}
|
|
944
1293
|
if (spec.checks)
|
|
945
1294
|
normalized.checks = spec.checks;
|
|
946
1295
|
if (spec.secrets)
|