@mp-lb/arcnet 0.0.0-stage → 0.1.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 +19 -2
- package/dist/addDependency.d.ts +3 -0
- package/dist/addDependency.d.ts.map +1 -0
- package/dist/addDependency.js +29 -0
- package/dist/addDependency.js.map +1 -0
- package/dist/apiClient.d.ts +3 -0
- package/dist/apiClient.d.ts.map +1 -0
- package/dist/apiClient.js +39 -0
- package/dist/apiClient.js.map +1 -0
- package/dist/checkIdentity.d.ts +3 -0
- package/dist/checkIdentity.d.ts.map +1 -0
- package/dist/checkIdentity.js +9 -0
- package/dist/checkIdentity.js.map +1 -0
- package/dist/ciOutputs.d.ts +2 -0
- package/dist/ciOutputs.d.ts.map +1 -0
- package/dist/ciOutputs.js +31 -0
- package/dist/ciOutputs.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -0
- package/dist/createCli.d.ts +3 -0
- package/dist/createCli.d.ts.map +1 -0
- package/dist/createCli.js +83 -0
- package/dist/createCli.js.map +1 -0
- package/dist/createProject.d.ts +26 -0
- package/dist/createProject.d.ts.map +1 -0
- package/dist/createProject.js +35 -0
- package/dist/createProject.js.map +1 -0
- package/dist/createSetupCommand.d.ts +3 -0
- package/dist/createSetupCommand.d.ts.map +1 -0
- package/dist/createSetupCommand.js +11 -0
- package/dist/createSetupCommand.js.map +1 -0
- package/dist/deploy.d.ts +3 -0
- package/dist/deploy.d.ts.map +1 -0
- package/dist/deploy.js +47 -0
- package/dist/deploy.js.map +1 -0
- package/dist/fail.d.ts +2 -0
- package/dist/fail.d.ts.map +1 -0
- package/dist/fail.js +4 -0
- package/dist/fail.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/inspectReferences.d.ts +3 -0
- package/dist/inspectReferences.d.ts.map +1 -0
- package/dist/inspectReferences.js +10 -0
- package/dist/inspectReferences.js.map +1 -0
- package/dist/inspectStaticTree.d.ts +2 -0
- package/dist/inspectStaticTree.d.ts.map +1 -0
- package/dist/inspectStaticTree.js +17 -0
- package/dist/inspectStaticTree.js.map +1 -0
- package/dist/packageInfo.d.ts +3 -0
- package/dist/packageInfo.d.ts.map +1 -0
- package/dist/packageInfo.js +9 -0
- package/dist/packageInfo.js.map +1 -0
- package/dist/parseSelection.d.ts +3 -0
- package/dist/parseSelection.d.ts.map +1 -0
- package/dist/parseSelection.js +13 -0
- package/dist/parseSelection.js.map +1 -0
- package/dist/parseSetupInput.d.ts +16 -0
- package/dist/parseSetupInput.d.ts.map +1 -0
- package/dist/parseSetupInput.js +34 -0
- package/dist/parseSetupInput.js.map +1 -0
- package/dist/readSelection.d.ts +15 -0
- package/dist/readSelection.d.ts.map +1 -0
- package/dist/readSelection.js +36 -0
- package/dist/readSelection.js.map +1 -0
- package/dist/renderPlan.d.ts +27 -0
- package/dist/renderPlan.d.ts.map +1 -0
- package/dist/renderPlan.js +25 -0
- package/dist/renderPlan.js.map +1 -0
- package/dist/renderResult.d.ts +5 -0
- package/dist/renderResult.d.ts.map +1 -0
- package/dist/renderResult.js +15 -0
- package/dist/renderResult.js.map +1 -0
- package/dist/resolveRepository.d.ts +2 -0
- package/dist/resolveRepository.d.ts.map +1 -0
- package/dist/resolveRepository.js +24 -0
- package/dist/resolveRepository.js.map +1 -0
- package/dist/runProcess.d.ts +2 -0
- package/dist/runProcess.d.ts.map +1 -0
- package/dist/runProcess.js +16 -0
- package/dist/runProcess.js.map +1 -0
- package/dist/runTerraform.d.ts +8 -0
- package/dist/runTerraform.d.ts.map +1 -0
- package/dist/runTerraform.js +111 -0
- package/dist/runTerraform.js.map +1 -0
- package/dist/runVercel.d.ts +34 -0
- package/dist/runVercel.d.ts.map +1 -0
- package/dist/runVercel.js +52 -0
- package/dist/runVercel.js.map +1 -0
- package/dist/safePath.d.ts +2 -0
- package/dist/safePath.d.ts.map +1 -0
- package/dist/safePath.js +35 -0
- package/dist/safePath.js.map +1 -0
- package/dist/safeTargetPath.d.ts +2 -0
- package/dist/safeTargetPath.d.ts.map +1 -0
- package/dist/safeTargetPath.js +29 -0
- package/dist/safeTargetPath.js.map +1 -0
- package/dist/schemas.d.ts +106 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +85 -0
- package/dist/schemas.js.map +1 -0
- package/dist/selectNetwork.d.ts +26 -0
- package/dist/selectNetwork.d.ts.map +1 -0
- package/dist/selectNetwork.js +48 -0
- package/dist/selectNetwork.js.map +1 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +192 -0
- package/dist/setup.js.map +1 -0
- package/dist/setupReport.d.ts +8 -0
- package/dist/setupReport.d.ts.map +1 -0
- package/dist/setupReport.js +2 -0
- package/dist/setupReport.js.map +1 -0
- package/dist/setupVercel.d.ts +9 -0
- package/dist/setupVercel.d.ts.map +1 -0
- package/dist/setupVercel.js +72 -0
- package/dist/setupVercel.js.map +1 -0
- package/dist/stageSite.d.ts +3 -0
- package/dist/stageSite.d.ts.map +1 -0
- package/dist/stageSite.js +31 -0
- package/dist/stageSite.js.map +1 -0
- package/dist/validateConfig.d.ts +3 -0
- package/dist/validateConfig.d.ts.map +1 -0
- package/dist/validateConfig.js +17 -0
- package/dist/validateConfig.js.map +1 -0
- package/docs/public/commands.md +58 -0
- package/docs/public/llms-full.txt +79 -0
- package/package.json +49 -3
- package/src/addDependency.ts +41 -0
- package/src/apiClient.ts +51 -0
- package/src/checkIdentity.ts +18 -0
- package/src/ciOutputs.ts +47 -0
- package/src/cli.ts +11 -0
- package/src/createCli.ts +118 -0
- package/src/createProject.ts +66 -0
- package/src/createSetupCommand.ts +16 -0
- package/src/deploy.ts +71 -0
- package/src/fail.ts +3 -0
- package/src/index.ts +2 -0
- package/src/inspectReferences.ts +20 -0
- package/src/inspectStaticTree.ts +19 -0
- package/src/packageInfo.ts +10 -0
- package/src/parseSelection.ts +22 -0
- package/src/parseSetupInput.ts +40 -0
- package/src/readSelection.ts +47 -0
- package/src/renderPlan.ts +27 -0
- package/src/renderResult.ts +18 -0
- package/src/resolveRepository.ts +38 -0
- package/src/runProcess.ts +26 -0
- package/src/runTerraform.ts +182 -0
- package/src/runVercel.ts +77 -0
- package/src/safePath.ts +43 -0
- package/src/safeTargetPath.ts +32 -0
- package/src/schemas.ts +126 -0
- package/src/selectNetwork.ts +73 -0
- package/src/setup.ts +277 -0
- package/src/setupReport.ts +11 -0
- package/src/setupVercel.ts +126 -0
- package/src/stageSite.ts +54 -0
- package/src/validateConfig.ts +22 -0
- package/templates/deploy-arc.yaml +73 -0
- package/templates/deploy-vercel.yaml +43 -0
- package/templates/network.yaml +105 -0
- package/templates/vercel-static/main.tf +128 -0
- package/templates/vercel-static/module.yaml +51 -0
package/src/deploy.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { fail } from "./fail.js";
|
|
3
|
+
import { inspectReferences } from "./inspectReferences.js";
|
|
4
|
+
import type {
|
|
5
|
+
RunProcess,
|
|
6
|
+
VercelConfig,
|
|
7
|
+
VercelRequest,
|
|
8
|
+
VercelSite,
|
|
9
|
+
} from "./schemas.js";
|
|
10
|
+
import { stageSite } from "./stageSite.js";
|
|
11
|
+
import { validateConfig } from "./validateConfig.js";
|
|
12
|
+
|
|
13
|
+
export const deploy = async (
|
|
14
|
+
config: VercelConfig,
|
|
15
|
+
root: string,
|
|
16
|
+
request: VercelRequest,
|
|
17
|
+
run: RunProcess,
|
|
18
|
+
environment: NodeJS.ProcessEnv,
|
|
19
|
+
target = "production",
|
|
20
|
+
) => {
|
|
21
|
+
validateConfig(config, root);
|
|
22
|
+
if (config.approved !== true)
|
|
23
|
+
fail(
|
|
24
|
+
"Deployment disabled. Review account, project references, routing and production-trigger ownership before setting approved=true.",
|
|
25
|
+
);
|
|
26
|
+
if (!["production", "preview"].includes(target))
|
|
27
|
+
fail("Target must be production or preview.");
|
|
28
|
+
if (
|
|
29
|
+
environment.GITHUB_ACTIONS === "true" &&
|
|
30
|
+
(environment.GITHUB_REPOSITORY !== config.repository ||
|
|
31
|
+
environment.GITHUB_REF !== "refs/heads/main")
|
|
32
|
+
)
|
|
33
|
+
fail(
|
|
34
|
+
"CI deployment is restricted to the configured repository main branch.",
|
|
35
|
+
);
|
|
36
|
+
await inspectReferences(config, request); // Check every identity before building or uploading any site.
|
|
37
|
+
const stages: Array<[VercelSite, string]> = [];
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
for (const site of Object.values(config.sites)) {
|
|
41
|
+
const buildEnv = Object.fromEntries(
|
|
42
|
+
Object.entries(environment).filter(([k]) => !k.startsWith("VERCEL_")),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
await run(site.build[0]!, site.build.slice(1), root, buildEnv);
|
|
46
|
+
stages.push([site, stageSite(root, site, config.account)]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const [site, stage] of stages) {
|
|
50
|
+
const args = [
|
|
51
|
+
"--yes",
|
|
52
|
+
"vercel@63.1.2",
|
|
53
|
+
"deploy",
|
|
54
|
+
"--prebuilt",
|
|
55
|
+
"--yes",
|
|
56
|
+
...(target === "production" ? ["--prod"] : []),
|
|
57
|
+
"--token",
|
|
58
|
+
environment.VERCEL_TOKEN!,
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
await run("npx", args, stage, {
|
|
62
|
+
...environment,
|
|
63
|
+
VERCEL_PROJECT_ID: site.projectId,
|
|
64
|
+
VERCEL_ORG_ID: config.account.id,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
} finally {
|
|
68
|
+
for (const [, stage] of stages)
|
|
69
|
+
rmSync(stage, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
};
|
package/src/fail.ts
ADDED
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { checkIdentity } from "./checkIdentity.js";
|
|
2
|
+
import { fail } from "./fail.js";
|
|
3
|
+
import type { VercelConfig, VercelRequest } from "./schemas.js";
|
|
4
|
+
|
|
5
|
+
export const inspectReferences = async (
|
|
6
|
+
config: VercelConfig,
|
|
7
|
+
request: VercelRequest,
|
|
8
|
+
) => {
|
|
9
|
+
for (const site of Object.values(config.sites)) {
|
|
10
|
+
if (site.mode !== "existing")
|
|
11
|
+
fail(
|
|
12
|
+
`Project ${site.projectName} is new. Create it explicitly, review and commit the returned reference before deployment.`,
|
|
13
|
+
);
|
|
14
|
+
checkIdentity(
|
|
15
|
+
await request("GET", `/v9/projects/${site.projectId}`, config.account),
|
|
16
|
+
site,
|
|
17
|
+
config.account,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { lstatSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { fail } from "./fail.js";
|
|
4
|
+
|
|
5
|
+
export const inspectStaticTree = (path: string) => {
|
|
6
|
+
for (const name of readdirSync(path)) {
|
|
7
|
+
const p = join(path, name),
|
|
8
|
+
stat = lstatSync(p);
|
|
9
|
+
|
|
10
|
+
if (
|
|
11
|
+
stat.isSymbolicLink() ||
|
|
12
|
+
[".git", ".env", "node_modules"].includes(name) ||
|
|
13
|
+
name.startsWith(".env.")
|
|
14
|
+
)
|
|
15
|
+
fail(`Refusing unsafe static artifact entry: ${name}`);
|
|
16
|
+
if (stat.isDirectory()) inspectStaticTree(p);
|
|
17
|
+
else if (!stat.isFile()) fail("Static output contains a non-regular file.");
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
export const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
6
|
+
export const packageVersion = z
|
|
7
|
+
.object({ version: z.string() })
|
|
8
|
+
.parse(
|
|
9
|
+
JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")),
|
|
10
|
+
).version;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
serviceDeploySchema,
|
|
3
|
+
type ServiceDeploy,
|
|
4
|
+
} from "@mp-lb/zapper/dist/arc/schemas.js";
|
|
5
|
+
|
|
6
|
+
export const parseSelection = (
|
|
7
|
+
input: unknown,
|
|
8
|
+
): Record<string, ServiceDeploy> => {
|
|
9
|
+
if (
|
|
10
|
+
!input ||
|
|
11
|
+
typeof input !== "object" ||
|
|
12
|
+
Array.isArray(input) ||
|
|
13
|
+
Object.keys(input).length === 0
|
|
14
|
+
)
|
|
15
|
+
throw new Error("Services file must be a nonempty JSON object.");
|
|
16
|
+
return Object.fromEntries(
|
|
17
|
+
Object.entries(input).map(([name, block]) => [
|
|
18
|
+
name,
|
|
19
|
+
serviceDeploySchema.parse(block),
|
|
20
|
+
]),
|
|
21
|
+
);
|
|
22
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createSetupCommand } from "./createSetupCommand.js";
|
|
3
|
+
|
|
4
|
+
const schema = z.object({
|
|
5
|
+
service: z.string(),
|
|
6
|
+
dockerfile: z.string(),
|
|
7
|
+
repository: z.string().optional(),
|
|
8
|
+
services: z.string().optional(),
|
|
9
|
+
vercel: z.string().optional(),
|
|
10
|
+
dryRun: z.boolean().default(false),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const parseSetupInput = (args: string[]) => {
|
|
14
|
+
const command = createSetupCommand()
|
|
15
|
+
.exitOverride()
|
|
16
|
+
.configureOutput({ writeErr: () => {} });
|
|
17
|
+
|
|
18
|
+
// Commander owns argument parsing; reject duplicate flags instead of silently taking the last value.
|
|
19
|
+
const flags = args.filter((value) => value.startsWith("--"));
|
|
20
|
+
if (new Set(flags).size !== flags.length)
|
|
21
|
+
throw new Error("Duplicate setup option.");
|
|
22
|
+
command.parse(args, { from: "user" });
|
|
23
|
+
|
|
24
|
+
const slug = z
|
|
25
|
+
.string()
|
|
26
|
+
.max(23)
|
|
27
|
+
.regex(/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/)
|
|
28
|
+
.parse(command.args[0]);
|
|
29
|
+
|
|
30
|
+
const input = schema.parse(command.opts());
|
|
31
|
+
|
|
32
|
+
const options = {
|
|
33
|
+
...input,
|
|
34
|
+
"seen-service": command.getOptionValueSource("service") === "cli",
|
|
35
|
+
"seen-dockerfile": command.getOptionValueSource("dockerfile") === "cli",
|
|
36
|
+
"seen-repository": command.getOptionValueSource("repository") === "cli",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return { slug, options, dry: input.dryRun };
|
|
40
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { fail } from "./fail.js";
|
|
3
|
+
import { parseSelection } from "./parseSelection.js";
|
|
4
|
+
const read = (path: string) => readFileSync(path, "utf8");
|
|
5
|
+
|
|
6
|
+
export const readSelection = (file: string, safe: (name: string) => string) => {
|
|
7
|
+
const input: unknown = JSON.parse(read(safe(file)));
|
|
8
|
+
const selection = parseSelection(input);
|
|
9
|
+
|
|
10
|
+
const supported = [
|
|
11
|
+
"cloud-run-web",
|
|
12
|
+
"gce-worker",
|
|
13
|
+
"vercel-static",
|
|
14
|
+
"shared-mongo-atlas",
|
|
15
|
+
"upstash-redis",
|
|
16
|
+
"aws-s3",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
for (const [name, block] of Object.entries(selection)) {
|
|
20
|
+
if (
|
|
21
|
+
!/^[a-z][a-z0-9-]*$/.test(name) ||
|
|
22
|
+
["gcp", "registry", "ci", "myshop"].includes(name) ||
|
|
23
|
+
/myshop/i.test(JSON.stringify(block))
|
|
24
|
+
)
|
|
25
|
+
fail(
|
|
26
|
+
"Invalid service selection: MyShop is local-only and project module names are reserved.",
|
|
27
|
+
);
|
|
28
|
+
if (!supported.includes(block.module))
|
|
29
|
+
fail(
|
|
30
|
+
`Unsupported starter module: ${block.module}. Use a reviewed custom Arc manifest.`,
|
|
31
|
+
);
|
|
32
|
+
if (
|
|
33
|
+
["cloud-run-web", "gce-worker"].includes(block.module) &&
|
|
34
|
+
(!block.dockerfile || !existsSync(safe(block.dockerfile)))
|
|
35
|
+
)
|
|
36
|
+
fail(`Service ${name} requires an existing explicit Dockerfile.`);
|
|
37
|
+
if (
|
|
38
|
+
["cloud-run-web", "vercel-static"].includes(block.module) &&
|
|
39
|
+
(!block.domain || !/^[a-z0-9.-]+$/.test(block.domain))
|
|
40
|
+
)
|
|
41
|
+
fail(`Service ${name} requires an explicit domain.`);
|
|
42
|
+
if (block["deploy-path"]) safe(block["deploy-path"]);
|
|
43
|
+
if (block["local-config"]) safe(block["local-config"]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return selection;
|
|
47
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { VercelConfig } from "./schemas.js";
|
|
2
|
+
import { validateConfig } from "./validateConfig.js";
|
|
3
|
+
|
|
4
|
+
export const renderPlan = (config: VercelConfig, root: string) => {
|
|
5
|
+
validateConfig(config, root);
|
|
6
|
+
return {
|
|
7
|
+
mode: "vercel",
|
|
8
|
+
account: config.account,
|
|
9
|
+
approved: config.approved,
|
|
10
|
+
terraform: false,
|
|
11
|
+
changesProjectSettings: false,
|
|
12
|
+
changesDNS: false,
|
|
13
|
+
sites: Object.entries(config.sites).map(([name, site]) => ({
|
|
14
|
+
name,
|
|
15
|
+
operation:
|
|
16
|
+
site.mode === "new"
|
|
17
|
+
? "explicit-create-required-before-deploy"
|
|
18
|
+
: "reference-existing",
|
|
19
|
+
projectId: site.projectId ?? null,
|
|
20
|
+
projectName: site.projectName,
|
|
21
|
+
build: site.build,
|
|
22
|
+
output: site.output,
|
|
23
|
+
outputConfig: site.outputConfig,
|
|
24
|
+
deployment: "prebuilt-static",
|
|
25
|
+
})),
|
|
26
|
+
};
|
|
27
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const renderResult = (
|
|
2
|
+
value: unknown,
|
|
3
|
+
options: { json?: boolean; jsonl?: boolean },
|
|
4
|
+
) => {
|
|
5
|
+
if (options.json && options.jsonl)
|
|
6
|
+
throw new Error("Choose either --json or --jsonl.");
|
|
7
|
+
if (options.jsonl)
|
|
8
|
+
return JSON.stringify({ type: "command.completed", result: value });
|
|
9
|
+
if (options.json) return JSON.stringify(value);
|
|
10
|
+
if (
|
|
11
|
+
value &&
|
|
12
|
+
typeof value === "object" &&
|
|
13
|
+
"message" in value &&
|
|
14
|
+
typeof value.message === "string"
|
|
15
|
+
)
|
|
16
|
+
return value.message;
|
|
17
|
+
return JSON.stringify(value, null, 2);
|
|
18
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { fail } from "./fail.js";
|
|
3
|
+
|
|
4
|
+
export const resolveRepository = (
|
|
5
|
+
root: string,
|
|
6
|
+
configured?: string,
|
|
7
|
+
): string => {
|
|
8
|
+
let repository = configured;
|
|
9
|
+
|
|
10
|
+
if (!repository) {
|
|
11
|
+
let origin;
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
origin = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
15
|
+
cwd: root,
|
|
16
|
+
encoding: "utf8",
|
|
17
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
18
|
+
}).trim();
|
|
19
|
+
} catch {
|
|
20
|
+
fail("Cannot read Git origin. Pass --repository owner/repo.");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
repository =
|
|
24
|
+
/^(?:git@github\.com:|https:\/\/github\.com\/)([^/]+\/[^/]+?)(?:\.git)?$/.exec(
|
|
25
|
+
origin!,
|
|
26
|
+
)?.[1];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
!/^[A-Za-z0-9][A-Za-z0-9-]*\/[A-Za-z0-9][A-Za-z0-9._-]*$/.test(
|
|
31
|
+
repository ?? "",
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
fail(
|
|
35
|
+
"Repository must be an explicit GitHub owner/repo (or a GitHub origin).",
|
|
36
|
+
);
|
|
37
|
+
return repository!;
|
|
38
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
export const runProcess = (
|
|
4
|
+
command: string,
|
|
5
|
+
args: string[],
|
|
6
|
+
cwd: string,
|
|
7
|
+
env: NodeJS.ProcessEnv,
|
|
8
|
+
machine = false,
|
|
9
|
+
input?: string,
|
|
10
|
+
) => {
|
|
11
|
+
const result = spawnSync(command, args, {
|
|
12
|
+
cwd,
|
|
13
|
+
env,
|
|
14
|
+
input,
|
|
15
|
+
stdio: [
|
|
16
|
+
input === undefined ? "ignore" : "pipe",
|
|
17
|
+
machine ? 2 : "inherit",
|
|
18
|
+
"inherit",
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (result.error || result.status !== 0)
|
|
23
|
+
throw new Error(
|
|
24
|
+
`${command} failed (${result.status ?? "could not start"}). No automatic retry; earlier cloud changes may have succeeded.`,
|
|
25
|
+
);
|
|
26
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { loadManifest } from "@mp-lb/zapper/dist/arc/manifest.js";
|
|
2
|
+
import { resolveModules } from "@mp-lb/zapper/dist/arc/modules.js";
|
|
3
|
+
import {
|
|
4
|
+
deployBlockSchema,
|
|
5
|
+
networkConfigSchema,
|
|
6
|
+
} from "@mp-lb/zapper/dist/arc/schemas.js";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdtempSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
writeFileSync,
|
|
14
|
+
} from "node:fs";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
16
|
+
import { devNull, tmpdir } from "node:os";
|
|
17
|
+
import { dirname, join, resolve } from "node:path";
|
|
18
|
+
import { parse } from "yaml";
|
|
19
|
+
import { runProcess } from "./runProcess.js";
|
|
20
|
+
import { envPoolSchema, terraformConfigSchema } from "./schemas.js";
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
|
|
23
|
+
export const runTerraform = (
|
|
24
|
+
command: "check" | "plan" | "deploy",
|
|
25
|
+
root: string,
|
|
26
|
+
machine = false,
|
|
27
|
+
) => {
|
|
28
|
+
const here = join(root, ".arcnet");
|
|
29
|
+
const read = (name: string) => readFileSync(join(here, name), "utf8");
|
|
30
|
+
const config = terraformConfigSchema.parse(JSON.parse(read("project.json")));
|
|
31
|
+
const zap = parse(readFileSync(join(root, "zap.yaml"), "utf8"));
|
|
32
|
+
const network = networkConfigSchema.parse(parse(read("network.yaml")));
|
|
33
|
+
const deployment = deployBlockSchema.parse(zap.deploy);
|
|
34
|
+
if (config.version !== 1 || zap.project !== config.slug)
|
|
35
|
+
throw new Error("Project config does not match zap.yaml.");
|
|
36
|
+
const services = Object.keys(deployment?.services ?? {});
|
|
37
|
+
if (
|
|
38
|
+
JSON.stringify(services.sort()) !==
|
|
39
|
+
JSON.stringify([...config.services].sort()) ||
|
|
40
|
+
/myshop/i.test(JSON.stringify(deployment.services))
|
|
41
|
+
)
|
|
42
|
+
throw new Error(
|
|
43
|
+
"Service selection differs or includes local-only MyShop. Review scope before deployment.",
|
|
44
|
+
);
|
|
45
|
+
const blocks = Object.values(deployment.services);
|
|
46
|
+
for (const service of blocks)
|
|
47
|
+
if (
|
|
48
|
+
["cloud-run-web", "gce-worker"].includes(service.module) &&
|
|
49
|
+
(!service.dockerfile || !existsSync(join(root, service.dockerfile)))
|
|
50
|
+
)
|
|
51
|
+
throw new Error("Container Dockerfile is missing.");
|
|
52
|
+
if (deployment.project?.ci?.params?.repository !== config.repository)
|
|
53
|
+
throw new Error(
|
|
54
|
+
"Deployment manifest repository differs from project config.",
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const publicEnv: unknown = existsSync(join(here, "env.production.json"))
|
|
58
|
+
? JSON.parse(read("env.production.json"))
|
|
59
|
+
: {};
|
|
60
|
+
|
|
61
|
+
let secretEnv;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
secretEnv = JSON.parse(process.env.ARCNET_ENV_JSON || "{}");
|
|
65
|
+
} catch {
|
|
66
|
+
throw new Error("ARCNET_ENV_JSON must be valid JSON (value omitted).");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const pool of [publicEnv, secretEnv])
|
|
70
|
+
if (
|
|
71
|
+
!pool ||
|
|
72
|
+
Array.isArray(pool) ||
|
|
73
|
+
typeof pool !== "object" ||
|
|
74
|
+
Object.entries(pool).some(
|
|
75
|
+
([k, v]) => !/^[A-Z_][A-Z0-9_]*$/.test(k) || typeof v !== "string",
|
|
76
|
+
)
|
|
77
|
+
)
|
|
78
|
+
throw new Error(
|
|
79
|
+
"Environment pools must be JSON objects mapping environment names to strings.",
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const envPool = {
|
|
83
|
+
...envPoolSchema.parse(publicEnv),
|
|
84
|
+
...envPoolSchema.parse(secretEnv),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
if (command === "check") {
|
|
88
|
+
return {
|
|
89
|
+
operation: command,
|
|
90
|
+
message:
|
|
91
|
+
"Offline config check passed. Cloud access, image runtime and live deployment have NOT been verified.",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (command === "deploy" && config.approved !== true)
|
|
96
|
+
throw new Error(
|
|
97
|
+
"Deployment disabled. Complete prerequisites and obtain target/cost/IAM/DNS approval before committing approved=true.",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (process.env.GITHUB_ACTIONS === "true") {
|
|
101
|
+
if (
|
|
102
|
+
process.env.GITHUB_REPOSITORY !== config.repository ||
|
|
103
|
+
process.env.GITHUB_REF !== "refs/heads/main"
|
|
104
|
+
)
|
|
105
|
+
throw new Error(
|
|
106
|
+
"CI deployment is restricted to the configured repository main branch.",
|
|
107
|
+
);
|
|
108
|
+
if (
|
|
109
|
+
!/^projects\/\d+\/locations\/global\/workloadIdentityPools\/github\/providers\/github-actions$/.test(
|
|
110
|
+
config.workloadIdentityProvider,
|
|
111
|
+
)
|
|
112
|
+
)
|
|
113
|
+
throw new Error(
|
|
114
|
+
"Set workloadIdentityProvider from the approved first deployment.",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const modulesDir = resolve(
|
|
119
|
+
dirname(require.resolve("@mp-lb/zapper")),
|
|
120
|
+
"../arc",
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const instances = resolveModules(loadManifest(root), modulesDir, root);
|
|
124
|
+
for (const instance of instances)
|
|
125
|
+
for (const credential of instance.manifest.credentials)
|
|
126
|
+
if (!process.env[credential.name])
|
|
127
|
+
throw new Error(
|
|
128
|
+
`${credential.name} is required for ${instance.key}; supply a scoped operator credential through environment, never commit it.`,
|
|
129
|
+
);
|
|
130
|
+
if (JSON.stringify(network).includes("REPLACE_"))
|
|
131
|
+
throw new Error(
|
|
132
|
+
"Network contains REPLACE_ placeholders. Commit reviewed network/billing settings before planning or deployment.",
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const missing = blocks
|
|
136
|
+
.flatMap((service) => service.env ?? [])
|
|
137
|
+
.filter((key) => !key.includes("=") && !Object.hasOwn(envPool, key));
|
|
138
|
+
|
|
139
|
+
if (missing.length)
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Missing app environment keys: ${missing.join(", ")}. Supply ARCNET_ENV_JSON or committed public env.`,
|
|
142
|
+
);
|
|
143
|
+
for (const tool of [
|
|
144
|
+
"terraform",
|
|
145
|
+
"gcloud",
|
|
146
|
+
...(blocks.some((s) => ["cloud-run-web", "gce-worker"].includes(s.module))
|
|
147
|
+
? ["docker"]
|
|
148
|
+
: []),
|
|
149
|
+
])
|
|
150
|
+
if (spawnSync(tool, ["--version"], { stdio: "ignore" }).status !== 0)
|
|
151
|
+
throw new Error(
|
|
152
|
+
`Required executable unavailable: ${tool}. See StackForge ArcNet prerequisites.`,
|
|
153
|
+
);
|
|
154
|
+
const executable = require.resolve("@mp-lb/zapper");
|
|
155
|
+
// The engine requires a file for operator configuration. Keep this transient,
|
|
156
|
+
// and explicitly disable its machine-global credentials fallback.
|
|
157
|
+
const temporary = mkdtempSync(join(tmpdir(), "arcnet-operator-"));
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const operator = join(temporary, "operator.json");
|
|
161
|
+
writeFileSync(operator, JSON.stringify({ credentials: devNull }), {
|
|
162
|
+
mode: 0o600,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
runProcess(
|
|
166
|
+
process.execPath,
|
|
167
|
+
[executable, "arc", command],
|
|
168
|
+
root,
|
|
169
|
+
{
|
|
170
|
+
...process.env,
|
|
171
|
+
ZAP_ARC_NETWORK_FILE: join(here, "network.yaml"),
|
|
172
|
+
ZAP_ARC_CONFIG: operator,
|
|
173
|
+
},
|
|
174
|
+
machine,
|
|
175
|
+
JSON.stringify(envPool),
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return { operation: command, message: `Zap Arc ${command} completed.` };
|
|
179
|
+
} finally {
|
|
180
|
+
rmSync(temporary, { recursive: true, force: true });
|
|
181
|
+
}
|
|
182
|
+
};
|
package/src/runVercel.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { apiClient } from "./apiClient.js";
|
|
4
|
+
import { createProject } from "./createProject.js";
|
|
5
|
+
import { deploy } from "./deploy.js";
|
|
6
|
+
import { inspectReferences } from "./inspectReferences.js";
|
|
7
|
+
import { renderPlan } from "./renderPlan.js";
|
|
8
|
+
import { runProcess } from "./runProcess.js";
|
|
9
|
+
import { validateConfig } from "./validateConfig.js";
|
|
10
|
+
|
|
11
|
+
export const runVercel = async (
|
|
12
|
+
command: string,
|
|
13
|
+
root: string,
|
|
14
|
+
argument?: string,
|
|
15
|
+
machine = false,
|
|
16
|
+
) => {
|
|
17
|
+
const path = join(root, ".arcnet/project.json");
|
|
18
|
+
const original = readFileSync(path, "utf8");
|
|
19
|
+
const config = validateConfig(JSON.parse(original), root);
|
|
20
|
+
if (command === "check" || command === "plan")
|
|
21
|
+
return renderPlan(config, root);
|
|
22
|
+
if (command === "create" && process.env.GITHUB_ACTIONS === "true")
|
|
23
|
+
throw new Error(
|
|
24
|
+
"Project creation is a separate local operator step, never a CI side effect.",
|
|
25
|
+
);
|
|
26
|
+
if (["deploy", "preview"].includes(command) && !config.approved)
|
|
27
|
+
throw new Error(
|
|
28
|
+
"Deployment disabled (approved=false). No remote calls made.",
|
|
29
|
+
);
|
|
30
|
+
const request = apiClient(process.env.VERCEL_TOKEN);
|
|
31
|
+
|
|
32
|
+
if (command === "inspect") {
|
|
33
|
+
await inspectReferences(config, request);
|
|
34
|
+
return {
|
|
35
|
+
operation: command,
|
|
36
|
+
message:
|
|
37
|
+
"All existing project IDs, names and account scopes match. No settings or DNS changed.",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (command === "create") {
|
|
42
|
+
if (!argument) throw new Error("Choose a configured site.");
|
|
43
|
+
const next = await createProject(config, argument, request);
|
|
44
|
+
if (next === config)
|
|
45
|
+
return {
|
|
46
|
+
operation: command,
|
|
47
|
+
message:
|
|
48
|
+
"Already an existing reference; identity verified, nothing created.",
|
|
49
|
+
};
|
|
50
|
+
if (readFileSync(path, "utf8") !== original)
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Config changed during creation. Recover the created project ID ${next.sites[argument]!.projectId} manually; no local settings overwritten.`,
|
|
53
|
+
);
|
|
54
|
+
writeFileSync(`${path}.new`, JSON.stringify(next, null, 2) + "\n", {
|
|
55
|
+
flag: "wx",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
renameSync(`${path}.new`, path);
|
|
59
|
+
return {
|
|
60
|
+
operation: command,
|
|
61
|
+
projectId: next.sites[argument]!.projectId,
|
|
62
|
+
message:
|
|
63
|
+
"Created project; review and commit project.json before CI. Deployment remains disabled.",
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await deploy(
|
|
68
|
+
config,
|
|
69
|
+
root,
|
|
70
|
+
request,
|
|
71
|
+
(cmd, args, cwd, env) => runProcess(cmd, args, cwd, env, machine),
|
|
72
|
+
process.env,
|
|
73
|
+
command === "preview" ? "preview" : "production",
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return { operation: command, message: "Selected static sites deployed." };
|
|
77
|
+
};
|
package/src/safePath.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { lstatSync } from "node:fs";
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { fail } from "./fail.js";
|
|
4
|
+
|
|
5
|
+
export const safePath = (root: string, name: string) => {
|
|
6
|
+
if (
|
|
7
|
+
typeof name !== "string" ||
|
|
8
|
+
!name ||
|
|
9
|
+
isAbsolute(name) ||
|
|
10
|
+
name.includes("\\")
|
|
11
|
+
)
|
|
12
|
+
fail("Expected a project-relative path.");
|
|
13
|
+
|
|
14
|
+
const target = resolve(root, name),
|
|
15
|
+
rel = relative(root, target);
|
|
16
|
+
|
|
17
|
+
if (
|
|
18
|
+
!rel ||
|
|
19
|
+
rel === ".." ||
|
|
20
|
+
rel.startsWith("../") ||
|
|
21
|
+
rel
|
|
22
|
+
.split("/")
|
|
23
|
+
.some((p) => [".git", "node_modules", ".arcnet"].includes(p)) ||
|
|
24
|
+
/^apps\/myshop(?:\/|$)/i.test(rel)
|
|
25
|
+
)
|
|
26
|
+
fail(`Unsafe or local-only artifact path: ${name}`);
|
|
27
|
+
let at = root;
|
|
28
|
+
|
|
29
|
+
for (const part of rel.split("/")) {
|
|
30
|
+
at = join(at, part);
|
|
31
|
+
let stat;
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
stat = lstatSync(at);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (stat?.isSymbolicLink()) fail(`Symlink not allowed: ${name}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return target;
|
|
43
|
+
};
|