run402 1.60.1 → 1.60.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/lib/deploy-v2.mjs +27 -191
- package/lib/deploy.mjs +35 -2
- package/package.json +1 -1
- package/sdk/dist/namespaces/deploy.js +258 -2
- package/sdk/dist/namespaces/deploy.js.map +1 -1
- package/sdk/dist/node/deploy-manifest.d.ts +80 -0
- package/sdk/dist/node/deploy-manifest.d.ts.map +1 -0
- package/sdk/dist/node/deploy-manifest.js +356 -0
- package/sdk/dist/node/deploy-manifest.js.map +1 -0
- package/sdk/dist/node/index.d.ts +2 -0
- package/sdk/dist/node/index.d.ts.map +1 -1
- package/sdk/dist/node/index.js +1 -0
- package/sdk/dist/node/index.js.map +1 -1
package/lib/deploy-v2.mjs
CHANGED
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { readFileSync } from "node:fs";
|
|
27
|
-
import { resolve, dirname, isAbsolute
|
|
28
|
-
import { githubActionsCredentials } from "#sdk/node";
|
|
27
|
+
import { resolve, dirname, isAbsolute } from "node:path";
|
|
28
|
+
import { githubActionsCredentials, normalizeDeployManifest } from "#sdk/node";
|
|
29
29
|
import { getSdk } from "./sdk.mjs";
|
|
30
30
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
31
31
|
import { API, allowanceAuthHeaders, getActiveProjectId, resolveProjectId } from "./config.mjs";
|
|
@@ -206,11 +206,12 @@ async function applyCmd(args) {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
let raw;
|
|
209
|
+
let manifestPath = null;
|
|
209
210
|
if (opts.spec) {
|
|
210
211
|
raw = opts.spec;
|
|
211
212
|
} else if (opts.manifest) {
|
|
212
213
|
try {
|
|
213
|
-
|
|
214
|
+
manifestPath = isAbsolute(opts.manifest) ? opts.manifest : resolve(process.cwd(), opts.manifest);
|
|
214
215
|
raw = readFileSync(manifestPath, "utf-8");
|
|
215
216
|
} catch (err) {
|
|
216
217
|
fail({
|
|
@@ -235,7 +236,7 @@ async function applyCmd(args) {
|
|
|
235
236
|
}
|
|
236
237
|
rejectLegacySecretManifest(spec, {
|
|
237
238
|
source: opts.manifest ? "manifest" : opts.spec ? "spec" : "stdin",
|
|
238
|
-
...(
|
|
239
|
+
...(manifestPath ? { path: manifestPath } : {}),
|
|
239
240
|
});
|
|
240
241
|
|
|
241
242
|
// GH-232: Reject empty specs client-side. Without this guard,
|
|
@@ -269,38 +270,44 @@ async function applyCmd(args) {
|
|
|
269
270
|
hint: "Did you mean to write a 'site.replace' or 'database.migrations' block? See https://run402.com/schemas/manifest.v1.json",
|
|
270
271
|
details: {
|
|
271
272
|
field: opts.manifest ? "manifest" : opts.spec ? "spec" : "stdin",
|
|
272
|
-
...(
|
|
273
|
+
...(manifestPath ? { path: manifestPath } : {}),
|
|
273
274
|
meaningful_keys: meaningful,
|
|
274
275
|
},
|
|
275
276
|
});
|
|
276
277
|
}
|
|
277
278
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (opts.project && spec.project_id && spec.project_id !== opts.project) {
|
|
279
|
+
const manifestProject = spec.project ?? spec.project_id;
|
|
280
|
+
if (opts.project && manifestProject && manifestProject !== opts.project) {
|
|
281
281
|
fail({
|
|
282
282
|
code: "BAD_USAGE",
|
|
283
|
-
message: `project_id conflict:
|
|
284
|
-
details: { spec_project_id:
|
|
283
|
+
message: `project_id conflict: manifest project=${manifestProject} but --project=${opts.project}`,
|
|
284
|
+
details: { spec_project_id: manifestProject, flag_project_id: opts.project },
|
|
285
285
|
});
|
|
286
286
|
}
|
|
287
|
-
if (opts.project) spec.project_id = opts.project;
|
|
288
287
|
const useGithubActionsOidc = hasGithubActionsOidcEnv();
|
|
289
|
-
|
|
290
|
-
|
|
288
|
+
let defaultProject;
|
|
289
|
+
if (!opts.project && !manifestProject) {
|
|
290
|
+
defaultProject = useGithubActionsOidc ? resolveCiProjectId() : resolveProjectId(null);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let normalizedManifest;
|
|
294
|
+
try {
|
|
295
|
+
normalizedManifest = await normalizeDeployManifest(spec, {
|
|
296
|
+
baseDir: manifestPath ? dirname(manifestPath) : process.cwd(),
|
|
297
|
+
...(opts.project ? { project: opts.project } : {}),
|
|
298
|
+
...(defaultProject ? { defaultProject } : {}),
|
|
299
|
+
});
|
|
300
|
+
} catch (err) {
|
|
301
|
+
reportSdkError(err);
|
|
291
302
|
}
|
|
292
303
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
// are accepted at the manifest layer (project_id is friendlier for agents
|
|
296
|
-
// sharing JSON manifests with the MCP tool).
|
|
297
|
-
const releaseSpec = mapManifestToReleaseSpec(spec);
|
|
298
|
-
const idempotencyKey = spec.idempotency_key;
|
|
304
|
+
const releaseSpec = normalizedManifest.spec;
|
|
305
|
+
const idempotencyKey = normalizedManifest.idempotencyKey;
|
|
299
306
|
|
|
300
307
|
let sdkOpts;
|
|
301
308
|
if (useGithubActionsOidc) {
|
|
302
309
|
sdkOpts = {
|
|
303
|
-
credentials: githubActionsCredentials({ projectId:
|
|
310
|
+
credentials: githubActionsCredentials({ projectId: releaseSpec.project, apiBase: API }),
|
|
304
311
|
disablePaidFetch: true,
|
|
305
312
|
};
|
|
306
313
|
} else {
|
|
@@ -668,174 +675,3 @@ function parsePositiveInt(value, flag) {
|
|
|
668
675
|
}
|
|
669
676
|
return parsed;
|
|
670
677
|
}
|
|
671
|
-
|
|
672
|
-
// ─── Manifest → ReleaseSpec ──────────────────────────────────────────────────
|
|
673
|
-
|
|
674
|
-
function mapManifestToReleaseSpec(spec) {
|
|
675
|
-
const out = { project: spec.project_id };
|
|
676
|
-
if (spec.base !== undefined) out.base = spec.base;
|
|
677
|
-
if (spec.subdomains !== undefined) out.subdomains = spec.subdomains;
|
|
678
|
-
if (spec.secrets !== undefined) out.secrets = spec.secrets;
|
|
679
|
-
if (spec.routes !== undefined) out.routes = spec.routes;
|
|
680
|
-
if (spec.checks !== undefined) out.checks = spec.checks;
|
|
681
|
-
|
|
682
|
-
if (spec.database) {
|
|
683
|
-
out.database = {};
|
|
684
|
-
if (spec.database.expose !== undefined) out.database.expose = spec.database.expose;
|
|
685
|
-
if (spec.database.zero_downtime !== undefined) out.database.zero_downtime = spec.database.zero_downtime;
|
|
686
|
-
if (spec.database.migrations) {
|
|
687
|
-
out.database.migrations = spec.database.migrations.map((m) => {
|
|
688
|
-
const mm = { id: m.id };
|
|
689
|
-
if (m.sql !== undefined) mm.sql = m.sql;
|
|
690
|
-
if (m.sql_ref !== undefined) mm.sql_ref = m.sql_ref;
|
|
691
|
-
if (m.checksum !== undefined) mm.checksum = m.checksum;
|
|
692
|
-
if (m.transaction !== undefined) mm.transaction = m.transaction;
|
|
693
|
-
return mm;
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
if (spec.functions) {
|
|
699
|
-
out.functions = {};
|
|
700
|
-
if (spec.functions.replace) out.functions.replace = mapFunctionMap(spec.functions.replace);
|
|
701
|
-
if (spec.functions.patch) {
|
|
702
|
-
out.functions.patch = {};
|
|
703
|
-
if (spec.functions.patch.set) out.functions.patch.set = mapFunctionMap(spec.functions.patch.set);
|
|
704
|
-
if (spec.functions.patch.delete) out.functions.patch.delete = spec.functions.patch.delete;
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
if (spec.site) {
|
|
709
|
-
if (spec.site.replace) {
|
|
710
|
-
out.site = { replace: mapFileMap(spec.site.replace) };
|
|
711
|
-
} else if (spec.site.patch) {
|
|
712
|
-
const patch = {};
|
|
713
|
-
if (spec.site.patch.put) patch.put = mapFileMap(spec.site.patch.put);
|
|
714
|
-
if (spec.site.patch.delete) patch.delete = spec.site.patch.delete;
|
|
715
|
-
out.site = { patch };
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
return out;
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
function mapFunctionMap(map) {
|
|
723
|
-
const out = {};
|
|
724
|
-
for (const [name, fn] of Object.entries(map)) {
|
|
725
|
-
const f = {};
|
|
726
|
-
if (fn.runtime) f.runtime = fn.runtime;
|
|
727
|
-
if (fn.source !== undefined) f.source = fileEntryToContentSource(fn.source);
|
|
728
|
-
if (fn.files) f.files = mapFileMap(fn.files);
|
|
729
|
-
if (fn.entrypoint !== undefined) f.entrypoint = fn.entrypoint;
|
|
730
|
-
if (fn.config !== undefined) f.config = fn.config;
|
|
731
|
-
if (fn.schedule !== undefined) f.schedule = fn.schedule;
|
|
732
|
-
out[name] = f;
|
|
733
|
-
}
|
|
734
|
-
return out;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function mapFileMap(map) {
|
|
738
|
-
const out = {};
|
|
739
|
-
for (const [path, entry] of Object.entries(map)) {
|
|
740
|
-
out[path] = fileEntryToContentSource(entry);
|
|
741
|
-
}
|
|
742
|
-
return out;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function fileEntryToContentSource(entry) {
|
|
746
|
-
if (entry === null || entry === undefined) return entry;
|
|
747
|
-
if (typeof entry === "string") return entry;
|
|
748
|
-
if (entry instanceof Uint8Array) return entry;
|
|
749
|
-
if (typeof entry === "object") {
|
|
750
|
-
if (entry.encoding === "base64" && typeof entry.data === "string") {
|
|
751
|
-
const bytes = Buffer.from(entry.data, "base64");
|
|
752
|
-
const u8 = new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
753
|
-
return entry.contentType ? { data: u8, contentType: entry.contentType } : u8;
|
|
754
|
-
}
|
|
755
|
-
if (typeof entry.data === "string") {
|
|
756
|
-
return entry.contentType ? { data: entry.data, contentType: entry.contentType } : entry.data;
|
|
757
|
-
}
|
|
758
|
-
// Pre-resolved ContentRef shape — pass through.
|
|
759
|
-
if (typeof entry.sha256 === "string" && typeof entry.size === "number") {
|
|
760
|
-
return entry;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
return entry;
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
/**
|
|
767
|
-
* Resolve any `{ "path": "..." }` entries in the manifest to inline data.
|
|
768
|
-
* Mirrors the legacy deploy.mjs behavior so `run402 deploy apply` accepts
|
|
769
|
-
* the same files-with-paths shape that `run402 deploy` does today.
|
|
770
|
-
*/
|
|
771
|
-
function resolveFileDataPaths(spec, baseDir) {
|
|
772
|
-
// Site files
|
|
773
|
-
if (spec.site?.replace) resolveMap(spec.site.replace, baseDir);
|
|
774
|
-
if (spec.site?.patch?.put) resolveMap(spec.site.patch.put, baseDir);
|
|
775
|
-
// Function files
|
|
776
|
-
const visitFns = (fnMap) => {
|
|
777
|
-
if (!fnMap) return;
|
|
778
|
-
for (const fn of Object.values(fnMap)) {
|
|
779
|
-
if (fn.source && typeof fn.source === "object" && fn.source.path) {
|
|
780
|
-
const resolved = readFileEntry(fn.source, baseDir);
|
|
781
|
-
if (resolved) fn.source = resolved;
|
|
782
|
-
}
|
|
783
|
-
if (fn.files) resolveMap(fn.files, baseDir);
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
visitFns(spec.functions?.replace);
|
|
787
|
-
visitFns(spec.functions?.patch?.set);
|
|
788
|
-
// Migration sql_path / sql_file
|
|
789
|
-
if (spec.database?.migrations) {
|
|
790
|
-
for (const m of spec.database.migrations) {
|
|
791
|
-
if (!m.sql && m.sql_path) {
|
|
792
|
-
try {
|
|
793
|
-
const p = isAbsolute(m.sql_path) ? m.sql_path : join(baseDir, m.sql_path);
|
|
794
|
-
m.sql = readFileSync(p, "utf-8");
|
|
795
|
-
delete m.sql_path;
|
|
796
|
-
} catch (err) {
|
|
797
|
-
fail({
|
|
798
|
-
code: "BAD_USAGE",
|
|
799
|
-
message: `Failed to read migration sql_path '${m.sql_path}': ${err.message}`,
|
|
800
|
-
details: { migration_id: m.id, sql_path: m.sql_path },
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
function resolveMap(map, baseDir) {
|
|
809
|
-
for (const [key, entry] of Object.entries(map)) {
|
|
810
|
-
if (entry && typeof entry === "object" && typeof entry.path === "string" && entry.data === undefined) {
|
|
811
|
-
const resolved = readFileEntry(entry, baseDir);
|
|
812
|
-
if (resolved) map[key] = resolved;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function readFileEntry(entry, baseDir) {
|
|
818
|
-
try {
|
|
819
|
-
const p = isAbsolute(entry.path) ? entry.path : join(baseDir, entry.path);
|
|
820
|
-
const buf = readFileSync(p);
|
|
821
|
-
const out = {};
|
|
822
|
-
// Detect text vs binary via simple UTF-8 round-trip; mirrors the bundle
|
|
823
|
-
// deploy behavior. Image/font types get base64; HTML/CSS/JS stay UTF-8.
|
|
824
|
-
const looksTextual = !entry.contentType?.match(/^(image|font|application\/(pdf|wasm|octet-stream|zip))/);
|
|
825
|
-
if (looksTextual) {
|
|
826
|
-
out.data = buf.toString("utf-8");
|
|
827
|
-
out.encoding = "utf-8";
|
|
828
|
-
} else {
|
|
829
|
-
out.data = buf.toString("base64");
|
|
830
|
-
out.encoding = "base64";
|
|
831
|
-
}
|
|
832
|
-
if (entry.contentType) out.contentType = entry.contentType;
|
|
833
|
-
return out;
|
|
834
|
-
} catch (err) {
|
|
835
|
-
fail({
|
|
836
|
-
code: "BAD_USAGE",
|
|
837
|
-
message: `Failed to read file '${entry.path}': ${err.message}`,
|
|
838
|
-
details: { path: entry.path },
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
}
|
package/lib/deploy.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { resolveProjectId } from "./config.mjs";
|
|
|
4
4
|
import { resolveFilePathsInManifest, resolveMigrationsFile } from "./manifest.mjs";
|
|
5
5
|
import { getSdk } from "./sdk.mjs";
|
|
6
6
|
import { reportSdkError, fail } from "./sdk-errors.mjs";
|
|
7
|
+
import { normalizeDeployManifest } from "#sdk/node";
|
|
7
8
|
|
|
8
9
|
const HELP = `run402 deploy — Deploy to an existing project on Run402
|
|
9
10
|
|
|
@@ -347,10 +348,26 @@ export async function run(args) {
|
|
|
347
348
|
manifest.project_id = resolveProjectId(null);
|
|
348
349
|
}
|
|
349
350
|
|
|
350
|
-
// Strip fields that aren't part of the bundleDeploy contract.
|
|
351
351
|
const projectId = manifest.project_id;
|
|
352
|
-
delete manifest.project_id;
|
|
353
352
|
delete manifest.name;
|
|
353
|
+
|
|
354
|
+
if (isV2Manifest(manifest)) {
|
|
355
|
+
try {
|
|
356
|
+
const normalized = await normalizeDeployManifest(manifest, {
|
|
357
|
+
baseDir: opts.manifest ? dirname(resolve(opts.manifest)) : process.cwd(),
|
|
358
|
+
});
|
|
359
|
+
const result = await getSdk().deploy.apply(normalized.spec, {
|
|
360
|
+
idempotencyKey: normalized.idempotencyKey,
|
|
361
|
+
});
|
|
362
|
+
console.log(JSON.stringify({ project_id: projectId, ...result }, null, 2));
|
|
363
|
+
} catch (err) {
|
|
364
|
+
reportSdkError(err);
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Strip fields that aren't part of the bundleDeploy contract.
|
|
370
|
+
delete manifest.project_id;
|
|
354
371
|
delete manifest.migrations_file;
|
|
355
372
|
|
|
356
373
|
try {
|
|
@@ -360,3 +377,19 @@ export async function run(args) {
|
|
|
360
377
|
reportSdkError(err);
|
|
361
378
|
}
|
|
362
379
|
}
|
|
380
|
+
|
|
381
|
+
function isV2Manifest(manifest) {
|
|
382
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) return false;
|
|
383
|
+
if (manifest.database !== undefined) return true;
|
|
384
|
+
if (manifest.site !== undefined) return true;
|
|
385
|
+
if (manifest.subdomains !== undefined) return true;
|
|
386
|
+
if (manifest.routes !== undefined) return true;
|
|
387
|
+
if (manifest.checks !== undefined) return true;
|
|
388
|
+
if (manifest.secrets && typeof manifest.secrets === "object" && !Array.isArray(manifest.secrets)) {
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
if (manifest.functions && typeof manifest.functions === "object" && !Array.isArray(manifest.functions)) {
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
return false;
|
|
395
|
+
}
|
package/package.json
CHANGED
|
@@ -764,6 +764,43 @@ async function startInternal(client, spec, opts) {
|
|
|
764
764
|
},
|
|
765
765
|
};
|
|
766
766
|
}
|
|
767
|
+
const RELEASE_SPEC_FIELDS = new Set([
|
|
768
|
+
"project",
|
|
769
|
+
"base",
|
|
770
|
+
"database",
|
|
771
|
+
"secrets",
|
|
772
|
+
"functions",
|
|
773
|
+
"site",
|
|
774
|
+
"subdomains",
|
|
775
|
+
"routes",
|
|
776
|
+
"checks",
|
|
777
|
+
]);
|
|
778
|
+
const DEPLOYABLE_SPEC_FIELDS = [
|
|
779
|
+
"database",
|
|
780
|
+
"site",
|
|
781
|
+
"functions",
|
|
782
|
+
"secrets",
|
|
783
|
+
"subdomains",
|
|
784
|
+
"routes",
|
|
785
|
+
"checks",
|
|
786
|
+
];
|
|
787
|
+
const BASE_SPEC_FIELDS = new Set(["release", "release_id"]);
|
|
788
|
+
const DATABASE_SPEC_FIELDS = new Set(["migrations", "expose", "zero_downtime"]);
|
|
789
|
+
const MIGRATION_SPEC_FIELDS = new Set(["id", "checksum", "sql", "sql_ref", "transaction"]);
|
|
790
|
+
const FUNCTIONS_SPEC_FIELDS = new Set(["replace", "patch"]);
|
|
791
|
+
const FUNCTIONS_PATCH_FIELDS = new Set(["set", "delete"]);
|
|
792
|
+
const FUNCTION_SPEC_FIELDS = new Set([
|
|
793
|
+
"runtime",
|
|
794
|
+
"source",
|
|
795
|
+
"files",
|
|
796
|
+
"entrypoint",
|
|
797
|
+
"config",
|
|
798
|
+
"schedule",
|
|
799
|
+
]);
|
|
800
|
+
const FUNCTION_CONFIG_FIELDS = new Set(["timeoutSeconds", "memoryMb"]);
|
|
801
|
+
const SITE_SPEC_FIELDS = new Set(["replace", "patch"]);
|
|
802
|
+
const SITE_PATCH_FIELDS = new Set(["put", "delete"]);
|
|
803
|
+
const SUBDOMAINS_SPEC_FIELDS = new Set(["set", "add", "remove"]);
|
|
767
804
|
function validateSpec(spec) {
|
|
768
805
|
if (!spec || typeof spec !== "object") {
|
|
769
806
|
throw new Run402DeployError("ReleaseSpec must be an object", {
|
|
@@ -775,6 +812,11 @@ function validateSpec(spec) {
|
|
|
775
812
|
context: "validating spec",
|
|
776
813
|
});
|
|
777
814
|
}
|
|
815
|
+
const raw = spec;
|
|
816
|
+
validateKnownFields(raw, "spec", RELEASE_SPEC_FIELDS, {
|
|
817
|
+
project_id: "Use `project` in ReleaseSpec, or call `loadDeployManifest()` / `normalizeDeployManifest()` for MCP/CLI-style manifests.",
|
|
818
|
+
subdomain: "Use `subdomains: { set: [name] }`.",
|
|
819
|
+
});
|
|
778
820
|
if (!spec.project || typeof spec.project !== "string") {
|
|
779
821
|
throw new Run402DeployError("ReleaseSpec.project is required", {
|
|
780
822
|
code: "INVALID_SPEC",
|
|
@@ -785,7 +827,17 @@ function validateSpec(spec) {
|
|
|
785
827
|
context: "validating spec",
|
|
786
828
|
});
|
|
787
829
|
}
|
|
788
|
-
|
|
830
|
+
validateBaseSpec(raw.base);
|
|
831
|
+
validateDatabaseSpec(raw.database);
|
|
832
|
+
validateFunctionsSpec(raw.functions);
|
|
833
|
+
validateSiteSpec(raw.site);
|
|
834
|
+
validateSubdomainsSpec(raw.subdomains);
|
|
835
|
+
validateRoutesSpec(raw.routes);
|
|
836
|
+
validateChecksSpec(raw.checks);
|
|
837
|
+
validateSecretsSpec(raw.secrets);
|
|
838
|
+
const subdomains = raw.subdomains;
|
|
839
|
+
const set = subdomains?.set;
|
|
840
|
+
if (set && set.length > 1) {
|
|
789
841
|
throw new Run402DeployError("subdomains.set accepts at most one subdomain per project; multi-subdomain support is not yet available", {
|
|
790
842
|
code: "SUBDOMAIN_MULTI_NOT_SUPPORTED",
|
|
791
843
|
phase: "validate",
|
|
@@ -795,7 +847,211 @@ function validateSpec(spec) {
|
|
|
795
847
|
context: "validating spec",
|
|
796
848
|
});
|
|
797
849
|
}
|
|
798
|
-
|
|
850
|
+
if (!hasDeployableContent(raw)) {
|
|
851
|
+
throw new Run402DeployError(`ReleaseSpec contains no deployable sections. Expected at least one non-empty section: ${DEPLOYABLE_SPEC_FIELDS.join(", ")}`, {
|
|
852
|
+
code: "MANIFEST_EMPTY",
|
|
853
|
+
phase: "validate",
|
|
854
|
+
resource: "spec",
|
|
855
|
+
retryable: false,
|
|
856
|
+
fix: { action: "set_field", path: "site.replace" },
|
|
857
|
+
body: { deployable_fields: DEPLOYABLE_SPEC_FIELDS },
|
|
858
|
+
context: "validating spec",
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function validateBaseSpec(base) {
|
|
863
|
+
if (base === undefined)
|
|
864
|
+
return;
|
|
865
|
+
const obj = requireObject(base, "base");
|
|
866
|
+
validateKnownFields(obj, "base", BASE_SPEC_FIELDS);
|
|
867
|
+
if (hasOwn(obj, "release") && hasOwn(obj, "release_id")) {
|
|
868
|
+
throw invalidSpec("ReleaseSpec.base must use either release or release_id, not both", "base");
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
function validateDatabaseSpec(database) {
|
|
872
|
+
if (database === undefined)
|
|
873
|
+
return;
|
|
874
|
+
const obj = requireObject(database, "database");
|
|
875
|
+
validateKnownFields(obj, "database", DATABASE_SPEC_FIELDS);
|
|
876
|
+
if (obj.migrations !== undefined) {
|
|
877
|
+
if (!Array.isArray(obj.migrations)) {
|
|
878
|
+
throw invalidSpec("ReleaseSpec.database.migrations must be an array", "database.migrations");
|
|
879
|
+
}
|
|
880
|
+
for (const [index, migration] of obj.migrations.entries()) {
|
|
881
|
+
const m = requireObject(migration, `database.migrations.${index}`);
|
|
882
|
+
validateKnownFields(m, `database.migrations.${index}`, MIGRATION_SPEC_FIELDS);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
if (obj.expose !== undefined) {
|
|
886
|
+
requireObject(obj.expose, "database.expose");
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
function validateFunctionsSpec(functions) {
|
|
890
|
+
if (functions === undefined)
|
|
891
|
+
return;
|
|
892
|
+
const obj = requireObject(functions, "functions");
|
|
893
|
+
validateKnownFields(obj, "functions", FUNCTIONS_SPEC_FIELDS);
|
|
894
|
+
if (obj.replace !== undefined) {
|
|
895
|
+
validateFunctionMap(obj.replace, "functions.replace");
|
|
896
|
+
}
|
|
897
|
+
if (obj.patch !== undefined) {
|
|
898
|
+
const patch = requireObject(obj.patch, "functions.patch");
|
|
899
|
+
validateKnownFields(patch, "functions.patch", FUNCTIONS_PATCH_FIELDS);
|
|
900
|
+
if (patch.set !== undefined)
|
|
901
|
+
validateFunctionMap(patch.set, "functions.patch.set");
|
|
902
|
+
if (patch.delete !== undefined)
|
|
903
|
+
validateStringArray(patch.delete, "functions.patch.delete");
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
function validateFunctionMap(value, resource) {
|
|
907
|
+
const map = requireObject(value, resource);
|
|
908
|
+
for (const [name, fn] of Object.entries(map)) {
|
|
909
|
+
const entry = requireObject(fn, `${resource}.${name}`);
|
|
910
|
+
validateKnownFields(entry, `${resource}.${name}`, FUNCTION_SPEC_FIELDS);
|
|
911
|
+
if (entry.config !== undefined) {
|
|
912
|
+
const config = requireObject(entry.config, `${resource}.${name}.config`);
|
|
913
|
+
validateKnownFields(config, `${resource}.${name}.config`, FUNCTION_CONFIG_FIELDS);
|
|
914
|
+
}
|
|
915
|
+
if (entry.files !== undefined) {
|
|
916
|
+
requireObject(entry.files, `${resource}.${name}.files`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
function validateSiteSpec(site) {
|
|
921
|
+
if (site === undefined)
|
|
922
|
+
return;
|
|
923
|
+
const obj = requireObject(site, "site");
|
|
924
|
+
validateKnownFields(obj, "site", SITE_SPEC_FIELDS, {
|
|
925
|
+
file: "Use `site.replace` or `site.patch.put` with a path-keyed file map.",
|
|
926
|
+
files: "Use `site.replace` or `site.patch.put` with a path-keyed file map.",
|
|
927
|
+
});
|
|
928
|
+
if (hasOwn(obj, "replace") && hasOwn(obj, "patch")) {
|
|
929
|
+
throw invalidSpec("ReleaseSpec.site must use either replace or patch, not both", "site");
|
|
930
|
+
}
|
|
931
|
+
if (obj.replace !== undefined) {
|
|
932
|
+
requireObject(obj.replace, "site.replace");
|
|
933
|
+
}
|
|
934
|
+
if (obj.patch !== undefined) {
|
|
935
|
+
const patch = requireObject(obj.patch, "site.patch");
|
|
936
|
+
validateKnownFields(patch, "site.patch", SITE_PATCH_FIELDS);
|
|
937
|
+
if (patch.put !== undefined)
|
|
938
|
+
requireObject(patch.put, "site.patch.put");
|
|
939
|
+
if (patch.delete !== undefined)
|
|
940
|
+
validateStringArray(patch.delete, "site.patch.delete");
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function validateSubdomainsSpec(subdomains) {
|
|
944
|
+
if (subdomains === undefined)
|
|
945
|
+
return;
|
|
946
|
+
const obj = requireObject(subdomains, "subdomains");
|
|
947
|
+
validateKnownFields(obj, "subdomains", SUBDOMAINS_SPEC_FIELDS);
|
|
948
|
+
if (obj.set !== undefined)
|
|
949
|
+
validateStringArray(obj.set, "subdomains.set");
|
|
950
|
+
if (obj.add !== undefined)
|
|
951
|
+
validateStringArray(obj.add, "subdomains.add");
|
|
952
|
+
if (obj.remove !== undefined)
|
|
953
|
+
validateStringArray(obj.remove, "subdomains.remove");
|
|
954
|
+
}
|
|
955
|
+
function validateRoutesSpec(routes) {
|
|
956
|
+
if (routes === undefined)
|
|
957
|
+
return;
|
|
958
|
+
requireObject(routes, "routes");
|
|
959
|
+
}
|
|
960
|
+
function validateChecksSpec(checks) {
|
|
961
|
+
if (checks === undefined)
|
|
962
|
+
return;
|
|
963
|
+
if (!Array.isArray(checks)) {
|
|
964
|
+
throw invalidSpec("ReleaseSpec.checks must be an array", "checks");
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
function validateKnownFields(obj, resource, allowed, hints = {}) {
|
|
968
|
+
for (const key of Object.keys(obj)) {
|
|
969
|
+
if (allowed.has(key))
|
|
970
|
+
continue;
|
|
971
|
+
const field = resource === "spec" ? `spec.${key}` : `${resource}.${key}`;
|
|
972
|
+
const hint = hints[key] ? ` ${hints[key]}` : "";
|
|
973
|
+
throw invalidSpec(`Unknown ReleaseSpec field: ${field}.${hint}`, field);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function requireObject(value, resource) {
|
|
977
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
978
|
+
throw invalidSpec(`ReleaseSpec.${resource} must be an object`, resource);
|
|
979
|
+
}
|
|
980
|
+
return value;
|
|
981
|
+
}
|
|
982
|
+
function validateStringArray(value, resource) {
|
|
983
|
+
if (!Array.isArray(value)) {
|
|
984
|
+
throw invalidSpec(`ReleaseSpec.${resource} must be an array`, resource);
|
|
985
|
+
}
|
|
986
|
+
if (value.some((entry) => typeof entry !== "string")) {
|
|
987
|
+
throw invalidSpec(`ReleaseSpec.${resource} entries must be strings`, resource);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function hasDeployableContent(spec) {
|
|
991
|
+
return (hasDatabaseContent(spec.database) ||
|
|
992
|
+
hasSiteContent(spec.site) ||
|
|
993
|
+
hasFunctionsContent(spec.functions) ||
|
|
994
|
+
hasSecretsContent(spec.secrets) ||
|
|
995
|
+
hasSubdomainsContent(spec.subdomains) ||
|
|
996
|
+
hasRecordEntries(spec.routes) ||
|
|
997
|
+
hasArrayEntries(spec.checks));
|
|
998
|
+
}
|
|
999
|
+
function hasDatabaseContent(database) {
|
|
1000
|
+
if (!isRecord(database))
|
|
1001
|
+
return false;
|
|
1002
|
+
return hasArrayEntries(database.migrations) || hasRecordEntries(database.expose);
|
|
1003
|
+
}
|
|
1004
|
+
function hasSiteContent(site) {
|
|
1005
|
+
if (!isRecord(site))
|
|
1006
|
+
return false;
|
|
1007
|
+
if (hasRecordEntries(site.replace))
|
|
1008
|
+
return true;
|
|
1009
|
+
if (!isRecord(site.patch))
|
|
1010
|
+
return false;
|
|
1011
|
+
return hasRecordEntries(site.patch.put) || hasArrayEntries(site.patch.delete);
|
|
1012
|
+
}
|
|
1013
|
+
function hasFunctionsContent(functions) {
|
|
1014
|
+
if (!isRecord(functions))
|
|
1015
|
+
return false;
|
|
1016
|
+
if (hasRecordEntries(functions.replace))
|
|
1017
|
+
return true;
|
|
1018
|
+
if (!isRecord(functions.patch))
|
|
1019
|
+
return false;
|
|
1020
|
+
return hasRecordEntries(functions.patch.set) || hasArrayEntries(functions.patch.delete);
|
|
1021
|
+
}
|
|
1022
|
+
function hasSecretsContent(secrets) {
|
|
1023
|
+
if (!isRecord(secrets))
|
|
1024
|
+
return false;
|
|
1025
|
+
return hasArrayEntries(secrets.require) || hasArrayEntries(secrets.delete);
|
|
1026
|
+
}
|
|
1027
|
+
function hasSubdomainsContent(subdomains) {
|
|
1028
|
+
if (!isRecord(subdomains))
|
|
1029
|
+
return false;
|
|
1030
|
+
return (hasArrayEntries(subdomains.set) ||
|
|
1031
|
+
hasArrayEntries(subdomains.add) ||
|
|
1032
|
+
hasArrayEntries(subdomains.remove));
|
|
1033
|
+
}
|
|
1034
|
+
function hasRecordEntries(value) {
|
|
1035
|
+
return isRecord(value) && Object.keys(value).length > 0;
|
|
1036
|
+
}
|
|
1037
|
+
function hasArrayEntries(value) {
|
|
1038
|
+
return Array.isArray(value) && value.length > 0;
|
|
1039
|
+
}
|
|
1040
|
+
function hasOwn(obj, key) {
|
|
1041
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
1042
|
+
}
|
|
1043
|
+
function isRecord(value) {
|
|
1044
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1045
|
+
}
|
|
1046
|
+
function invalidSpec(message, resource) {
|
|
1047
|
+
return new Run402DeployError(message, {
|
|
1048
|
+
code: "INVALID_SPEC",
|
|
1049
|
+
phase: "validate",
|
|
1050
|
+
resource,
|
|
1051
|
+
retryable: false,
|
|
1052
|
+
fix: { action: "set_field", path: resource.replace(/^spec\./, "") },
|
|
1053
|
+
context: "validating spec",
|
|
1054
|
+
});
|
|
799
1055
|
}
|
|
800
1056
|
function normalizePlanResponse(plan) {
|
|
801
1057
|
const raw = plan;
|