@deployfoundation/foundation-deploy 0.2.51 → 0.2.53
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 +4 -1
- package/dist/bin/app.js +6 -3
- package/dist/bin/foundation-deploy.js +141 -305
- package/dist/bin/release-account.js +1 -0
- package/dist/chunk-1pww63wd.js +301 -0
- package/dist/chunk-3614v9rr.js +907 -0
- package/dist/chunk-5wgwscd0.js +4 -0
- package/dist/{chunk-gnz747z2.js → chunk-96fjaysn.js} +204 -884
- package/dist/chunk-fjdgxw8h.js +516 -0
- package/dist/chunk-jf5rw6h4.js +1117 -0
- package/dist/chunk-m76jt5rc.js +376 -0
- package/dist/chunk-mh1206ne.js +1764 -0
- package/dist/chunk-nvsj7bbw.js +259 -0
- package/dist/chunk-p7zkm4pp.js +151 -0
- package/dist/chunk-pxhftba1.js +336 -0
- package/dist/{chunk-efqdhd2e.js → chunk-spq5kr1t.js} +333 -90
- package/dist/chunk-tey0gh86.js +38 -0
- package/dist/src/index.js +17 -13
- package/package.json +3 -1
- package/src/deploy/cli.ts +71 -0
- package/src/deploy/crm.ts +491 -0
- package/src/deploy/runtime-secret.ts +9 -0
- package/src/stacks/agent-stack.ts +68 -11
- package/src/stacks/api-stack.ts +340 -87
- package/src/stacks/data-stack.ts +27 -1
- package/src/stacks/secret-bearing-reads.ts +131 -0
package/README.md
CHANGED
|
@@ -54,9 +54,12 @@ bun run packages/infra/bin/foundation-deploy.ts <command> --instance <path>
|
|
|
54
54
|
| `github-app-create` | Create the instance's GitHub App from the shipped manifest template. |
|
|
55
55
|
| `slack-manifest` | Print the instance's Slack app manifest as JSON (the Slack CLI's `get-manifest` hook contract). |
|
|
56
56
|
| `stage-customization` | Validate and stage a tenant-owned runtime config. |
|
|
57
|
+
| `crm-key` | Write the company CRM's `crm/1` key, piped in with `--stdin`, into `<secretPrefix>/crm/http` (`crm.provider` kind `http`). |
|
|
58
|
+
| `crm-check` | Run the `crm/1` conformance suite: the read cases against the instance's CRM with its stored key, or, with `--base-url --tenant --key-stdin` and no instance, any server, write cases included. |
|
|
59
|
+
| `crm-replay` | Replay the agent's CRM writes (JSONL) into the instance's own table after a rollback to the DynamoDB provider. |
|
|
57
60
|
|
|
58
61
|
`--instance <path>` is required for every command (or `FOUNDATION_INSTANCE_FILE`
|
|
59
|
-
in a pipeline). There is no instance *name*, no `instances/` directory and no
|
|
62
|
+
in a pipeline), except `crm-check --base-url`, which checks a server rather than a deployment. There is no instance *name*, no `instances/` directory and no
|
|
60
63
|
default: a deploy that guessed its target would be a deploy into the wrong
|
|
61
64
|
account.
|
|
62
65
|
|
package/dist/bin/app.js
CHANGED
|
@@ -8,14 +8,17 @@ import {
|
|
|
8
8
|
FoundationPages,
|
|
9
9
|
FoundationPipeline,
|
|
10
10
|
NewsletterStack
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-spq5kr1t.js";
|
|
12
|
+
import"../chunk-pxhftba1.js";
|
|
13
|
+
import"../chunk-dz91avrb.js";
|
|
12
14
|
import {
|
|
13
15
|
adminsFor,
|
|
14
16
|
instanceNames,
|
|
15
17
|
loadInstanceFile,
|
|
16
18
|
provisionsIntegration
|
|
17
|
-
} from "../chunk-
|
|
18
|
-
import"../chunk-
|
|
19
|
+
} from "../chunk-fjdgxw8h.js";
|
|
20
|
+
import"../chunk-96fjaysn.js";
|
|
21
|
+
import"../chunk-5wgwscd0.js";
|
|
19
22
|
|
|
20
23
|
// bin/app.ts
|
|
21
24
|
import * as cdk from "aws-cdk-lib";
|
|
@@ -1,297 +1,74 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @bun
|
|
3
3
|
import {
|
|
4
|
-
BASE_SLACK_EVENTS,
|
|
5
|
-
CAPABILITY_IDS,
|
|
6
|
-
CUSTOMIZATION_ARTIFACT_NAME,
|
|
7
4
|
RELEASE_BUCKET_ENV,
|
|
8
5
|
RELEASE_VERSION_RE,
|
|
6
|
+
manifestKey,
|
|
7
|
+
releaseCacheDir,
|
|
8
|
+
skillsKey,
|
|
9
|
+
verifyManifest
|
|
10
|
+
} from "../chunk-pxhftba1.js";
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_RELEASE_BUCKET,
|
|
13
|
+
expectedKeyArn
|
|
14
|
+
} from "../chunk-dz91avrb.js";
|
|
15
|
+
import {
|
|
16
|
+
argv,
|
|
17
|
+
aws,
|
|
18
|
+
awsContext,
|
|
19
|
+
awsMutate,
|
|
20
|
+
callerAccountId,
|
|
21
|
+
cdkEnv,
|
|
22
|
+
createSecretString,
|
|
23
|
+
instanceBanner,
|
|
24
|
+
loadInstanceContext,
|
|
25
|
+
putSecretJson,
|
|
26
|
+
putSecretString,
|
|
27
|
+
readSecretJson,
|
|
28
|
+
readSecretString,
|
|
29
|
+
resolveInstanceFilePath,
|
|
30
|
+
run,
|
|
31
|
+
runCapture,
|
|
32
|
+
secretExists,
|
|
33
|
+
stackExists,
|
|
34
|
+
stackOutput
|
|
35
|
+
} from "../chunk-nvsj7bbw.js";
|
|
36
|
+
import {
|
|
37
|
+
CUSTOMIZATION_ARTIFACT_NAME,
|
|
9
38
|
instanceNames,
|
|
39
|
+
mongoCollectionPolicyEnv,
|
|
40
|
+
slackCommandPrefix,
|
|
41
|
+
versionedConfigKey
|
|
42
|
+
} from "../chunk-fjdgxw8h.js";
|
|
43
|
+
import"../chunk-m76jt5rc.js";
|
|
44
|
+
import"../chunk-3614v9rr.js";
|
|
45
|
+
import {
|
|
46
|
+
BASE_SLACK_EVENTS,
|
|
47
|
+
CAPABILITY_IDS,
|
|
10
48
|
isScheduledJob,
|
|
11
49
|
isWebhookJob,
|
|
12
50
|
jobWebhookKey,
|
|
13
|
-
loadInstanceFile,
|
|
14
|
-
manifestKey,
|
|
15
51
|
parseInstanceConfig,
|
|
16
52
|
parseSchedule,
|
|
17
|
-
releaseCacheDir,
|
|
18
53
|
requiredGithubPermissions,
|
|
19
54
|
requiredSlackCommands,
|
|
20
|
-
requiredSlackScopes
|
|
21
|
-
|
|
22
|
-
slackCommandPrefix,
|
|
23
|
-
verifyManifest,
|
|
24
|
-
versionedConfigKey
|
|
25
|
-
} from "../chunk-gnz747z2.js";
|
|
55
|
+
requiredSlackScopes
|
|
56
|
+
} from "../chunk-96fjaysn.js";
|
|
26
57
|
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
} from "../chunk-dz91avrb.js";
|
|
58
|
+
__require
|
|
59
|
+
} from "../chunk-5wgwscd0.js";
|
|
30
60
|
|
|
31
61
|
// src/deploy/cli.ts
|
|
32
62
|
import { readFileSync as readFileSync10 } from "node:fs";
|
|
33
63
|
|
|
34
|
-
// src/deploy/aws.ts
|
|
35
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
36
|
-
import { tmpdir } from "node:os";
|
|
37
|
-
import { join } from "node:path";
|
|
38
|
-
|
|
39
|
-
// src/deploy/instance.ts
|
|
40
|
-
import { dirname } from "node:path";
|
|
41
|
-
var INSTANCE_FILE_ENV = "FOUNDATION_INSTANCE_FILE";
|
|
42
|
-
function resolveInstanceFilePath(argv = [], env = {}) {
|
|
43
|
-
for (let i = 0;i < argv.length; i++) {
|
|
44
|
-
const arg = argv[i] ?? "";
|
|
45
|
-
if (arg === "--instance") {
|
|
46
|
-
const next = argv[i + 1];
|
|
47
|
-
if (next === undefined || next.startsWith("-"))
|
|
48
|
-
throw new Error("--instance needs a path, e.g. --instance .foundation/instance.yaml");
|
|
49
|
-
return next;
|
|
50
|
-
}
|
|
51
|
-
if (arg.startsWith("--instance=")) {
|
|
52
|
-
const value = arg.slice("--instance=".length);
|
|
53
|
-
if (value === "")
|
|
54
|
-
throw new Error("--instance needs a path");
|
|
55
|
-
return value;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const fromEnv = env[INSTANCE_FILE_ENV];
|
|
59
|
-
if (fromEnv !== undefined && fromEnv !== "")
|
|
60
|
-
return fromEnv;
|
|
61
|
-
throw new Error(`--instance <path> is required (or set ${INSTANCE_FILE_ENV}); it is the path to the deployment's instance YAML`);
|
|
62
|
-
}
|
|
63
|
-
function loadInstanceContext(path, cwd = process.cwd()) {
|
|
64
|
-
const file = loadInstanceFile(path, cwd);
|
|
65
|
-
return { ...file, root: dirname(file.path) };
|
|
66
|
-
}
|
|
67
|
-
function loadInstanceFromArgs(argv = process.argv.slice(2), env = process.env) {
|
|
68
|
-
return loadInstanceContext(resolveInstanceFilePath(argv, env));
|
|
69
|
-
}
|
|
70
|
-
function instanceBanner(context) {
|
|
71
|
-
const { instance } = context;
|
|
72
|
-
return `instance: ${instance.name} (account ${instance.aws.account}, region ${instance.aws.region})
|
|
73
|
-
file: ${context.path}
|
|
74
|
-
config: ${context.configPath}`;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// src/deploy/sh.ts
|
|
78
|
-
import { spawn as nodeSpawn } from "node:child_process";
|
|
79
|
-
function formatCommand(cmd, env) {
|
|
80
|
-
const prefix = Object.entries(env ?? {}).map(([k, v]) => `${k}=${quote(v)}`).join(" ");
|
|
81
|
-
const body = cmd.map(quote).join(" ");
|
|
82
|
-
return prefix === "" ? body : `${prefix} ${body}`;
|
|
83
|
-
}
|
|
84
|
-
function quote(value) {
|
|
85
|
-
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(value) ? value : `'${value.replaceAll("'", `'\\''`)}'`;
|
|
86
|
-
}
|
|
87
|
-
function start(cmd, opts, stdout) {
|
|
88
|
-
const [bin, ...rest] = cmd;
|
|
89
|
-
if (bin === undefined)
|
|
90
|
-
throw new Error("run: empty command");
|
|
91
|
-
const child = nodeSpawn(bin, rest, {
|
|
92
|
-
cwd: opts.cwd,
|
|
93
|
-
env: { ...process.env, ...opts.env },
|
|
94
|
-
stdio: [
|
|
95
|
-
opts.stdin === undefined ? "ignore" : "pipe",
|
|
96
|
-
stdout,
|
|
97
|
-
stdout === "inherit" ? "inherit" : "pipe"
|
|
98
|
-
]
|
|
99
|
-
});
|
|
100
|
-
if (opts.stdin !== undefined && child.stdin !== null) {
|
|
101
|
-
child.stdin.end(opts.stdin);
|
|
102
|
-
}
|
|
103
|
-
return child;
|
|
104
|
-
}
|
|
105
|
-
function exited(child) {
|
|
106
|
-
return new Promise((resolve, reject) => {
|
|
107
|
-
child.once("error", reject);
|
|
108
|
-
child.once("close", (code, signal) => resolve(code ?? (signal === null ? 1 : 128)));
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
function collect(stream) {
|
|
112
|
-
if (stream === null)
|
|
113
|
-
return Promise.resolve("");
|
|
114
|
-
return new Promise((resolve, reject) => {
|
|
115
|
-
const chunks = [];
|
|
116
|
-
stream.on("data", (c) => chunks.push(c));
|
|
117
|
-
stream.once("error", reject);
|
|
118
|
-
stream.once("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
async function run(cmd, opts = {}) {
|
|
122
|
-
if (opts.dryRun === true) {
|
|
123
|
-
console.log(` $ ${formatCommand(cmd, opts.env)}`);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
const code = await exited(start(cmd, opts, "inherit"));
|
|
127
|
-
if (code !== 0)
|
|
128
|
-
throw new Error(`${formatCommand(cmd)} exited ${code}`);
|
|
129
|
-
}
|
|
130
|
-
async function runCapture(cmd, opts = {}) {
|
|
131
|
-
const child = start(cmd, opts, "pipe");
|
|
132
|
-
const [out, err, code] = await Promise.all([
|
|
133
|
-
collect(child.stdout),
|
|
134
|
-
collect(child.stderr),
|
|
135
|
-
exited(child)
|
|
136
|
-
]);
|
|
137
|
-
if (code !== 0)
|
|
138
|
-
throw new Error(`${formatCommand(cmd)} exited ${code}: ${err.trim()}`);
|
|
139
|
-
return out.trim();
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// src/deploy/aws.ts
|
|
143
|
-
function resolveProfile(fallback, explicit, env = process.env) {
|
|
144
|
-
if (env.FOUNDATION_NO_PROFILE === "1")
|
|
145
|
-
return "";
|
|
146
|
-
if (explicit !== undefined)
|
|
147
|
-
return explicit;
|
|
148
|
-
if (env.AWS_PROFILE !== undefined && env.AWS_PROFILE !== "")
|
|
149
|
-
return env.AWS_PROFILE;
|
|
150
|
-
if (env.GITHUB_ACTIONS === "true")
|
|
151
|
-
return "";
|
|
152
|
-
return fallback;
|
|
153
|
-
}
|
|
154
|
-
function awsContext(overrides = {}) {
|
|
155
|
-
const env = overrides.env ?? process.env;
|
|
156
|
-
const paths = overrides.paths ?? loadInstanceFromArgs();
|
|
157
|
-
const instance = paths.instance;
|
|
158
|
-
return {
|
|
159
|
-
profile: resolveProfile(instance.aws.profile, overrides.profile, env),
|
|
160
|
-
region: overrides.region ?? env.AWS_REGION ?? instance.aws.region,
|
|
161
|
-
dryRun: overrides.dryRun ?? false,
|
|
162
|
-
instance,
|
|
163
|
-
paths,
|
|
164
|
-
names: instanceNames(instance)
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
function argv(ctx, args) {
|
|
168
|
-
const profile = ctx.profile === "" ? [] : ["--profile", ctx.profile];
|
|
169
|
-
return ["aws", ...args, ...profile, "--region", ctx.region];
|
|
170
|
-
}
|
|
171
|
-
function cdkEnv(ctx) {
|
|
172
|
-
return {
|
|
173
|
-
...ctx.profile === "" ? {} : { AWS_PROFILE: ctx.profile },
|
|
174
|
-
AWS_REGION: ctx.region,
|
|
175
|
-
CDK_DEFAULT_REGION: ctx.region
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
async function aws(ctx, args, stdin) {
|
|
179
|
-
return runCapture(argv(ctx, args), { stdin });
|
|
180
|
-
}
|
|
181
|
-
async function awsMutate(ctx, args, stdin) {
|
|
182
|
-
if (ctx.dryRun === true) {
|
|
183
|
-
console.log(` $ ${argv(ctx, args).join(" ")}${stdin === undefined ? "" : " < (stdin)"}`);
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
await run(argv(ctx, args), { stdin });
|
|
187
|
-
}
|
|
188
|
-
async function stackOutput(ctx, stack, key) {
|
|
189
|
-
const value = await aws(ctx, [
|
|
190
|
-
"cloudformation",
|
|
191
|
-
"describe-stacks",
|
|
192
|
-
"--stack-name",
|
|
193
|
-
stack,
|
|
194
|
-
"--query",
|
|
195
|
-
`Stacks[0].Outputs[?OutputKey=='${key}'].OutputValue`,
|
|
196
|
-
"--output",
|
|
197
|
-
"text"
|
|
198
|
-
]);
|
|
199
|
-
if (value === "" || value === "None")
|
|
200
|
-
throw new Error(`stack ${stack} has no output ${key} — has it been deployed?`);
|
|
201
|
-
return value;
|
|
202
|
-
}
|
|
203
|
-
async function stackExists(ctx, stack) {
|
|
204
|
-
try {
|
|
205
|
-
await aws(ctx, [
|
|
206
|
-
"cloudformation",
|
|
207
|
-
"describe-stacks",
|
|
208
|
-
"--stack-name",
|
|
209
|
-
stack,
|
|
210
|
-
"--output",
|
|
211
|
-
"text"
|
|
212
|
-
]);
|
|
213
|
-
return true;
|
|
214
|
-
} catch {
|
|
215
|
-
return false;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
async function secretExists(ctx, secretId) {
|
|
219
|
-
try {
|
|
220
|
-
await aws(ctx, [
|
|
221
|
-
"secretsmanager",
|
|
222
|
-
"describe-secret",
|
|
223
|
-
"--secret-id",
|
|
224
|
-
secretId,
|
|
225
|
-
"--output",
|
|
226
|
-
"text"
|
|
227
|
-
]);
|
|
228
|
-
return true;
|
|
229
|
-
} catch {
|
|
230
|
-
return false;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
async function readSecretString(ctx, secretId) {
|
|
234
|
-
return aws(ctx, [
|
|
235
|
-
"secretsmanager",
|
|
236
|
-
"get-secret-value",
|
|
237
|
-
"--secret-id",
|
|
238
|
-
secretId,
|
|
239
|
-
"--query",
|
|
240
|
-
"SecretString",
|
|
241
|
-
"--output",
|
|
242
|
-
"text"
|
|
243
|
-
]);
|
|
244
|
-
}
|
|
245
|
-
async function readSecretJson(ctx, secretId) {
|
|
246
|
-
return JSON.parse(await readSecretString(ctx, secretId));
|
|
247
|
-
}
|
|
248
|
-
async function putSecretString(ctx, secretId, value) {
|
|
249
|
-
await throughSecretFile(value, (file) => awsMutate(ctx, [
|
|
250
|
-
"secretsmanager",
|
|
251
|
-
"put-secret-value",
|
|
252
|
-
"--secret-id",
|
|
253
|
-
secretId,
|
|
254
|
-
"--secret-string",
|
|
255
|
-
`file://${file}`
|
|
256
|
-
]));
|
|
257
|
-
}
|
|
258
|
-
async function createSecretString(ctx, secretId, value, description) {
|
|
259
|
-
await throughSecretFile(value, (file) => awsMutate(ctx, [
|
|
260
|
-
"secretsmanager",
|
|
261
|
-
"create-secret",
|
|
262
|
-
"--name",
|
|
263
|
-
secretId,
|
|
264
|
-
"--description",
|
|
265
|
-
description,
|
|
266
|
-
"--secret-string",
|
|
267
|
-
`file://${file}`
|
|
268
|
-
]));
|
|
269
|
-
}
|
|
270
|
-
async function throughSecretFile(value, body) {
|
|
271
|
-
const dir = mkdtempSync(join(tmpdir(), "foundation-secret-"));
|
|
272
|
-
const file = join(dir, "value.json");
|
|
273
|
-
try {
|
|
274
|
-
writeFileSync(file, value, { mode: 384 });
|
|
275
|
-
await body(file);
|
|
276
|
-
} finally {
|
|
277
|
-
rmSync(dir, { recursive: true, force: true });
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
async function putSecretJson(ctx, secretId, value) {
|
|
281
|
-
await putSecretString(ctx, secretId, JSON.stringify(value));
|
|
282
|
-
}
|
|
283
|
-
async function callerAccountId(ctx) {
|
|
284
|
-
return aws(ctx, ["sts", "get-caller-identity", "--query", "Account", "--output", "text"]);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
64
|
// src/deploy/config-sync.ts
|
|
288
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync
|
|
289
|
-
import { tmpdir
|
|
290
|
-
import { join as
|
|
65
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, mkdtempSync, readFileSync as readFileSync3, readdirSync } from "node:fs";
|
|
66
|
+
import { tmpdir } from "node:os";
|
|
67
|
+
import { join as join2, resolve as resolve3 } from "node:path";
|
|
291
68
|
|
|
292
69
|
// src/deploy/paths.ts
|
|
293
70
|
import { existsSync, readFileSync } from "node:fs";
|
|
294
|
-
import { dirname
|
|
71
|
+
import { dirname, resolve } from "node:path";
|
|
295
72
|
import { fileURLToPath } from "node:url";
|
|
296
73
|
var PACKAGE_NAME = "@deployfoundation/foundation-deploy";
|
|
297
74
|
function packageRoot(from) {
|
|
@@ -305,20 +82,20 @@ function packageRoot(from) {
|
|
|
305
82
|
return dir;
|
|
306
83
|
} catch {}
|
|
307
84
|
}
|
|
308
|
-
const parent =
|
|
85
|
+
const parent = dirname(dir);
|
|
309
86
|
if (parent === dir)
|
|
310
87
|
throw new Error(`cannot find the ${PACKAGE_NAME} package root above ${from}`);
|
|
311
88
|
dir = parent;
|
|
312
89
|
}
|
|
313
90
|
}
|
|
314
|
-
var INFRA_ROOT = packageRoot(
|
|
91
|
+
var INFRA_ROOT = packageRoot(dirname(fileURLToPath(import.meta.url)));
|
|
315
92
|
var FOUNDATION_ROOT = resolve(INFRA_ROOT, "..", "..");
|
|
316
93
|
var AGENT_DOCKERFILE = "packages/infra/agent-image/Dockerfile";
|
|
317
94
|
var PACKAGE_ASSETS = resolve(INFRA_ROOT, "src", "deploy", "assets");
|
|
318
95
|
|
|
319
96
|
// src/deploy/release.ts
|
|
320
97
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
321
|
-
import { dirname as
|
|
98
|
+
import { dirname as dirname2, isAbsolute, join, resolve as resolve2 } from "node:path";
|
|
322
99
|
|
|
323
100
|
// src/release/kms.ts
|
|
324
101
|
import { KMSClient, SignCommand, VerifyCommand } from "@aws-sdk/client-kms";
|
|
@@ -357,14 +134,14 @@ function flag(args, name) {
|
|
|
357
134
|
return value ?? (inline === undefined ? undefined : inline.slice(name.length + 1));
|
|
358
135
|
}
|
|
359
136
|
function toolVersion(infraRoot = INFRA_ROOT) {
|
|
360
|
-
const { version } = JSON.parse(readFileSync2(
|
|
137
|
+
const { version } = JSON.parse(readFileSync2(join(infraRoot, "package.json"), "utf8"));
|
|
361
138
|
return version;
|
|
362
139
|
}
|
|
363
140
|
function foundationVersion(release) {
|
|
364
141
|
return release?.version ?? `v${toolVersion()}`;
|
|
365
142
|
}
|
|
366
143
|
function hasFoundationWorkspace(foundationRoot = FOUNDATION_ROOT) {
|
|
367
|
-
return existsSync2(
|
|
144
|
+
return existsSync2(join(foundationRoot, "packages", "gateway", "package.json"));
|
|
368
145
|
}
|
|
369
146
|
function releaseRequest(args, options = {}) {
|
|
370
147
|
const env = options.env ?? process.env;
|
|
@@ -387,7 +164,7 @@ function releaseRequest(args, options = {}) {
|
|
|
387
164
|
return { version, bucket };
|
|
388
165
|
}
|
|
389
166
|
async function s3Download(ctx, bucket, key, dest) {
|
|
390
|
-
mkdirSync(
|
|
167
|
+
mkdirSync(dirname2(dest), { recursive: true });
|
|
391
168
|
await aws(ctx, ["s3", "cp", `s3://${bucket}/${key}`, dest]);
|
|
392
169
|
}
|
|
393
170
|
async function verifyRelease(ctx, request, deps = {}) {
|
|
@@ -397,7 +174,7 @@ async function verifyRelease(ctx, request, deps = {}) {
|
|
|
397
174
|
const local = localManifestRef(request);
|
|
398
175
|
const version = request.version;
|
|
399
176
|
const cacheDir = deps.cacheDir ?? releaseCacheDir(version ?? "unversioned", env);
|
|
400
|
-
const manifestPath = local ??
|
|
177
|
+
const manifestPath = local ?? join(cacheDir, "manifest.json");
|
|
401
178
|
if (local === undefined) {
|
|
402
179
|
const source = s3ManifestRef(request);
|
|
403
180
|
await download(ctx, source.bucket, source.key, manifestPath);
|
|
@@ -408,7 +185,7 @@ async function verifyRelease(ctx, request, deps = {}) {
|
|
|
408
185
|
...version === undefined ? {} : { expectVersion: version },
|
|
409
186
|
...deps.verifyArtifacts === false ? {} : {
|
|
410
187
|
readArtifact: async (key) => {
|
|
411
|
-
const dest =
|
|
188
|
+
const dest = join(cacheDir, "artifacts", key.split("/").slice(-1)[0] ?? "artifact");
|
|
412
189
|
await download(ctx, request.bucket, key, dest);
|
|
413
190
|
return readFileSync2(dest);
|
|
414
191
|
}
|
|
@@ -425,7 +202,7 @@ function plannedRelease(request) {
|
|
|
425
202
|
return {
|
|
426
203
|
version,
|
|
427
204
|
bucket: request.bucket,
|
|
428
|
-
manifestPath: local ??
|
|
205
|
+
manifestPath: local ?? join(releaseCacheDir(version), "manifest.json")
|
|
429
206
|
};
|
|
430
207
|
}
|
|
431
208
|
function localManifestRef(request) {
|
|
@@ -524,13 +301,13 @@ async function configSync(ctx, opts = {}) {
|
|
|
524
301
|
}
|
|
525
302
|
async function unpackReleaseSkills(ctx, release) {
|
|
526
303
|
const key = release.manifest?.skills.key ?? skillsKey(release.version);
|
|
527
|
-
const dir =
|
|
528
|
-
const tarball =
|
|
304
|
+
const dir = mkdtempSync(join2(tmpdir(), "foundation-skills-"));
|
|
305
|
+
const tarball = join2(dir, "skills.tar.gz");
|
|
529
306
|
await awsMutate(ctx, ["s3", "cp", `s3://${release.bucket}/${key}`, tarball]);
|
|
530
307
|
return await extractSkillsTarball(tarball, dir, { dryRun: ctx.dryRun });
|
|
531
308
|
}
|
|
532
309
|
async function extractSkillsTarball(tarball, dir, opts = {}) {
|
|
533
|
-
const contents =
|
|
310
|
+
const contents = join2(dir, "skills");
|
|
534
311
|
await run(["tar", "-xzf", tarball, "-C", dir], { dryRun: opts.dryRun });
|
|
535
312
|
if (opts.dryRun !== true && !existsSync3(contents))
|
|
536
313
|
throw new Error(`${tarball} has no top-level skills/ directory`);
|
|
@@ -543,7 +320,7 @@ async function extractSkillsTarball(tarball, dir, opts = {}) {
|
|
|
543
320
|
import { createHash } from "node:crypto";
|
|
544
321
|
import { existsSync as existsSync4 } from "node:fs";
|
|
545
322
|
import { createRequire } from "node:module";
|
|
546
|
-
import { join as
|
|
323
|
+
import { join as join4 } from "node:path";
|
|
547
324
|
|
|
548
325
|
// ../connectors/src/github.ts
|
|
549
326
|
var GITHUB_TOKEN_RESPONSE_MAX_BYTES = 128 * 1024;
|
|
@@ -773,9 +550,6 @@ function isSafeOpaqueString(value, max) {
|
|
|
773
550
|
function isPlainObject(value) {
|
|
774
551
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
775
552
|
}
|
|
776
|
-
// ../connectors/src/crm.ts
|
|
777
|
-
var CRM_MAX_REQUEST_BYTES = 512 * 1024;
|
|
778
|
-
var CRM_MAX_RESPONSE_BYTES = 512 * 1024;
|
|
779
553
|
// ../connectors/src/upwork.ts
|
|
780
554
|
var UPWORK_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
781
555
|
var UPWORK_MAX_ACCESS_TOKEN_TTL_SECONDS = 24 * 60 * 60;
|
|
@@ -962,8 +736,8 @@ function adminsCsvFromFile(path) {
|
|
|
962
736
|
|
|
963
737
|
// src/deploy/endpoint.ts
|
|
964
738
|
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
965
|
-
import { tmpdir as
|
|
966
|
-
import { join as
|
|
739
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
740
|
+
import { join as join3 } from "node:path";
|
|
967
741
|
function cliRunner(ctx) {
|
|
968
742
|
return {
|
|
969
743
|
capture: (args) => aws(ctx, args),
|
|
@@ -1111,8 +885,8 @@ async function smokeInvoke(runner, opts) {
|
|
|
1111
885
|
await runner.mutate([...args, "/dev/stdout"]);
|
|
1112
886
|
return;
|
|
1113
887
|
}
|
|
1114
|
-
const dir = await mkdtemp(
|
|
1115
|
-
const outFile =
|
|
888
|
+
const dir = await mkdtemp(join3(tmpdir2(), "foundation-smoke-"));
|
|
889
|
+
const outFile = join3(dir, "response.json");
|
|
1116
890
|
let out;
|
|
1117
891
|
try {
|
|
1118
892
|
const printed = await runner.capture([...args, outFile]);
|
|
@@ -1443,6 +1217,7 @@ function composeRuntimeSecret(inputs) {
|
|
|
1443
1217
|
...inputs.etradeProxyFunctionArn !== undefined && inputs.etradeProxyFunctionArn !== "" ? { FOUNDATION_ETRADE_PROXY_ARN: inputs.etradeProxyFunctionArn } : {},
|
|
1444
1218
|
...inputs.routineIngressFunctionArn !== undefined && inputs.routineIngressFunctionArn !== "" ? { FOUNDATION_ROUTINE_INGRESS_FUNCTION_ARN: inputs.routineIngressFunctionArn } : {},
|
|
1445
1219
|
FOUNDATION_MONGODB_SECRET_ID: names.secretMongodbReadonly,
|
|
1220
|
+
...instance.mongodbReadonly.deniedCollections.length > 0 || instance.mongodbReadonly.unsafeReadDefaultDeniedCollections.length > 0 ? { FOUNDATION_MONGODB_COLLECTION_POLICY: mongoCollectionPolicyEnv(instance.mongodbReadonly) } : {},
|
|
1446
1221
|
FOUNDATION_SLACK_TEAM_ID: slackApp.team_id !== undefined && slackApp.team_id !== "" ? slackApp.team_id : instance.slack.teamId
|
|
1447
1222
|
};
|
|
1448
1223
|
}
|
|
@@ -1453,11 +1228,11 @@ import {
|
|
|
1453
1228
|
mkdirSync as mkdirSync3,
|
|
1454
1229
|
readFileSync as readFileSync5,
|
|
1455
1230
|
realpathSync,
|
|
1456
|
-
rmSync
|
|
1231
|
+
rmSync,
|
|
1457
1232
|
statSync,
|
|
1458
|
-
writeFileSync
|
|
1233
|
+
writeFileSync
|
|
1459
1234
|
} from "node:fs";
|
|
1460
|
-
import { dirname as
|
|
1235
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, posix, relative, resolve as resolve4, sep } from "node:path";
|
|
1461
1236
|
import { isDeepStrictEqual } from "node:util";
|
|
1462
1237
|
function sourceRootFor(sourceDir) {
|
|
1463
1238
|
if (sourceDir === undefined || sourceDir === "")
|
|
@@ -1561,7 +1336,7 @@ function stageCustomization(options) {
|
|
|
1561
1336
|
const platformConfigPath = options.paths.configPath;
|
|
1562
1337
|
const stagedPath = stagedCustomizationPath(options.paths);
|
|
1563
1338
|
if (options.dryRun !== true)
|
|
1564
|
-
|
|
1339
|
+
rmSync(stagedPath, { force: true });
|
|
1565
1340
|
const customization = instance.customization;
|
|
1566
1341
|
if (customization === undefined)
|
|
1567
1342
|
return { status: "not-configured", runtimeConfigPath: platformConfigPath };
|
|
@@ -1579,8 +1354,8 @@ function stageCustomization(options) {
|
|
|
1579
1354
|
assertValidTenantJobs(tenantConfig);
|
|
1580
1355
|
assertProtectedPolicy(platformConfig, tenantConfig);
|
|
1581
1356
|
if (options.dryRun !== true) {
|
|
1582
|
-
mkdirSync3(
|
|
1583
|
-
|
|
1357
|
+
mkdirSync3(dirname3(stagedPath), { recursive: true });
|
|
1358
|
+
writeFileSync(stagedPath, yaml);
|
|
1584
1359
|
}
|
|
1585
1360
|
return {
|
|
1586
1361
|
status: "staged",
|
|
@@ -1657,10 +1432,10 @@ function cdkCommand() {
|
|
|
1657
1432
|
}
|
|
1658
1433
|
}
|
|
1659
1434
|
function cdkAppCommand(infraRoot = INFRA_ROOT) {
|
|
1660
|
-
const source =
|
|
1435
|
+
const source = join4(infraRoot, "bin", "app.ts");
|
|
1661
1436
|
if (existsSync4(source))
|
|
1662
1437
|
return `bun run ${source}`;
|
|
1663
|
-
return `${process.execPath} ${
|
|
1438
|
+
return `${process.execPath} ${join4(infraRoot, "dist", "bin", "app.js")}`;
|
|
1664
1439
|
}
|
|
1665
1440
|
async function cdkDeploy(ctx, opts) {
|
|
1666
1441
|
const context = [
|
|
@@ -1931,9 +1706,9 @@ import { createServer } from "node:http";
|
|
|
1931
1706
|
|
|
1932
1707
|
// src/deploy/github-app-manifest.ts
|
|
1933
1708
|
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1934
|
-
import { join as
|
|
1709
|
+
import { join as join5 } from "node:path";
|
|
1935
1710
|
import { parse as parseYaml } from "yaml";
|
|
1936
|
-
var GITHUB_APP_MANIFEST_PATH =
|
|
1711
|
+
var GITHUB_APP_MANIFEST_PATH = join5(PACKAGE_ASSETS, "github-app-manifest.yml");
|
|
1937
1712
|
function manifestUrl(instance) {
|
|
1938
1713
|
const repo = instance.github.defaultRepo ?? instanceNames(instance).defaultRepo;
|
|
1939
1714
|
return `https://github.com/${repo === "" ? instance.github.org : repo}`;
|
|
@@ -2148,9 +1923,9 @@ import { resolve as resolve5 } from "node:path";
|
|
|
2148
1923
|
|
|
2149
1924
|
// src/deploy/slack-manifest.ts
|
|
2150
1925
|
import { readFileSync as readFileSync8 } from "node:fs";
|
|
2151
|
-
import { join as
|
|
1926
|
+
import { join as join6 } from "node:path";
|
|
2152
1927
|
import { parse as parse2 } from "yaml";
|
|
2153
|
-
var SLACK_APP_MANIFEST_PATH =
|
|
1928
|
+
var SLACK_APP_MANIFEST_PATH = join6(PACKAGE_ASSETS, "slack-app-manifest.yml");
|
|
2154
1929
|
function buildSlackManifest(template, instance, env) {
|
|
2155
1930
|
const events = env.EVENTS_REQUEST_URL;
|
|
2156
1931
|
const commands = env.COMMANDS_REQUEST_URL ?? events?.replace(/\/events$/, "/commands");
|
|
@@ -2598,7 +2373,10 @@ var COMMANDS = [
|
|
|
2598
2373
|
"github-app-create",
|
|
2599
2374
|
"slack-manifest",
|
|
2600
2375
|
"stage-customization",
|
|
2601
|
-
"job-key"
|
|
2376
|
+
"job-key",
|
|
2377
|
+
"crm-key",
|
|
2378
|
+
"crm-check",
|
|
2379
|
+
"crm-replay"
|
|
2602
2380
|
];
|
|
2603
2381
|
var USAGE = `usage: foundation-deploy <command> --instance <path> [options]
|
|
2604
2382
|
|
|
@@ -2617,6 +2395,12 @@ commands
|
|
|
2617
2395
|
slack-manifest print the instance's Slack app manifest as JSON
|
|
2618
2396
|
stage-customization validate and stage a tenant-owned runtime config
|
|
2619
2397
|
job-key <id> print a webhook job's URL and signing key as JSON
|
|
2398
|
+
crm-key write the company CRM's crm/1 key, read from stdin, into
|
|
2399
|
+
the instance's <secretPrefix>/crm/http secret
|
|
2400
|
+
crm-check run the crm/1 conformance suite against the instance's
|
|
2401
|
+
CRM (crm.provider kind http) with its stored key
|
|
2402
|
+
crm-replay replay the agent's CRM writes (JSONL) into the
|
|
2403
|
+
instance's own table after a rollback to dynamodb
|
|
2620
2404
|
|
|
2621
2405
|
options
|
|
2622
2406
|
--instance <path> REQUIRED: path to the deployment's instance YAML
|
|
@@ -2652,6 +2436,24 @@ job-key options
|
|
|
2652
2436
|
--put-secret <id> write {url, key} into this Secrets Manager secret
|
|
2653
2437
|
(created if absent) instead of printing the key
|
|
2654
2438
|
|
|
2439
|
+
crm-key options
|
|
2440
|
+
--stdin REQUIRED: read the key from a pipe, never argv, e.g.
|
|
2441
|
+
pbpaste | foundation-deploy crm-key --instance <path> --stdin
|
|
2442
|
+
|
|
2443
|
+
crm-check options
|
|
2444
|
+
--allow-skips pass despite cases only a person can set up (they need
|
|
2445
|
+
the CRM's own harness); without it they count as failed
|
|
2446
|
+
--base-url <url> check any crm/1 server instead of the instance's: needs
|
|
2447
|
+
--tenant <id> the tenant its /meta names, and the key on stdin
|
|
2448
|
+
--key-stdin (--key-stdin). No --instance in this form.
|
|
2449
|
+
--write with --base-url only: also run the write cases, which
|
|
2450
|
+
create records and spend the week's outreach allowance;
|
|
2451
|
+
point it at a sandbox tenant
|
|
2452
|
+
|
|
2453
|
+
crm-replay options
|
|
2454
|
+
--from <file> REQUIRED: the writes to replay, one JSON object per line
|
|
2455
|
+
(docs/crm-provider-contract.md); --dry-run validates only
|
|
2456
|
+
|
|
2655
2457
|
github-app-create options
|
|
2656
2458
|
--org <org> GitHub org (default: the instance's github.org)
|
|
2657
2459
|
--port <n> local callback port (default 8765)
|
|
@@ -2731,6 +2533,21 @@ ${USAGE}`);
|
|
|
2731
2533
|
console.log(JSON.stringify(result));
|
|
2732
2534
|
return 0;
|
|
2733
2535
|
}
|
|
2536
|
+
if (command === "crm-check" && args.includes("--base-url")) {
|
|
2537
|
+
if (args.some((arg) => arg === "--instance" || arg.startsWith("--instance=")))
|
|
2538
|
+
throw new Error("crm-check takes --instance or --base-url, not both");
|
|
2539
|
+
const baseUrl = flag2(args, "--base-url");
|
|
2540
|
+
const tenantId = flag2(args, "--tenant");
|
|
2541
|
+
if (baseUrl === undefined || tenantId === undefined || !args.includes("--key-stdin"))
|
|
2542
|
+
throw new Error("crm-check --base-url needs --tenant <id> and --key-stdin");
|
|
2543
|
+
const { crmCheckTarget, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
|
|
2544
|
+
return crmCheckTarget({
|
|
2545
|
+
baseUrl,
|
|
2546
|
+
tenantId,
|
|
2547
|
+
write: args.includes("--write"),
|
|
2548
|
+
allowSkips: args.includes("--allow-skips")
|
|
2549
|
+
}, defaultCrmCommandDeps());
|
|
2550
|
+
}
|
|
2734
2551
|
if (command === "github-app-create") {
|
|
2735
2552
|
const paths = loadInstanceContext(resolveInstanceFilePath(args, process.env));
|
|
2736
2553
|
console.log(instanceBanner(paths));
|
|
@@ -2795,6 +2612,25 @@ ${USAGE}`);
|
|
|
2795
2612
|
console.log(` ${result.status}: ${result.runtimeConfigPath}`);
|
|
2796
2613
|
return 0;
|
|
2797
2614
|
}
|
|
2615
|
+
case "crm-key": {
|
|
2616
|
+
const { crmKey, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
|
|
2617
|
+
await crmKey(ctx, { stdin: args.includes("--stdin") }, defaultCrmCommandDeps());
|
|
2618
|
+
return 0;
|
|
2619
|
+
}
|
|
2620
|
+
case "crm-check": {
|
|
2621
|
+
if (args.includes("--write"))
|
|
2622
|
+
throw new Error("crm-check --write creates records and spends the week's outreach allowance, so it never runs against the instance's own tenant: use --base-url with a sandbox tenant");
|
|
2623
|
+
const { crmCheck, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
|
|
2624
|
+
return crmCheck(ctx, { allowSkips: args.includes("--allow-skips") }, defaultCrmCommandDeps());
|
|
2625
|
+
}
|
|
2626
|
+
case "crm-replay": {
|
|
2627
|
+
const from = flag2(args, "--from");
|
|
2628
|
+
if (from === undefined || from.startsWith("-"))
|
|
2629
|
+
throw new Error("crm-replay needs --from <file>: the writes to replay, as JSONL");
|
|
2630
|
+
const { crmReplay, defaultCrmCommandDeps } = await import("../chunk-1pww63wd.js");
|
|
2631
|
+
const result = await crmReplay(ctx, { from }, defaultCrmCommandDeps());
|
|
2632
|
+
return result.refused.length > 0 ? 1 : 0;
|
|
2633
|
+
}
|
|
2798
2634
|
default:
|
|
2799
2635
|
console.error(`unhandled command: ${command}
|
|
2800
2636
|
|