@mp-lb/arcnet 0.0.0-stage → 0.2.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 +48 -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 +90 -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/secrets/assertNewSource.d.ts +3 -0
- package/dist/secrets/assertNewSource.d.ts.map +1 -0
- package/dist/secrets/assertNewSource.js +32 -0
- package/dist/secrets/assertNewSource.js.map +1 -0
- package/dist/secrets/config.d.ts +37 -0
- package/dist/secrets/config.d.ts.map +1 -0
- package/dist/secrets/config.js +67 -0
- package/dist/secrets/config.js.map +1 -0
- package/dist/secrets/createSecretsCommands.d.ts +3 -0
- package/dist/secrets/createSecretsCommands.d.ts.map +1 -0
- package/dist/secrets/createSecretsCommands.js +93 -0
- package/dist/secrets/createSecretsCommands.js.map +1 -0
- package/dist/secrets/loadSecrets.d.ts +3 -0
- package/dist/secrets/loadSecrets.d.ts.map +1 -0
- package/dist/secrets/loadSecrets.js +35 -0
- package/dist/secrets/loadSecrets.js.map +1 -0
- package/dist/secrets/promptPassword.d.ts +2 -0
- package/dist/secrets/promptPassword.d.ts.map +1 -0
- package/dist/secrets/promptPassword.js +42 -0
- package/dist/secrets/promptPassword.js.map +1 -0
- package/dist/secrets/provisionSource.d.ts +25 -0
- package/dist/secrets/provisionSource.d.ts.map +1 -0
- package/dist/secrets/provisionSource.js +141 -0
- package/dist/secrets/provisionSource.js.map +1 -0
- package/dist/secrets/readRemoteSource.d.ts +7 -0
- package/dist/secrets/readRemoteSource.d.ts.map +1 -0
- package/dist/secrets/readRemoteSource.js +32 -0
- package/dist/secrets/readRemoteSource.js.map +1 -0
- package/dist/secrets/runWithSecrets.d.ts +6 -0
- package/dist/secrets/runWithSecrets.d.ts.map +1 -0
- package/dist/secrets/runWithSecrets.js +59 -0
- package/dist/secrets/runWithSecrets.js.map +1 -0
- package/dist/secrets/secretCommand.d.ts +2 -0
- package/dist/secrets/secretCommand.d.ts.map +1 -0
- package/dist/secrets/secretCommand.js +17 -0
- package/dist/secrets/secretCommand.js.map +1 -0
- package/dist/secrets/selectSecrets.d.ts +3 -0
- package/dist/secrets/selectSecrets.d.ts.map +1 -0
- package/dist/secrets/selectSecrets.js +18 -0
- package/dist/secrets/selectSecrets.js.map +1 -0
- package/dist/secrets/syncSecrets.d.ts +10 -0
- package/dist/secrets/syncSecrets.d.ts.map +1 -0
- package/dist/secrets/syncSecrets.js +43 -0
- package/dist/secrets/syncSecrets.js.map +1 -0
- package/dist/secrets/wireSecretsWorkflows.d.ts +2 -0
- package/dist/secrets/wireSecretsWorkflows.d.ts.map +1 -0
- package/dist/secrets/wireSecretsWorkflows.js +48 -0
- package/dist/secrets/wireSecretsWorkflows.js.map +1 -0
- package/dist/secrets/withDeploymentSecrets.d.ts +2 -0
- package/dist/secrets/withDeploymentSecrets.d.ts.map +1 -0
- package/dist/secrets/withDeploymentSecrets.js +58 -0
- package/dist/secrets/withDeploymentSecrets.js.map +1 -0
- package/dist/secrets/writeSecretsWorkflow.d.ts +3 -0
- package/dist/secrets/writeSecretsWorkflow.d.ts.map +1 -0
- package/dist/secrets/writeSecretsWorkflow.js +60 -0
- package/dist/secrets/writeSecretsWorkflow.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 +132 -0
- package/docs/public/llms-full.txt +182 -0
- package/package.json +50 -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 +128 -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/secrets/assertNewSource.ts +40 -0
- package/src/secrets/config.ts +91 -0
- package/src/secrets/createSecretsCommands.ts +189 -0
- package/src/secrets/loadSecrets.ts +53 -0
- package/src/secrets/promptPassword.ts +46 -0
- package/src/secrets/provisionSource.ts +238 -0
- package/src/secrets/readRemoteSource.ts +73 -0
- package/src/secrets/runWithSecrets.ts +71 -0
- package/src/secrets/secretCommand.ts +26 -0
- package/src/secrets/selectSecrets.ts +33 -0
- package/src/secrets/syncSecrets.ts +60 -0
- package/src/secrets/wireSecretsWorkflows.ts +57 -0
- package/src/secrets/withDeploymentSecrets.ts +77 -0
- package/src/secrets/writeSecretsWorkflow.ts +83 -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
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { stringify } from "yaml";
|
|
4
|
+
import { safeTargetPath } from "../safeTargetPath.js";
|
|
5
|
+
import type { SecretsConfig } from "./config.js";
|
|
6
|
+
import { wireSecretsWorkflows } from "./wireSecretsWorkflows.js";
|
|
7
|
+
|
|
8
|
+
export const writeSecretsWorkflow = (
|
|
9
|
+
config: SecretsConfig,
|
|
10
|
+
root: string,
|
|
11
|
+
configFile: string,
|
|
12
|
+
) => {
|
|
13
|
+
const path = safeTargetPath(
|
|
14
|
+
root,
|
|
15
|
+
".github/workflows/validate-arcnet-secrets.yaml",
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const marker =
|
|
19
|
+
"# Generated by arcnet secrets; configure sources in .arcnet/secrets.json.\n";
|
|
20
|
+
|
|
21
|
+
if (existsSync(path) && !readFileSync(path, "utf8").startsWith(marker))
|
|
22
|
+
throw new Error(
|
|
23
|
+
"Existing validate-arcnet-secrets.yaml is not ArcNet-managed; move it before continuing.",
|
|
24
|
+
);
|
|
25
|
+
if (configFile !== ".arcnet/secrets.json")
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Generated CI requires the standard .arcnet/secrets.json location.",
|
|
28
|
+
);
|
|
29
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
30
|
+
|
|
31
|
+
const env = Object.fromEntries(
|
|
32
|
+
config.sources
|
|
33
|
+
.flatMap((source) => [
|
|
34
|
+
source.keyEnv,
|
|
35
|
+
...(!source.snapshot && source.tokenEnv ? [source.tokenEnv] : []),
|
|
36
|
+
])
|
|
37
|
+
.map((key) => [key, `\u0024{{ secrets.${key} }}`]),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
writeFileSync(
|
|
41
|
+
path,
|
|
42
|
+
marker +
|
|
43
|
+
stringify({
|
|
44
|
+
name: "Validate ArcNet secrets",
|
|
45
|
+
on: {
|
|
46
|
+
workflow_dispatch: {},
|
|
47
|
+
push: {
|
|
48
|
+
branches: ["main"],
|
|
49
|
+
paths: [
|
|
50
|
+
".arcnet/**",
|
|
51
|
+
".github/workflows/validate-arcnet-secrets.yaml",
|
|
52
|
+
"package.json",
|
|
53
|
+
"pnpm-lock.yaml",
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
permissions: { contents: "read" },
|
|
58
|
+
jobs: {
|
|
59
|
+
validate: {
|
|
60
|
+
if: `github.repository == '${config.repository}' && github.ref == 'refs/heads/main'`,
|
|
61
|
+
"runs-on": "ubuntu-latest",
|
|
62
|
+
"timeout-minutes": 10,
|
|
63
|
+
steps: [
|
|
64
|
+
{
|
|
65
|
+
uses: "actions/checkout@v4",
|
|
66
|
+
with: { "persist-credentials": false },
|
|
67
|
+
},
|
|
68
|
+
{ uses: "pnpm/action-setup@v4" },
|
|
69
|
+
{ uses: "actions/setup-node@v4", with: { "node-version": 22 } },
|
|
70
|
+
{ run: "pnpm install --frozen-lockfile --ignore-scripts" },
|
|
71
|
+
{
|
|
72
|
+
name: "Decrypt, merge and validate selected keys",
|
|
73
|
+
run: "pnpm exec arcnet env check",
|
|
74
|
+
env,
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
wireSecretsWorkflows(dirname(path), env);
|
|
83
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
networkConfigSchema,
|
|
3
|
+
type ServiceDeploy,
|
|
4
|
+
} from "@mp-lb/zapper/dist/arc/schemas.js";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { parse } from "yaml";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { fail } from "./fail.js";
|
|
10
|
+
import { packageRoot } from "./packageInfo.js";
|
|
11
|
+
const networkSchema = networkConfigSchema.extend({ "gcp-project": z.string() });
|
|
12
|
+
const read = (path: string) => readFileSync(path, "utf8");
|
|
13
|
+
|
|
14
|
+
export const selectNetwork = (
|
|
15
|
+
slug: string,
|
|
16
|
+
selection: Record<string, ServiceDeploy> | undefined,
|
|
17
|
+
safe: (name: string) => string,
|
|
18
|
+
) => {
|
|
19
|
+
const network = networkSchema.parse(
|
|
20
|
+
parse(
|
|
21
|
+
read(
|
|
22
|
+
existsSync(safe("arcnet.network.yaml"))
|
|
23
|
+
? safe("arcnet.network.yaml")
|
|
24
|
+
: join(packageRoot, "templates/network.yaml"),
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const selectedModules = selection
|
|
30
|
+
? Object.values(selection).map((s) => s.module)
|
|
31
|
+
: ["cloud-run-web"];
|
|
32
|
+
|
|
33
|
+
const providerMap: Record<string, string[]> = {
|
|
34
|
+
"cloud-run-web": ["google", "cloudflare"],
|
|
35
|
+
"gce-worker": ["google"],
|
|
36
|
+
"vercel-static": ["vercel", "cloudflare"],
|
|
37
|
+
"shared-mongo-atlas": ["mongodbatlas"],
|
|
38
|
+
"upstash-redis": ["upstash"],
|
|
39
|
+
"aws-s3": ["aws"],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const providers = new Set([
|
|
43
|
+
"google",
|
|
44
|
+
...selectedModules.flatMap((m) => providerMap[m] ?? []),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
network.providers = Object.fromEntries(
|
|
48
|
+
[...providers].map((k) => {
|
|
49
|
+
const provider = network.providers[k];
|
|
50
|
+
if (!provider) fail(`Network is missing provider ${k}.`);
|
|
51
|
+
return [k, provider];
|
|
52
|
+
}),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const defaults = new Set([
|
|
56
|
+
"github-ci",
|
|
57
|
+
"gcp-project",
|
|
58
|
+
"project-base",
|
|
59
|
+
...selectedModules,
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
network["module-defaults"] = Object.fromEntries(
|
|
63
|
+
Object.entries(network["module-defaults"]).filter(([k]) => defaults.has(k)),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
network.env = { resolver: "false", "public-file": ".arcnet/unused.env" };
|
|
67
|
+
const gcpProject = network["gcp-project"]?.replaceAll("{slug}", slug);
|
|
68
|
+
if (!/^[a-z][a-z0-9-]{4,28}[a-z0-9]$/.test(gcpProject ?? ""))
|
|
69
|
+
fail(
|
|
70
|
+
"Network gcp-project must resolve to a valid 6–30 character project ID using {slug}.",
|
|
71
|
+
);
|
|
72
|
+
return { network, gcpProject };
|
|
73
|
+
};
|
package/src/setup.ts
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { setupReport } from "./setupReport.js";
|
|
2
|
+
import {
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
realpathSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { parseDocument, stringify } from "yaml";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
import { addDependency, hasDependency } from "./addDependency.js";
|
|
14
|
+
import { fail } from "./fail.js";
|
|
15
|
+
import { packageRoot } from "./packageInfo.js";
|
|
16
|
+
import { parseSetupInput } from "./parseSetupInput.js";
|
|
17
|
+
import { readSelection } from "./readSelection.js";
|
|
18
|
+
import { resolveRepository } from "./resolveRepository.js";
|
|
19
|
+
import { safeTargetPath } from "./safeTargetPath.js";
|
|
20
|
+
import { selectNetwork } from "./selectNetwork.js";
|
|
21
|
+
import { setupVercel } from "./setupVercel.js";
|
|
22
|
+
|
|
23
|
+
const home = packageRoot;
|
|
24
|
+
|
|
25
|
+
const read = (p: string) => readFileSync(p, "utf8");
|
|
26
|
+
const json = (value: unknown) => JSON.stringify(value, null, 2) + "\n";
|
|
27
|
+
|
|
28
|
+
export const setup = (args: string[], root = process.cwd()) => {
|
|
29
|
+
const { slug, options, dry } = parseSetupInput(args);
|
|
30
|
+
root = realpathSync(root);
|
|
31
|
+
const safe = (name: string) => safeTargetPath(root, name);
|
|
32
|
+
|
|
33
|
+
if (!options.services) {
|
|
34
|
+
const inputs = [".arcnet/services.json", "arcnet.services.json"].filter(
|
|
35
|
+
(name) => existsSync(safe(name)),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (inputs.length > 1)
|
|
39
|
+
fail(
|
|
40
|
+
"Both .arcnet/services.json and arcnet.services.json exist. Select one with --services.",
|
|
41
|
+
);
|
|
42
|
+
options.services = inputs[0];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
!options["seen-dockerfile"] &&
|
|
47
|
+
!existsSync(safe(options.dockerfile)) &&
|
|
48
|
+
existsSync(safe("Dockerfile"))
|
|
49
|
+
)
|
|
50
|
+
options.dockerfile = "Dockerfile";
|
|
51
|
+
const zapPath = safe("zap.yaml");
|
|
52
|
+
if (!existsSync(zapPath))
|
|
53
|
+
fail("Run setup from a target repository containing zap.yaml.");
|
|
54
|
+
const doc = parseDocument(read(zapPath));
|
|
55
|
+
if (doc.errors.length) fail("zap.yaml is invalid YAML; fix it before setup.");
|
|
56
|
+
|
|
57
|
+
const project = z
|
|
58
|
+
.object({
|
|
59
|
+
project: z.string(),
|
|
60
|
+
native: z.record(z.string(), z.unknown()).optional(),
|
|
61
|
+
docker: z.record(z.string(), z.unknown()).optional(),
|
|
62
|
+
containers: z.record(z.string(), z.unknown()).optional(),
|
|
63
|
+
deploy: z.unknown().optional(),
|
|
64
|
+
})
|
|
65
|
+
.parse(doc.toJS());
|
|
66
|
+
|
|
67
|
+
if (project.project !== slug)
|
|
68
|
+
fail(
|
|
69
|
+
`Slug must equal zap.yaml project (${project.project}). No files changed.`,
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
if (!options.vercel) {
|
|
73
|
+
const inputs = [".arcnet/vercel.json", "arcnet.vercel.json"].filter(
|
|
74
|
+
(name) => existsSync(safe(name)),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (inputs.length > 1)
|
|
78
|
+
fail(
|
|
79
|
+
"Both .arcnet/vercel.json and arcnet.vercel.json exist. Select one with --vercel.",
|
|
80
|
+
);
|
|
81
|
+
options.vercel = inputs[0];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (options.vercel) {
|
|
85
|
+
if (
|
|
86
|
+
options.services ||
|
|
87
|
+
options["seen-service"] ||
|
|
88
|
+
options["seen-dockerfile"] ||
|
|
89
|
+
options["seen-repository"]
|
|
90
|
+
)
|
|
91
|
+
fail(
|
|
92
|
+
"Vercel-only input cannot be combined with Terraform/service flags or a services.json input.",
|
|
93
|
+
);
|
|
94
|
+
if (
|
|
95
|
+
!existsSync(safe("pnpm-lock.yaml")) ||
|
|
96
|
+
!existsSync(safe("package.json"))
|
|
97
|
+
)
|
|
98
|
+
fail("A target pnpm package.json and lockfile are required.");
|
|
99
|
+
const pm = JSON.parse(read(safe("package.json"))).packageManager;
|
|
100
|
+
if (!/^pnpm@\d+\.\d+\.\d+(?:\+sha\d+\.[a-f0-9]+)?$/.test(pm ?? ""))
|
|
101
|
+
fail("Pin packageManager to an exact pnpm version.");
|
|
102
|
+
return setupVercel({
|
|
103
|
+
root,
|
|
104
|
+
slug,
|
|
105
|
+
safe,
|
|
106
|
+
file: options.vercel,
|
|
107
|
+
dry,
|
|
108
|
+
hasDeploy: Boolean(project.deploy),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
!/^[a-z][a-z0-9-]*$/.test(options.service) ||
|
|
114
|
+
["gcp", "registry", "ci"].includes(options.service) ||
|
|
115
|
+
/myshop/i.test(`${options.service} ${options.dockerfile}`)
|
|
116
|
+
)
|
|
117
|
+
fail("Invalid or reserved service name; MyShop must remain local-only.");
|
|
118
|
+
if (
|
|
119
|
+
!options.services &&
|
|
120
|
+
!Object.hasOwn(project.native ?? {}, options.service) &&
|
|
121
|
+
!Object.hasOwn(project.docker ?? {}, options.service) &&
|
|
122
|
+
!Object.hasOwn(project.containers ?? {}, options.service)
|
|
123
|
+
)
|
|
124
|
+
fail(`No local service named ${options.service} in zap.yaml.`);
|
|
125
|
+
if (
|
|
126
|
+
!options.services &&
|
|
127
|
+
(!existsSync(safe(options.dockerfile)) ||
|
|
128
|
+
!lstatSync(safe(options.dockerfile)).isFile())
|
|
129
|
+
)
|
|
130
|
+
fail(
|
|
131
|
+
`Missing ${options.dockerfile}. Add and test a backend-only Dockerfile listening on PORT=8080 with GET /health; use --dockerfile for another path.`,
|
|
132
|
+
);
|
|
133
|
+
if (!existsSync(safe("pnpm-lock.yaml")) || !existsSync(safe("package.json")))
|
|
134
|
+
fail(
|
|
135
|
+
"A pnpm project with committed package.json and pnpm-lock.yaml is required.",
|
|
136
|
+
);
|
|
137
|
+
const packageManager = JSON.parse(read(safe("package.json"))).packageManager;
|
|
138
|
+
if (
|
|
139
|
+
!/^pnpm@\d+\.\d+\.\d+(?:\+sha\d+\.[a-f0-9]+)?$/.test(packageManager ?? "")
|
|
140
|
+
)
|
|
141
|
+
fail(
|
|
142
|
+
"package.json must pin an exact pnpm packageManager version for reproducible CI.",
|
|
143
|
+
);
|
|
144
|
+
const repository = resolveRepository(root, options.repository);
|
|
145
|
+
if (
|
|
146
|
+
options.services &&
|
|
147
|
+
(options["seen-service"] || options["seen-dockerfile"])
|
|
148
|
+
)
|
|
149
|
+
fail("--services cannot be combined with --service or --dockerfile.");
|
|
150
|
+
|
|
151
|
+
const selection = options.services
|
|
152
|
+
? readSelection(options.services, safe)
|
|
153
|
+
: undefined;
|
|
154
|
+
|
|
155
|
+
const configPath = safe(".arcnet/project.json");
|
|
156
|
+
|
|
157
|
+
if (existsSync(configPath)) {
|
|
158
|
+
const old = JSON.parse(read(configPath));
|
|
159
|
+
if (
|
|
160
|
+
old.slug !== slug ||
|
|
161
|
+
old.repository !== repository ||
|
|
162
|
+
old.service !== options.service ||
|
|
163
|
+
old.dockerfile !== options.dockerfile ||
|
|
164
|
+
JSON.stringify(old.selection) !== JSON.stringify(selection)
|
|
165
|
+
)
|
|
166
|
+
fail(
|
|
167
|
+
"Existing ArcNet setup differs. Edit/review its committed config; setup never overwrites it.",
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const required = [
|
|
171
|
+
".arcnet/network.yaml",
|
|
172
|
+
".arcnet/.gitignore",
|
|
173
|
+
".github/workflows/deploy-arc.yaml",
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
if (
|
|
177
|
+
!project.deploy ||
|
|
178
|
+
!hasDependency(root) ||
|
|
179
|
+
required.some((p) => !existsSync(safe(p)))
|
|
180
|
+
)
|
|
181
|
+
fail(
|
|
182
|
+
"Incomplete ArcNet installation. Restore missing files from Git before rerunning.",
|
|
183
|
+
);
|
|
184
|
+
return setupReport(
|
|
185
|
+
"already-configured",
|
|
186
|
+
[],
|
|
187
|
+
"Already configured; preserved all existing files and edits.",
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (project.deploy)
|
|
192
|
+
fail(
|
|
193
|
+
"Existing deploy block detected. Migrate it explicitly; no files changed.",
|
|
194
|
+
);
|
|
195
|
+
const { network, gcpProject } = selectNetwork(slug, selection, safe);
|
|
196
|
+
|
|
197
|
+
const config = {
|
|
198
|
+
version: 1,
|
|
199
|
+
slug,
|
|
200
|
+
repository,
|
|
201
|
+
service: options.service,
|
|
202
|
+
dockerfile: options.dockerfile,
|
|
203
|
+
selection,
|
|
204
|
+
services: selection ? Object.keys(selection) : [options.service],
|
|
205
|
+
approved: false,
|
|
206
|
+
workloadIdentityProvider: "",
|
|
207
|
+
serviceAccount: `arc-deploy@${gcpProject}.iam.gserviceaccount.com`,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const deployedSelection = selection && structuredClone(selection);
|
|
211
|
+
for (const block of Object.values(deployedSelection ?? {}))
|
|
212
|
+
if (block.module === "vercel-static")
|
|
213
|
+
block.module = "./node_modules/@mp-lb/arcnet/templates/vercel-static";
|
|
214
|
+
doc.set("deploy", {
|
|
215
|
+
project: {
|
|
216
|
+
gcp: { module: "gcp-project" },
|
|
217
|
+
registry: { module: "project-base", "depends-on": ["gcp"] },
|
|
218
|
+
ci: {
|
|
219
|
+
module: "github-ci",
|
|
220
|
+
"depends-on": ["gcp"],
|
|
221
|
+
params: { repository },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
services: deployedSelection ?? {
|
|
225
|
+
[options.service]: {
|
|
226
|
+
module: "cloud-run-web",
|
|
227
|
+
domain: `${slug}-api.${network.dns.zone}`,
|
|
228
|
+
dockerfile: options.dockerfile,
|
|
229
|
+
env: [],
|
|
230
|
+
params: {
|
|
231
|
+
port: 8080,
|
|
232
|
+
"health-path": "/health",
|
|
233
|
+
"min-instances": 0,
|
|
234
|
+
"max-instances": 2,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const files = new Map([
|
|
241
|
+
[".arcnet/project.json", json(config)],
|
|
242
|
+
[".arcnet/network.yaml", stringify(network)],
|
|
243
|
+
[
|
|
244
|
+
".arcnet/.gitignore",
|
|
245
|
+
"node_modules/\n.terraform/\n*.tfstate*\n*.tfplan\ncredentials.env\n",
|
|
246
|
+
],
|
|
247
|
+
[
|
|
248
|
+
".github/workflows/deploy-arc.yaml",
|
|
249
|
+
read(join(home, "templates/deploy-arc.yaml")),
|
|
250
|
+
],
|
|
251
|
+
]);
|
|
252
|
+
|
|
253
|
+
for (const name of files.keys())
|
|
254
|
+
if (existsSync(safe(name)))
|
|
255
|
+
fail(`Refusing to overwrite ${name}. No files changed.`);
|
|
256
|
+
const manifest = addDependency(safe("package.json"));
|
|
257
|
+
if (dry)
|
|
258
|
+
return setupReport(
|
|
259
|
+
"dry-run",
|
|
260
|
+
[...files.keys(), "package.json", "zap.yaml"],
|
|
261
|
+
`Dry run: would pin @mp-lb/arcnet in package.json and add deploy.services (${config.services.join(", ")}), ${[...files.keys()].join(", ")}. No files written; no cloud commands run.`,
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
// Validate every destination before writing; preserve unrelated files and YAML comments.
|
|
265
|
+
for (const [name, content] of files) {
|
|
266
|
+
mkdirSync(dirname(safe(name)), { recursive: true });
|
|
267
|
+
writeFileSync(safe(name), content, { flag: "wx" });
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
writeFileSync(zapPath, doc.toString());
|
|
271
|
+
writeFileSync(safe("package.json"), manifest);
|
|
272
|
+
return setupReport(
|
|
273
|
+
"installed",
|
|
274
|
+
[...files.keys(), "package.json", "zap.yaml"],
|
|
275
|
+
"ArcNet files generated. Deployment is disabled (approved=false). Review the StackForge ArcNet prerequisites, network REPLACE_ placeholders, Dockerfile, domain, env whitelist and Git diff. Install: pnpm install. Offline check: pnpm exec arcnet check. Only the selected services are included; no existing Vercel project is automatically adopted. MyShop stays local-only.",
|
|
276
|
+
);
|
|
277
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type SetupReport = {
|
|
2
|
+
operation: "setup";
|
|
3
|
+
status: "dry-run" | "installed" | "already-configured";
|
|
4
|
+
files: string[];
|
|
5
|
+
message: string;
|
|
6
|
+
};
|
|
7
|
+
export const setupReport = (
|
|
8
|
+
status: SetupReport["status"],
|
|
9
|
+
files: string[],
|
|
10
|
+
message: string,
|
|
11
|
+
): SetupReport => ({ operation: "setup", status, files, message });
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { setupReport } from "./setupReport.js";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { isDeepStrictEqual } from "node:util";
|
|
5
|
+
import { addDependency, hasDependency } from "./addDependency.js";
|
|
6
|
+
import { packageRoot } from "./packageInfo.js";
|
|
7
|
+
import { vercelInputSchema } from "./schemas.js";
|
|
8
|
+
import { validateConfig } from "./validateConfig.js";
|
|
9
|
+
const home = packageRoot;
|
|
10
|
+
const json = (value: unknown) => JSON.stringify(value, null, 2) + "\n";
|
|
11
|
+
|
|
12
|
+
export const setupVercel = ({
|
|
13
|
+
root,
|
|
14
|
+
slug,
|
|
15
|
+
safe,
|
|
16
|
+
file,
|
|
17
|
+
dry,
|
|
18
|
+
hasDeploy,
|
|
19
|
+
}: {
|
|
20
|
+
root: string;
|
|
21
|
+
slug: string;
|
|
22
|
+
safe: (name: string) => string;
|
|
23
|
+
file: string;
|
|
24
|
+
dry: boolean;
|
|
25
|
+
hasDeploy: boolean;
|
|
26
|
+
}) => {
|
|
27
|
+
if (hasDeploy)
|
|
28
|
+
throw new Error(
|
|
29
|
+
"Existing Terraform deploy block found. Vercel-only reference mode cannot silently replace it.",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const input = vercelInputSchema.parse(
|
|
33
|
+
JSON.parse(readFileSync(safe(file), "utf8")),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const config = validateConfig(
|
|
37
|
+
{
|
|
38
|
+
version: 1,
|
|
39
|
+
mode: "vercel",
|
|
40
|
+
slug,
|
|
41
|
+
repository: input.repository,
|
|
42
|
+
approved: false,
|
|
43
|
+
account: input.account,
|
|
44
|
+
sites: input.sites,
|
|
45
|
+
},
|
|
46
|
+
root,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
Object.keys(input).some(
|
|
51
|
+
(k) => !["repository", "account", "sites"].includes(k),
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
throw new Error(
|
|
55
|
+
"Vercel input supports only repository, account and sites; ownership/import is separate.",
|
|
56
|
+
);
|
|
57
|
+
const path = safe(".arcnet/project.json");
|
|
58
|
+
|
|
59
|
+
const files = new Map([
|
|
60
|
+
[".arcnet/project.json", json(config)],
|
|
61
|
+
[".arcnet/.gitignore", "project.json.new\n"],
|
|
62
|
+
[
|
|
63
|
+
".github/workflows/deploy-arc.yaml",
|
|
64
|
+
readFileSync(join(home, "templates/deploy-vercel.yaml"), "utf8"),
|
|
65
|
+
],
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
if (existsSync(path)) {
|
|
69
|
+
const old = validateConfig(JSON.parse(readFileSync(path, "utf8")), root);
|
|
70
|
+
const previous = { ...old, approved: false };
|
|
71
|
+
const expected = { ...config, approved: false };
|
|
72
|
+
|
|
73
|
+
// Creation promotes a planned new site to an explicit reference; a setup rerun must preserve that receipt.
|
|
74
|
+
for (const [name, site] of Object.entries(expected.sites)) {
|
|
75
|
+
if (site.mode === "new" && previous.sites?.[name]?.mode === "existing") {
|
|
76
|
+
expected.sites[name] = {
|
|
77
|
+
...site,
|
|
78
|
+
mode: "existing",
|
|
79
|
+
projectId: previous.sites[name].projectId,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
delete expected.sites[name]!.creationApproved;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!isDeepStrictEqual(previous, expected))
|
|
87
|
+
throw new Error(
|
|
88
|
+
"Existing ArcNet configuration differs. Review committed config instead of overwriting it. After explicit creation, update the source input to the returned existing reference.",
|
|
89
|
+
);
|
|
90
|
+
if (!hasDependency(root))
|
|
91
|
+
throw new Error(
|
|
92
|
+
"Incomplete installation: add the ArcNet package dependency.",
|
|
93
|
+
);
|
|
94
|
+
for (const name of files.keys())
|
|
95
|
+
if (!existsSync(safe(name)))
|
|
96
|
+
throw new Error(`Incomplete installation: restore ${name} from Git.`);
|
|
97
|
+
return setupReport(
|
|
98
|
+
"already-configured",
|
|
99
|
+
[],
|
|
100
|
+
"Already configured; preserved all settings and files.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const name of files.keys())
|
|
105
|
+
if (existsSync(safe(name)))
|
|
106
|
+
throw new Error(`Refusing to overwrite ${name}. No files changed.`);
|
|
107
|
+
const manifest = addDependency(safe("package.json"));
|
|
108
|
+
if (dry)
|
|
109
|
+
return setupReport(
|
|
110
|
+
"dry-run",
|
|
111
|
+
[...files.keys(), "package.json"],
|
|
112
|
+
`Dry run: Vercel-only references for ${Object.keys(config.sites).join(", ")}. No files or cloud resources changed; no GCP/WIF/Terraform.`,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
for (const [name, content] of files) {
|
|
116
|
+
mkdirSync(dirname(safe(name)), { recursive: true });
|
|
117
|
+
writeFileSync(safe(name), content, { flag: "wx" });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
writeFileSync(safe("package.json"), manifest);
|
|
121
|
+
return setupReport(
|
|
122
|
+
"installed",
|
|
123
|
+
[...files.keys(), "package.json"],
|
|
124
|
+
"Generated Vercel-only config and GitHub workflow (approved=false). No zap.yaml changes, GCP, Terraform, DNS or project settings ownership. Offline plan: pnpm install, then pnpm exec arcnet plan",
|
|
125
|
+
);
|
|
126
|
+
};
|
package/src/stageSite.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cpSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
lstatSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { fail } from "./fail.js";
|
|
13
|
+
import { inspectStaticTree } from "./inspectStaticTree.js";
|
|
14
|
+
import { safePath } from "./safePath.js";
|
|
15
|
+
import type { VercelAccount, VercelSite } from "./schemas.js";
|
|
16
|
+
|
|
17
|
+
export const stageSite = (
|
|
18
|
+
root: string,
|
|
19
|
+
site: VercelSite,
|
|
20
|
+
account: VercelAccount,
|
|
21
|
+
) => {
|
|
22
|
+
const output = safePath(root, site.output);
|
|
23
|
+
if (
|
|
24
|
+
!existsSync(output) ||
|
|
25
|
+
!lstatSync(output).isDirectory() ||
|
|
26
|
+
!existsSync(join(output, "index.html"))
|
|
27
|
+
)
|
|
28
|
+
fail(`Static output needs index.html: ${site.output}`);
|
|
29
|
+
inspectStaticTree(output);
|
|
30
|
+
const stage = mkdtempSync(join(tmpdir(), "arcnet-static-"));
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(join(stage, ".vercel/output"), { recursive: true });
|
|
34
|
+
cpSync(output, join(stage, ".vercel/output/static"), { recursive: true });
|
|
35
|
+
writeFileSync(
|
|
36
|
+
join(stage, ".vercel/output/config.json"),
|
|
37
|
+
JSON.stringify(site.outputConfig),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
writeFileSync(
|
|
41
|
+
join(stage, ".vercel/project.json"),
|
|
42
|
+
JSON.stringify({
|
|
43
|
+
projectId: site.projectId,
|
|
44
|
+
orgId: account.id,
|
|
45
|
+
projectName: site.projectName,
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return stage;
|
|
50
|
+
} catch (e) {
|
|
51
|
+
rmSync(stage, { recursive: true, force: true });
|
|
52
|
+
throw e;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { fail } from "./fail.js";
|
|
2
|
+
import { safePath } from "./safePath.js";
|
|
3
|
+
import type { VercelConfig } from "./schemas.js";
|
|
4
|
+
import { vercelConfigSchema } from "./schemas.js";
|
|
5
|
+
|
|
6
|
+
export const validateConfig = (input: unknown, root: string): VercelConfig => {
|
|
7
|
+
const config = vercelConfigSchema.parse(input);
|
|
8
|
+
const identities = new Set<string>();
|
|
9
|
+
|
|
10
|
+
for (const site of Object.values(config.sites)) {
|
|
11
|
+
for (const id of [site.projectId, site.projectName].filter(
|
|
12
|
+
(id): id is string => Boolean(id),
|
|
13
|
+
)) {
|
|
14
|
+
if (identities.has(id)) fail("Duplicate Vercel project selection.");
|
|
15
|
+
identities.add(id);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
safePath(root, site.output);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return config;
|
|
22
|
+
};
|