@deployfoundation/foundation-deploy 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 +174 -0
- package/agent-image/Dockerfile +254 -0
- package/agent-image/bin/aws +36 -0
- package/agent-image/bin/gh +193 -0
- package/agent-image/bin/git-credential-sky +89 -0
- package/agent-image/security-overlay.yml +176 -0
- package/cdk.json +6 -0
- package/dist/bin/app.js +112 -0
- package/dist/bin/foundation-deploy.js +1906 -0
- package/dist/bin/release-account.js +154 -0
- package/dist/chunk-4aye5cee.js +2416 -0
- package/dist/chunk-9ddxyvq2.js +1455 -0
- package/dist/chunk-v7tz8g50.js +428 -0
- package/dist/src/index.js +88 -0
- package/package.json +38 -0
- package/pipeline/buildspec.yml +34 -0
- package/src/artifacts.ts +318 -0
- package/src/deploy/assets/github-app-manifest.yml +29 -0
- package/src/deploy/assets/slack-app-manifest.yml +95 -0
- package/src/deploy/aws.ts +265 -0
- package/src/deploy/cli.ts +212 -0
- package/src/deploy/config-sync.ts +93 -0
- package/src/deploy/config.ts +29 -0
- package/src/deploy/deploy.ts +566 -0
- package/src/deploy/endpoint.ts +242 -0
- package/src/deploy/github-app-create.ts +154 -0
- package/src/deploy/github-app-manifest.ts +53 -0
- package/src/deploy/image.ts +80 -0
- package/src/deploy/instance.ts +87 -0
- package/src/deploy/license-cache.ts +47 -0
- package/src/deploy/license.ts +272 -0
- package/src/deploy/paths.ts +65 -0
- package/src/deploy/post-deploy.ts +97 -0
- package/src/deploy/release.ts +282 -0
- package/src/deploy/runtime-secret.ts +241 -0
- package/src/deploy/setup.ts +393 -0
- package/src/deploy/sh.ts +74 -0
- package/src/deploy/slack-manifest.ts +112 -0
- package/src/deploy/stage-customization.ts +224 -0
- package/src/deploy/tracing.ts +243 -0
- package/src/deploy-permissions.ts +165 -0
- package/src/index.ts +60 -0
- package/src/lambda-bundle-context.ts +64 -0
- package/src/names.ts +170 -0
- package/src/release/kms.ts +86 -0
- package/src/release/manifest.ts +265 -0
- package/src/stacks/agent-stack.ts +938 -0
- package/src/stacks/api-stack.ts +1005 -0
- package/src/stacks/ci-stack.ts +96 -0
- package/src/stacks/data-stack.ts +446 -0
- package/src/stacks/network-stack.ts +282 -0
- package/src/stacks/newsletter-stack.ts +572 -0
- package/src/stacks/pipeline-stack.ts +242 -0
- package/src/stacks/release-account-stack.ts +229 -0
|
@@ -0,0 +1,1906 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @bun
|
|
3
|
+
import {
|
|
4
|
+
AGENT_DOCKERFILE,
|
|
5
|
+
FOUNDATION_ROOT,
|
|
6
|
+
INFRA_ROOT,
|
|
7
|
+
PACKAGE_ASSETS,
|
|
8
|
+
argv,
|
|
9
|
+
aws,
|
|
10
|
+
awsContext,
|
|
11
|
+
awsMutate,
|
|
12
|
+
callerAccountId,
|
|
13
|
+
cdkEnv,
|
|
14
|
+
createSecretString,
|
|
15
|
+
instanceBanner,
|
|
16
|
+
loadInstanceContext,
|
|
17
|
+
putSecretJson,
|
|
18
|
+
putSecretString,
|
|
19
|
+
readSecretJson,
|
|
20
|
+
readSecretString,
|
|
21
|
+
releaseContext,
|
|
22
|
+
releaseRequest,
|
|
23
|
+
resolveInstanceFilePath,
|
|
24
|
+
resolveRelease,
|
|
25
|
+
run,
|
|
26
|
+
runCapture,
|
|
27
|
+
secretExists,
|
|
28
|
+
stackExists,
|
|
29
|
+
stackOutput,
|
|
30
|
+
toolVersion
|
|
31
|
+
} from "../chunk-v7tz8g50.js";
|
|
32
|
+
import {
|
|
33
|
+
BASE_SLACK_EVENTS,
|
|
34
|
+
CAPABILITY_IDS,
|
|
35
|
+
CUSTOMIZATION_ARTIFACT_NAME,
|
|
36
|
+
instanceNames,
|
|
37
|
+
parseInstanceConfig,
|
|
38
|
+
parseSchedule,
|
|
39
|
+
requiredGithubPermissions,
|
|
40
|
+
requiredSlackCommands,
|
|
41
|
+
requiredSlackScopes,
|
|
42
|
+
skillsKey,
|
|
43
|
+
slackCommandPrefix
|
|
44
|
+
} from "../chunk-9ddxyvq2.js";
|
|
45
|
+
|
|
46
|
+
// src/deploy/config-sync.ts
|
|
47
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync } from "node:fs";
|
|
48
|
+
import { tmpdir } from "node:os";
|
|
49
|
+
import { join, resolve } from "node:path";
|
|
50
|
+
function productSkillsDir(foundationRoot = FOUNDATION_ROOT) {
|
|
51
|
+
const dir = resolve(foundationRoot, "skills");
|
|
52
|
+
if (!existsSync(dir))
|
|
53
|
+
return;
|
|
54
|
+
const entries = readdirSync(dir).filter((name) => name !== "README.md");
|
|
55
|
+
return entries.length > 0 ? dir : undefined;
|
|
56
|
+
}
|
|
57
|
+
async function configSync(ctx, opts = {}) {
|
|
58
|
+
if (opts.runtimeConfigPath === undefined && ctx.instance.customization !== undefined)
|
|
59
|
+
throw new Error("customized instances require a freshly validated runtimeConfigPath; run through `deploy` or `config:sync`");
|
|
60
|
+
const source = opts.runtimeConfigPath ?? ctx.paths.configPath;
|
|
61
|
+
const bucket = opts.bucket ?? (ctx.dryRun === true ? `<${ctx.names.data}.BucketName>` : await stackOutput(ctx, ctx.names.data, "BucketName"));
|
|
62
|
+
await awsMutate(ctx, ["s3", "cp", source, `s3://${bucket}/${ctx.names.configKey}`]);
|
|
63
|
+
const skills = opts.release === undefined ? productSkillsDir(opts.foundationRoot) : await (opts.releaseSkillsDir ?? unpackReleaseSkills)(ctx, opts.release);
|
|
64
|
+
if (skills !== undefined)
|
|
65
|
+
await awsMutate(ctx, ["s3", "sync", `${skills}/`, `s3://${bucket}/skills/`, "--delete"]);
|
|
66
|
+
return bucket;
|
|
67
|
+
}
|
|
68
|
+
async function unpackReleaseSkills(ctx, release) {
|
|
69
|
+
const key = release.manifest?.skills.key ?? skillsKey(release.version);
|
|
70
|
+
const dir = mkdtempSync(join(tmpdir(), "foundation-skills-"));
|
|
71
|
+
const tarball = join(dir, "skills.tar.gz");
|
|
72
|
+
const contents = join(dir, "skills");
|
|
73
|
+
mkdirSync(contents, { recursive: true });
|
|
74
|
+
await awsMutate(ctx, ["s3", "cp", `s3://${release.bucket}/${key}`, tarball]);
|
|
75
|
+
await run(["tar", "-xzf", tarball, "-C", contents], { dryRun: ctx.dryRun });
|
|
76
|
+
return contents;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/deploy/deploy.ts
|
|
80
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
81
|
+
import { createRequire } from "node:module";
|
|
82
|
+
import { join as join3 } from "node:path";
|
|
83
|
+
|
|
84
|
+
// ../connectors/src/github.ts
|
|
85
|
+
var GITHUB_TOKEN_RESPONSE_MAX_BYTES = 128 * 1024;
|
|
86
|
+
// ../connectors/src/google-errors.ts
|
|
87
|
+
var MAX_ERROR_BODY_BYTES = 16 * 1024;
|
|
88
|
+
|
|
89
|
+
// ../connectors/src/google-drive.ts
|
|
90
|
+
var MAX_GOOGLE_TOKEN_RESPONSE_BYTES = 128 * 1024;
|
|
91
|
+
|
|
92
|
+
// ../connectors/src/gmail.ts
|
|
93
|
+
var GMAIL_LIST_SUMMARY_MAX_BYTES = 1024 * 1024;
|
|
94
|
+
var GMAIL_SCOPES = [
|
|
95
|
+
"https://www.googleapis.com/auth/gmail.compose",
|
|
96
|
+
"https://www.googleapis.com/auth/gmail.modify"
|
|
97
|
+
].join(" ");
|
|
98
|
+
var MAX_PART_TEXT_BYTES = 200 * 1024;
|
|
99
|
+
var MAX_GMAIL_DECODED_TEXT_BYTES = 512 * 1024;
|
|
100
|
+
var MAX_GMAIL_JSON_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
101
|
+
// ../connectors/src/google-calendar.ts
|
|
102
|
+
var GOOGLE_CALENDAR_EVENT_SCOPE = "https://www.googleapis.com/auth/calendar.events";
|
|
103
|
+
var GOOGLE_CALENDAR_FREEBUSY_SCOPE = "https://www.googleapis.com/auth/calendar.events.freebusy";
|
|
104
|
+
var GOOGLE_CALENDAR_EVENTS_SCOPE = [
|
|
105
|
+
GOOGLE_CALENDAR_EVENT_SCOPE,
|
|
106
|
+
GOOGLE_CALENDAR_FREEBUSY_SCOPE
|
|
107
|
+
].join(" ");
|
|
108
|
+
var GOOGLE_CALENDAR_JSON_MAX_BYTES = 2 * 1024 * 1024;
|
|
109
|
+
// ../connectors/src/otter.ts
|
|
110
|
+
var OTTER_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
111
|
+
// ../connectors/src/knock.ts
|
|
112
|
+
var KNOCK_REGISTER_URL = "https://mcp.knock.app/register";
|
|
113
|
+
var KNOCK_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
114
|
+
var KNOCK_MAX_REQUEST_BYTES = 256 * 1024;
|
|
115
|
+
var MAX_CLIENT_ID_CHARS = 2048;
|
|
116
|
+
var MAX_REDIRECT_URI_CHARS = 2048;
|
|
117
|
+
class KnockResponseTooLargeError extends Error {
|
|
118
|
+
constructor() {
|
|
119
|
+
super(`Knock response exceeded ${KNOCK_MAX_RESPONSE_BYTES} bytes`);
|
|
120
|
+
this.name = "KnockResponseTooLargeError";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function parseKnockOAuthClient(raw) {
|
|
124
|
+
const value = parseJsonObject(raw, "Knock OAuth client");
|
|
125
|
+
const keys = Object.keys(value);
|
|
126
|
+
if (keys.length !== 2 || !keys.every((key) => key === "client_id" || key === "redirect_uri"))
|
|
127
|
+
throw new Error("Knock OAuth client must contain only client_id and redirect_uri");
|
|
128
|
+
return {
|
|
129
|
+
client_id: requiredOpaqueString(value.client_id, "Knock OAuth client: missing client_id", MAX_CLIENT_ID_CHARS),
|
|
130
|
+
redirect_uri: validRedirectUri(value.redirect_uri, "Knock OAuth client")
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function registerKnockPublicClient(args) {
|
|
134
|
+
const redirectUri = validRedirectUri(args.redirectUri, "Knock OAuth registration");
|
|
135
|
+
let response;
|
|
136
|
+
try {
|
|
137
|
+
response = await (args.fetchImpl ?? fetch)(KNOCK_REGISTER_URL, {
|
|
138
|
+
method: "POST",
|
|
139
|
+
redirect: "error",
|
|
140
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
141
|
+
body: JSON.stringify({
|
|
142
|
+
client_name: "Foundation Knock read-only MCP",
|
|
143
|
+
redirect_uris: [redirectUri],
|
|
144
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
145
|
+
response_types: ["code"],
|
|
146
|
+
token_endpoint_auth_method: "none"
|
|
147
|
+
})
|
|
148
|
+
});
|
|
149
|
+
} catch {
|
|
150
|
+
throw new Error("Knock OAuth registration could not be completed");
|
|
151
|
+
}
|
|
152
|
+
if (!response.ok)
|
|
153
|
+
throw new Error("Knock OAuth registration was rejected");
|
|
154
|
+
const body = parseJsonObject(await readBoundedKnockResponse(response), "Knock OAuth registration");
|
|
155
|
+
return {
|
|
156
|
+
client_id: requiredOpaqueString(body.client_id, "Knock OAuth registration: missing client_id", MAX_CLIENT_ID_CHARS),
|
|
157
|
+
redirect_uri: redirectUri
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
async function readBoundedKnockResponse(response, maxBytes = KNOCK_MAX_RESPONSE_BYTES) {
|
|
161
|
+
const declaredBytes = Number(response.headers.get("content-length") ?? "");
|
|
162
|
+
if (Number.isFinite(declaredBytes) && declaredBytes > maxBytes) {
|
|
163
|
+
await response.body?.cancel().catch(() => {
|
|
164
|
+
return;
|
|
165
|
+
});
|
|
166
|
+
throw new KnockResponseTooLargeError;
|
|
167
|
+
}
|
|
168
|
+
const reader = response.body?.getReader();
|
|
169
|
+
if (reader === undefined)
|
|
170
|
+
return "";
|
|
171
|
+
const chunks = [];
|
|
172
|
+
let total = 0;
|
|
173
|
+
while (true) {
|
|
174
|
+
const { done, value } = await reader.read();
|
|
175
|
+
if (done)
|
|
176
|
+
break;
|
|
177
|
+
total += value.byteLength;
|
|
178
|
+
if (total > maxBytes) {
|
|
179
|
+
await reader.cancel().catch(() => {
|
|
180
|
+
return;
|
|
181
|
+
});
|
|
182
|
+
throw new KnockResponseTooLargeError;
|
|
183
|
+
}
|
|
184
|
+
chunks.push(value);
|
|
185
|
+
}
|
|
186
|
+
const bytes = new Uint8Array(total);
|
|
187
|
+
let offset = 0;
|
|
188
|
+
for (const chunk of chunks) {
|
|
189
|
+
bytes.set(chunk, offset);
|
|
190
|
+
offset += chunk.byteLength;
|
|
191
|
+
}
|
|
192
|
+
return new TextDecoder().decode(bytes);
|
|
193
|
+
}
|
|
194
|
+
function parseJsonObject(raw, label) {
|
|
195
|
+
let value;
|
|
196
|
+
try {
|
|
197
|
+
value = JSON.parse(raw);
|
|
198
|
+
} catch {
|
|
199
|
+
throw new Error(`${label} must be valid JSON`);
|
|
200
|
+
}
|
|
201
|
+
if (!isPlainObject(value))
|
|
202
|
+
throw new Error(`${label} must be an object`);
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
function requiredOpaqueString(value, message, max) {
|
|
206
|
+
if (!isSafeOpaqueString(value, max))
|
|
207
|
+
throw new Error(message);
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
function validRedirectUri(value, label) {
|
|
211
|
+
if (typeof value !== "string" || value.length === 0 || value.length > MAX_REDIRECT_URI_CHARS)
|
|
212
|
+
throw new Error(`${label}: missing redirect_uri`);
|
|
213
|
+
let url;
|
|
214
|
+
try {
|
|
215
|
+
url = new URL(value);
|
|
216
|
+
} catch {
|
|
217
|
+
throw new Error(`${label}: redirect_uri must be an HTTPS URL`);
|
|
218
|
+
}
|
|
219
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "" || url.hash !== "" || url.toString() !== value)
|
|
220
|
+
throw new Error(`${label}: redirect_uri must be an HTTPS URL`);
|
|
221
|
+
return value;
|
|
222
|
+
}
|
|
223
|
+
function isSafeOpaqueString(value, max) {
|
|
224
|
+
return typeof value === "string" && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
225
|
+
}
|
|
226
|
+
function isPlainObject(value) {
|
|
227
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
228
|
+
}
|
|
229
|
+
// ../connectors/src/crm.ts
|
|
230
|
+
var CRM_MAX_REQUEST_BYTES = 512 * 1024;
|
|
231
|
+
var CRM_MAX_RESPONSE_BYTES = 512 * 1024;
|
|
232
|
+
// ../connectors/src/upwork.ts
|
|
233
|
+
var UPWORK_MAX_RESPONSE_BYTES = 1024 * 1024;
|
|
234
|
+
var UPWORK_MAX_ACCESS_TOKEN_TTL_SECONDS = 24 * 60 * 60;
|
|
235
|
+
var UPWORK_GRAPHQL_DOCUMENTS = Object.freeze({
|
|
236
|
+
search_jobs: Object.freeze({
|
|
237
|
+
operationName: "SearchJobs",
|
|
238
|
+
query: `query SearchJobs($filter: MarketplaceJobPostingsSearchFilter) {
|
|
239
|
+
marketplaceJobPostingsSearch(
|
|
240
|
+
marketPlaceJobFilter: $filter
|
|
241
|
+
searchType: USER_JOBS_SEARCH
|
|
242
|
+
sortAttributes: [{ field: RECENCY }]
|
|
243
|
+
) {
|
|
244
|
+
totalCount
|
|
245
|
+
edges {
|
|
246
|
+
node {
|
|
247
|
+
id
|
|
248
|
+
title
|
|
249
|
+
description
|
|
250
|
+
ciphertext
|
|
251
|
+
amount { displayValue currency }
|
|
252
|
+
skills { name }
|
|
253
|
+
client { totalFeedback }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
pageInfo { endCursor hasNextPage }
|
|
257
|
+
}
|
|
258
|
+
}`
|
|
259
|
+
}),
|
|
260
|
+
get_job: Object.freeze({
|
|
261
|
+
operationName: "JobDetails",
|
|
262
|
+
query: `query JobDetails($id: ID!) {
|
|
263
|
+
marketplaceJobPosting(id: $id) {
|
|
264
|
+
id
|
|
265
|
+
content { title description }
|
|
266
|
+
contractTerms { contractType }
|
|
267
|
+
clientCompanyPublic { id }
|
|
268
|
+
}
|
|
269
|
+
}`
|
|
270
|
+
}),
|
|
271
|
+
list_proposals: Object.freeze({
|
|
272
|
+
operationName: "FreelancerVendorProposals",
|
|
273
|
+
query: `query FreelancerVendorProposals(
|
|
274
|
+
$filter: VendorProposalFilter!
|
|
275
|
+
$sortAttribute: VendorProposalSortAttribute!
|
|
276
|
+
$pagination: Pagination!
|
|
277
|
+
) {
|
|
278
|
+
vendorProposals(
|
|
279
|
+
filter: $filter
|
|
280
|
+
sortAttribute: $sortAttribute
|
|
281
|
+
pagination: $pagination
|
|
282
|
+
) {
|
|
283
|
+
totalCount
|
|
284
|
+
edges {
|
|
285
|
+
node {
|
|
286
|
+
id
|
|
287
|
+
status { status }
|
|
288
|
+
marketplaceJobPosting { id content { title } }
|
|
289
|
+
terms { chargeRate { displayValue } }
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
pageInfo { endCursor hasNextPage }
|
|
293
|
+
}
|
|
294
|
+
}`
|
|
295
|
+
}),
|
|
296
|
+
connects_summary: Object.freeze({
|
|
297
|
+
operationName: "ConnectsSummary",
|
|
298
|
+
query: `query ConnectsSummary {
|
|
299
|
+
connectsSummary {
|
|
300
|
+
organizationId
|
|
301
|
+
connectsBalance
|
|
302
|
+
rolloverBalance
|
|
303
|
+
connectsBalanceFree
|
|
304
|
+
connectsBalancePaid
|
|
305
|
+
}
|
|
306
|
+
}`
|
|
307
|
+
}),
|
|
308
|
+
submit_proposal: Object.freeze({
|
|
309
|
+
operationName: "createJobProposal",
|
|
310
|
+
query: `mutation createJobProposal($input: CreateJobProposalInput!) {
|
|
311
|
+
createJobProposal(input: $input) {
|
|
312
|
+
newProposalId
|
|
313
|
+
status
|
|
314
|
+
error
|
|
315
|
+
}
|
|
316
|
+
}`
|
|
317
|
+
})
|
|
318
|
+
});
|
|
319
|
+
// src/deploy/config.ts
|
|
320
|
+
import { readFileSync } from "node:fs";
|
|
321
|
+
import { parse } from "yaml";
|
|
322
|
+
function adminsCsv(yamlText, source = "runtime config") {
|
|
323
|
+
const doc = parse(yamlText);
|
|
324
|
+
const admins = doc?.admins;
|
|
325
|
+
if (!Array.isArray(admins) || admins.length === 0)
|
|
326
|
+
throw new Error(`${source}: \`admins\` must be a non-empty list of Slack user ids`);
|
|
327
|
+
return admins.map((a) => {
|
|
328
|
+
if (typeof a !== "string" || a.trim() === "")
|
|
329
|
+
throw new Error(`${source}: admins entry is not a string: ${JSON.stringify(a)}`);
|
|
330
|
+
return a.trim();
|
|
331
|
+
}).join(",");
|
|
332
|
+
}
|
|
333
|
+
function adminsCsvFromFile(path) {
|
|
334
|
+
return adminsCsv(readFileSync(path, "utf8"), path);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/deploy/endpoint.ts
|
|
338
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
339
|
+
import { tmpdir as tmpdir2 } from "node:os";
|
|
340
|
+
import { join as join2 } from "node:path";
|
|
341
|
+
function cliRunner(ctx) {
|
|
342
|
+
return {
|
|
343
|
+
capture: (args) => aws(ctx, args),
|
|
344
|
+
mutate: (args) => awsMutate(ctx, args),
|
|
345
|
+
sleep: (ms) => new Promise((resolve2) => setTimeout(resolve2, ms))
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function runtimeIdFromArn(arn) {
|
|
349
|
+
const id = arn.split("/").pop() ?? "";
|
|
350
|
+
if (id === "")
|
|
351
|
+
throw new Error(`not an agent runtime arn: ${arn}`);
|
|
352
|
+
return id;
|
|
353
|
+
}
|
|
354
|
+
async function endpointState(runner, id, name) {
|
|
355
|
+
let text;
|
|
356
|
+
try {
|
|
357
|
+
text = await runner.capture([
|
|
358
|
+
"bedrock-agentcore-control",
|
|
359
|
+
"get-agent-runtime-endpoint",
|
|
360
|
+
"--agent-runtime-id",
|
|
361
|
+
id,
|
|
362
|
+
"--endpoint-name",
|
|
363
|
+
name,
|
|
364
|
+
"--query",
|
|
365
|
+
"[status, liveVersion]",
|
|
366
|
+
"--output",
|
|
367
|
+
"text"
|
|
368
|
+
]);
|
|
369
|
+
} catch {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const [status = "", liveVersion = ""] = text.trim().split(/\s+/);
|
|
373
|
+
return { status, liveVersion };
|
|
374
|
+
}
|
|
375
|
+
async function waitForEndpoint(runner, opts) {
|
|
376
|
+
const attempts = opts.attempts ?? 60;
|
|
377
|
+
const delayMs = opts.delayMs ?? 5000;
|
|
378
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
379
|
+
for (let i = 0;i < attempts; i++) {
|
|
380
|
+
const state = await endpointState(runner, opts.id, opts.name);
|
|
381
|
+
if (state !== undefined) {
|
|
382
|
+
log(` ${opts.name} endpoint: ${state.status} (version ${state.liveVersion})`);
|
|
383
|
+
if (state.status === "READY" && state.liveVersion === opts.version)
|
|
384
|
+
return;
|
|
385
|
+
if (state.status.endsWith("_FAILED"))
|
|
386
|
+
throw new Error(`${opts.name} endpoint ${state.status}`);
|
|
387
|
+
}
|
|
388
|
+
await runner.sleep(delayMs);
|
|
389
|
+
}
|
|
390
|
+
throw new Error(`timed out waiting for the ${opts.name} endpoint to reach version ${opts.version}`);
|
|
391
|
+
}
|
|
392
|
+
async function ensureEndpoint(runner, opts) {
|
|
393
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
394
|
+
const create = [
|
|
395
|
+
"bedrock-agentcore-control",
|
|
396
|
+
"create-agent-runtime-endpoint",
|
|
397
|
+
"--agent-runtime-id",
|
|
398
|
+
opts.id,
|
|
399
|
+
"--name",
|
|
400
|
+
opts.name,
|
|
401
|
+
"--agent-runtime-version",
|
|
402
|
+
opts.version
|
|
403
|
+
];
|
|
404
|
+
if (opts.dryRun === true) {
|
|
405
|
+
await runner.mutate(create);
|
|
406
|
+
return "created";
|
|
407
|
+
}
|
|
408
|
+
const state = await endpointState(runner, opts.id, opts.name);
|
|
409
|
+
if (state !== undefined) {
|
|
410
|
+
log(` ${opts.name} endpoint already exists: ${state.status} (version ${state.liveVersion})`);
|
|
411
|
+
return "exists";
|
|
412
|
+
}
|
|
413
|
+
await runner.mutate(create);
|
|
414
|
+
await waitForEndpoint(runner, opts);
|
|
415
|
+
return "created";
|
|
416
|
+
}
|
|
417
|
+
async function promoteEndpoint(runner, opts) {
|
|
418
|
+
await runner.mutate([
|
|
419
|
+
"bedrock-agentcore-control",
|
|
420
|
+
"update-agent-runtime-endpoint",
|
|
421
|
+
"--agent-runtime-id",
|
|
422
|
+
opts.id,
|
|
423
|
+
"--endpoint-name",
|
|
424
|
+
opts.name,
|
|
425
|
+
"--agent-runtime-version",
|
|
426
|
+
opts.version
|
|
427
|
+
]);
|
|
428
|
+
if (opts.dryRun === true)
|
|
429
|
+
return;
|
|
430
|
+
await waitForEndpoint(runner, opts);
|
|
431
|
+
}
|
|
432
|
+
async function currentRuntimeVersion(runner, id) {
|
|
433
|
+
const version = await runner.capture([
|
|
434
|
+
"bedrock-agentcore-control",
|
|
435
|
+
"get-agent-runtime",
|
|
436
|
+
"--agent-runtime-id",
|
|
437
|
+
id,
|
|
438
|
+
"--query",
|
|
439
|
+
"agentRuntimeVersion",
|
|
440
|
+
"--output",
|
|
441
|
+
"text"
|
|
442
|
+
]);
|
|
443
|
+
if (version === "" || version === "None")
|
|
444
|
+
throw new Error(`runtime ${id} reports no version`);
|
|
445
|
+
return version;
|
|
446
|
+
}
|
|
447
|
+
var SMOKE_EXPECT = ['"authenticated":true', '"config_source":"s3"'];
|
|
448
|
+
function smokeSessionId(prefix) {
|
|
449
|
+
return `${prefix}-${Math.floor(Date.now() / 1000)}`.padEnd(40, "0");
|
|
450
|
+
}
|
|
451
|
+
async function smokeInvoke(runner, opts) {
|
|
452
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
453
|
+
const payload = opts.payload ?? '{"_smoke_test":true}';
|
|
454
|
+
const args = [
|
|
455
|
+
"bedrock-agentcore",
|
|
456
|
+
"invoke-agent-runtime",
|
|
457
|
+
"--agent-runtime-arn",
|
|
458
|
+
opts.arn,
|
|
459
|
+
...opts.qualifier === undefined ? [] : ["--qualifier", opts.qualifier],
|
|
460
|
+
"--runtime-session-id",
|
|
461
|
+
opts.sessionId,
|
|
462
|
+
"--payload",
|
|
463
|
+
Buffer.from(payload).toString("base64")
|
|
464
|
+
];
|
|
465
|
+
if (opts.dryRun === true) {
|
|
466
|
+
await runner.mutate([...args, "/dev/stdout"]);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const dir = await mkdtemp(join2(tmpdir2(), "foundation-smoke-"));
|
|
470
|
+
const outFile = join2(dir, "response.json");
|
|
471
|
+
let out;
|
|
472
|
+
try {
|
|
473
|
+
const printed = await runner.capture([...args, outFile]);
|
|
474
|
+
out = await readFile(outFile, "utf8").catch(() => "") || printed;
|
|
475
|
+
} finally {
|
|
476
|
+
await rm(dir, { recursive: true, force: true });
|
|
477
|
+
}
|
|
478
|
+
log(` ${out.replaceAll(`
|
|
479
|
+
`, `
|
|
480
|
+
`)}`);
|
|
481
|
+
for (const needle of opts.expect ?? SMOKE_EXPECT)
|
|
482
|
+
if (!out.includes(needle))
|
|
483
|
+
throw new Error(`smoke test: response does not contain ${needle}`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/deploy/image.ts
|
|
487
|
+
function imageTag(shortSha, dirty) {
|
|
488
|
+
const sha = shortSha.trim();
|
|
489
|
+
if (!/^[0-9a-f]{7,40}$/.test(sha))
|
|
490
|
+
throw new Error(`not a git short sha: ${JSON.stringify(sha)}`);
|
|
491
|
+
return dirty ? `${sha}-dirty` : sha;
|
|
492
|
+
}
|
|
493
|
+
function registryOf(repositoryUri) {
|
|
494
|
+
const host = repositoryUri.split("/")[0];
|
|
495
|
+
if (host === undefined || !host.includes(".dkr.ecr."))
|
|
496
|
+
throw new Error(`not an ECR repository URI: ${repositoryUri}`);
|
|
497
|
+
return host;
|
|
498
|
+
}
|
|
499
|
+
function ciCommitSha(env = process.env) {
|
|
500
|
+
const sha = env.CODEBUILD_RESOLVED_SOURCE_VERSION ?? env.GITHUB_SHA ?? "";
|
|
501
|
+
return /^[0-9a-f]{7,40}$/.test(sha) ? sha.slice(0, 7) : null;
|
|
502
|
+
}
|
|
503
|
+
async function currentImageTag(repoRoot) {
|
|
504
|
+
const ci = ciCommitSha();
|
|
505
|
+
if (ci !== null)
|
|
506
|
+
return imageTag(ci, false);
|
|
507
|
+
const sha = await runCapture(["git", "rev-parse", "--short", "HEAD"], { cwd: repoRoot });
|
|
508
|
+
const status = await runCapture(["git", "status", "--porcelain"], { cwd: repoRoot });
|
|
509
|
+
return imageTag(sha, status !== "");
|
|
510
|
+
}
|
|
511
|
+
async function ecrImageExists(ctx, opts) {
|
|
512
|
+
const args = [
|
|
513
|
+
"ecr",
|
|
514
|
+
"describe-images",
|
|
515
|
+
"--repository-name",
|
|
516
|
+
opts.repositoryName,
|
|
517
|
+
"--image-ids",
|
|
518
|
+
`imageTag=${opts.tag}`,
|
|
519
|
+
"--output",
|
|
520
|
+
"text"
|
|
521
|
+
];
|
|
522
|
+
if (ctx.dryRun === true) {
|
|
523
|
+
console.log(` $ ${argv(ctx, args).join(" ")}`);
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
await aws(ctx, args);
|
|
528
|
+
return true;
|
|
529
|
+
} catch {
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// src/deploy/license-cache.ts
|
|
535
|
+
function secretsManagerLicenseCache(ctx) {
|
|
536
|
+
const secretId = ctx.names.secretLicenseLastVerified;
|
|
537
|
+
return {
|
|
538
|
+
async read() {
|
|
539
|
+
if (ctx.dryRun === true)
|
|
540
|
+
return;
|
|
541
|
+
if (!await secretExists(ctx, secretId))
|
|
542
|
+
return;
|
|
543
|
+
const text = await readSecretString(ctx, secretId);
|
|
544
|
+
const value = JSON.parse(text);
|
|
545
|
+
return typeof value.verifiedAt === "string" ? value : undefined;
|
|
546
|
+
},
|
|
547
|
+
async write(value) {
|
|
548
|
+
const json = JSON.stringify(value);
|
|
549
|
+
if (ctx.dryRun !== true && !await secretExists(ctx, secretId)) {
|
|
550
|
+
await createSecretString(ctx, secretId, json, "Last successful Foundation license verification (no key material).");
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
await putSecretString(ctx, secretId, json);
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/deploy/license.ts
|
|
559
|
+
var DEFAULT_LICENSE_ENDPOINT = "https://license.foundry41.com/v1/verify";
|
|
560
|
+
var LICENSE_ENDPOINT_ENV = "FOUNDATION_LICENSE_ENDPOINT";
|
|
561
|
+
var GRACE_DAYS = 30;
|
|
562
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
563
|
+
function licenseEndpoint(explicit, env = process.env) {
|
|
564
|
+
return explicit ?? env[LICENSE_ENDPOINT_ENV] ?? DEFAULT_LICENSE_ENDPOINT;
|
|
565
|
+
}
|
|
566
|
+
async function verifyLicense(key, options) {
|
|
567
|
+
if (key === undefined || key === "")
|
|
568
|
+
return {
|
|
569
|
+
outcome: "skipped",
|
|
570
|
+
message: "no license.key in the instance file — skipping the Foundry 41 license check. Set license.key once your key is issued."
|
|
571
|
+
};
|
|
572
|
+
const endpoint = licenseEndpoint(options.endpoint);
|
|
573
|
+
const now = options.now ?? (() => new Date);
|
|
574
|
+
const attempts = options.attempts ?? 3;
|
|
575
|
+
const sleep = options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
|
|
576
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
577
|
+
let lastFailure = "";
|
|
578
|
+
for (let attempt = 1;attempt <= attempts; attempt++) {
|
|
579
|
+
let response;
|
|
580
|
+
try {
|
|
581
|
+
response = await fetchImpl(endpoint, {
|
|
582
|
+
method: "POST",
|
|
583
|
+
headers: { "content-type": "application/json" },
|
|
584
|
+
body: JSON.stringify({
|
|
585
|
+
key,
|
|
586
|
+
instanceId: options.instanceId,
|
|
587
|
+
version: options.version
|
|
588
|
+
}),
|
|
589
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 1e4)
|
|
590
|
+
});
|
|
591
|
+
} catch (error) {
|
|
592
|
+
lastFailure = error instanceof Error ? error.message : String(error);
|
|
593
|
+
if (attempt < attempts)
|
|
594
|
+
await sleep(2 ** (attempt - 1) * 1000);
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (response.status === 402 || response.status === 403)
|
|
598
|
+
throw new Error(`license refused for instance ${options.instanceId}: ${await failureText(response)}`);
|
|
599
|
+
if (!response.ok) {
|
|
600
|
+
lastFailure = `HTTP ${response.status}`;
|
|
601
|
+
if (attempt < attempts)
|
|
602
|
+
await sleep(2 ** (attempt - 1) * 1000);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
let body;
|
|
606
|
+
try {
|
|
607
|
+
body = await response.json();
|
|
608
|
+
} catch (error) {
|
|
609
|
+
lastFailure = `unreadable response: ${error instanceof Error ? error.message : String(error)}`;
|
|
610
|
+
if (attempt < attempts)
|
|
611
|
+
await sleep(2 ** (attempt - 1) * 1000);
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
if (body.status === "invalid")
|
|
615
|
+
throw new Error(`license refused for instance ${options.instanceId}: ${body.message ?? "the key is not valid for this instance"}`);
|
|
616
|
+
if (body.status !== "valid") {
|
|
617
|
+
lastFailure = `unexpected status "${String(body.status)}"`;
|
|
618
|
+
if (attempt < attempts)
|
|
619
|
+
await sleep(2 ** (attempt - 1) * 1000);
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
const verifiedAt = now().toISOString();
|
|
623
|
+
await cacheQuietly(options.cache, {
|
|
624
|
+
verifiedAt,
|
|
625
|
+
version: options.version,
|
|
626
|
+
...body.expiresAt === undefined ? {} : { expiresAt: body.expiresAt }
|
|
627
|
+
});
|
|
628
|
+
return {
|
|
629
|
+
outcome: "verified",
|
|
630
|
+
message: `license verified for ${options.instanceId}${body.expiresAt === undefined ? "" : ` (expires ${body.expiresAt})`}`,
|
|
631
|
+
...body.expiresAt === undefined ? {} : { expiresAt: body.expiresAt }
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
return grace(lastFailure, options, now());
|
|
635
|
+
}
|
|
636
|
+
async function grace(failure, options, now) {
|
|
637
|
+
const cached = await readQuietly(options.cache);
|
|
638
|
+
if (cached === undefined)
|
|
639
|
+
throw new Error(`the Foundation license endpoint could not be reached (${failure}) and this instance has no cached verification to fall back on. Retry, or set ${LICENSE_ENDPOINT_ENV} if you use a private endpoint.`);
|
|
640
|
+
const verifiedAt = Date.parse(cached.verifiedAt);
|
|
641
|
+
if (Number.isNaN(verifiedAt))
|
|
642
|
+
throw new Error(`the Foundation license endpoint could not be reached (${failure}) and the cached verification is unreadable (verifiedAt: ${cached.verifiedAt})`);
|
|
643
|
+
const ageDays = (now.getTime() - verifiedAt) / DAY_MS;
|
|
644
|
+
if (ageDays > GRACE_DAYS)
|
|
645
|
+
throw new Error(`the Foundation license endpoint could not be reached (${failure}) and the last verification is ${Math.floor(ageDays)} days old, past the ${GRACE_DAYS}-day grace period`);
|
|
646
|
+
const remaining = Math.max(0, Math.ceil(GRACE_DAYS - ageDays));
|
|
647
|
+
return {
|
|
648
|
+
outcome: "grace",
|
|
649
|
+
message: `the Foundation license endpoint could not be reached (${failure}); continuing on the verification cached ${Math.floor(ageDays)} days ago — ${remaining} days of grace left`,
|
|
650
|
+
...cached.expiresAt === undefined ? {} : { expiresAt: cached.expiresAt }
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
async function cacheQuietly(cache, value) {
|
|
654
|
+
if (cache === undefined)
|
|
655
|
+
return;
|
|
656
|
+
try {
|
|
657
|
+
await cache.write(value);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
console.warn(` could not cache the license verification: ${error instanceof Error ? error.message : String(error)}`);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
async function readQuietly(cache) {
|
|
663
|
+
if (cache === undefined)
|
|
664
|
+
return;
|
|
665
|
+
try {
|
|
666
|
+
return await cache.read();
|
|
667
|
+
} catch {
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
async function plannedLicenseCheck(key, options) {
|
|
672
|
+
return {
|
|
673
|
+
outcome: "skipped",
|
|
674
|
+
message: key === undefined || key === "" ? "no license.key in the instance file — nothing to verify" : `dry run — would verify ${options.instanceId} (${options.version}) against ${licenseEndpoint(options.endpoint)}`
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
async function failureText(response) {
|
|
678
|
+
try {
|
|
679
|
+
const body = await response.json();
|
|
680
|
+
return body.message ?? `HTTP ${response.status}`;
|
|
681
|
+
} catch {
|
|
682
|
+
return `HTTP ${response.status}`;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/deploy/runtime-secret.ts
|
|
687
|
+
var LIVE_ENDPOINT = "live";
|
|
688
|
+
function composeRuntimeSecret(inputs) {
|
|
689
|
+
const { instance, slackApp, tableName, bucketName } = inputs;
|
|
690
|
+
const names = instanceNames(instance);
|
|
691
|
+
for (const [key, value] of [
|
|
692
|
+
["bot_token", slackApp.bot_token],
|
|
693
|
+
["bot_user_id", slackApp.bot_user_id],
|
|
694
|
+
["table name", tableName],
|
|
695
|
+
["bucket name", bucketName]
|
|
696
|
+
]) {
|
|
697
|
+
if (typeof value !== "string" || value === "")
|
|
698
|
+
throw new Error(`runtime secret: missing ${key}`);
|
|
699
|
+
}
|
|
700
|
+
return {
|
|
701
|
+
FOUNDATION_INSTANCE: instance.name,
|
|
702
|
+
FOUNDATION_COMMAND_PREFIX: slackCommandPrefix(instance),
|
|
703
|
+
FOUNDATION_SLACK_BOT_TOKEN: slackApp.bot_token,
|
|
704
|
+
FOUNDATION_SLACK_BOT_USER_ID: slackApp.bot_user_id,
|
|
705
|
+
FOUNDATION_GITHUB_APP_SECRET_ID: names.secretGithubApp,
|
|
706
|
+
FOUNDATION_CODEX_SECRET_ID: names.secretCodex,
|
|
707
|
+
FOUNDATION_GOOGLE_AI_STUDIO_SECRET_ID: names.secretGoogleAiStudio,
|
|
708
|
+
FOUNDATION_GOOGLE_DRIVE_SECRET_ID: names.secretGoogleDrive,
|
|
709
|
+
FOUNDATION_GOOGLE_CALENDAR_SECRET_ID: names.secretGoogleCalendar,
|
|
710
|
+
FOUNDATION_GOOGLE_OAUTH_SECRET_ID: names.secretGoogleOauth,
|
|
711
|
+
FOUNDATION_TABLE_NAME: tableName,
|
|
712
|
+
...inputs.itemsTableName !== undefined && inputs.itemsTableName !== "" ? { FOUNDATION_ITEMS_TABLE_NAME: inputs.itemsTableName } : {},
|
|
713
|
+
FOUNDATION_BUCKET_NAME: bucketName,
|
|
714
|
+
...inputs.documentsBucketName !== undefined && inputs.documentsBucketName !== "" ? { FOUNDATION_DOCUMENTS_BUCKET_NAME: inputs.documentsBucketName } : {},
|
|
715
|
+
FOUNDATION_CONFIG_KEY: names.configKey,
|
|
716
|
+
FOUNDATION_DEFAULT_REPO: inputs.defaultRepo ?? names.defaultRepo,
|
|
717
|
+
FOUNDATION_GITHUB_ORG: instance.github.org,
|
|
718
|
+
...instance.github.workflowWriteRepos !== undefined && instance.github.workflowWriteRepos.length > 0 ? {
|
|
719
|
+
FOUNDATION_GITHUB_WORKFLOW_WRITE_REPOS: JSON.stringify(instance.github.workflowWriteRepos)
|
|
720
|
+
} : {},
|
|
721
|
+
FOUNDATION_RUNTIME_NAME: names.runtimeName,
|
|
722
|
+
...instance.github.appSlug !== "TBD" ? { FOUNDATION_GITHUB_APP_SLUG: instance.github.appSlug } : {},
|
|
723
|
+
...inputs.webSearchUrl !== undefined && inputs.webSearchUrl !== "" ? { FOUNDATION_WEB_SEARCH_URL: inputs.webSearchUrl } : {},
|
|
724
|
+
...inputs.invokeQueueUrl !== undefined && inputs.invokeQueueUrl !== "" ? { FOUNDATION_INVOKE_QUEUE_URL: inputs.invokeQueueUrl } : {},
|
|
725
|
+
...inputs.routineSchedulerRoleArn !== undefined && inputs.routineSchedulerRoleArn !== "" ? { FOUNDATION_ROUTINE_SCHEDULER_ROLE_ARN: inputs.routineSchedulerRoleArn } : {},
|
|
726
|
+
...inputs.agentRuntimeArn !== undefined && inputs.agentRuntimeArn !== "" ? { FOUNDATION_AGENT_RUNTIME_ARN: inputs.agentRuntimeArn } : {},
|
|
727
|
+
FOUNDATION_ROUTINE_GROUP: names.routineGroup,
|
|
728
|
+
FOUNDATION_AGENT_ENDPOINT: inputs.agentEndpoint !== undefined && inputs.agentEndpoint !== "" ? inputs.agentEndpoint : LIVE_ENDPOINT,
|
|
729
|
+
...inputs.readOnlyRoleArn !== undefined && inputs.readOnlyRoleArn !== "" ? { FOUNDATION_AWS_READONLY_ROLE_ARN: inputs.readOnlyRoleArn } : {},
|
|
730
|
+
...inputs.emailProxyFunctionArn !== undefined && inputs.emailProxyFunctionArn !== "" ? { FOUNDATION_EMAIL_PROXY_FUNCTION_ARN: inputs.emailProxyFunctionArn } : {},
|
|
731
|
+
...inputs.browserProxyFunctionArn !== undefined && inputs.browserProxyFunctionArn !== "" ? { FOUNDATION_BROWSER_PROXY_FUNCTION_ARN: inputs.browserProxyFunctionArn } : {},
|
|
732
|
+
...inputs.crmProxyFunctionArn !== undefined && inputs.crmProxyFunctionArn !== "" ? { FOUNDATION_CRM_PROXY_FUNCTION_ARN: inputs.crmProxyFunctionArn } : {},
|
|
733
|
+
...inputs.crmPolicyFingerprint !== undefined && inputs.crmPolicyFingerprint !== "" ? { FOUNDATION_CRM_POLICY_FINGERPRINT: inputs.crmPolicyFingerprint } : {},
|
|
734
|
+
...inputs.otterProxyFunctionArn !== undefined && inputs.otterProxyFunctionArn !== "" ? { FOUNDATION_OTTER_PROXY_FUNCTION_ARN: inputs.otterProxyFunctionArn } : {},
|
|
735
|
+
...inputs.knockProxyFunctionArn !== undefined && inputs.knockProxyFunctionArn !== "" ? { FOUNDATION_KNOCK_PROXY_FUNCTION_ARN: inputs.knockProxyFunctionArn } : {},
|
|
736
|
+
...inputs.upworkProxyFunctionArn !== undefined && inputs.upworkProxyFunctionArn !== "" ? { FOUNDATION_UPWORK_PROXY_ARN: inputs.upworkProxyFunctionArn } : {},
|
|
737
|
+
...inputs.routineIngressFunctionArn !== undefined && inputs.routineIngressFunctionArn !== "" ? { FOUNDATION_ROUTINE_INGRESS_FUNCTION_ARN: inputs.routineIngressFunctionArn } : {},
|
|
738
|
+
FOUNDATION_MONGODB_SECRET_ID: names.secretMongodbReadonly,
|
|
739
|
+
FOUNDATION_SLACK_TEAM_ID: slackApp.team_id !== undefined && slackApp.team_id !== "" ? slackApp.team_id : instance.slack.teamId
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// src/deploy/stage-customization.ts
|
|
744
|
+
import {
|
|
745
|
+
lstatSync,
|
|
746
|
+
mkdirSync as mkdirSync2,
|
|
747
|
+
readFileSync as readFileSync2,
|
|
748
|
+
realpathSync,
|
|
749
|
+
rmSync,
|
|
750
|
+
statSync,
|
|
751
|
+
writeFileSync
|
|
752
|
+
} from "node:fs";
|
|
753
|
+
import { dirname, isAbsolute, posix, relative, resolve as resolve2, sep } from "node:path";
|
|
754
|
+
import { isDeepStrictEqual } from "node:util";
|
|
755
|
+
function sourceRootFor(sourceDir) {
|
|
756
|
+
if (sourceDir === undefined || sourceDir === "")
|
|
757
|
+
throw new Error("Customization source is missing.");
|
|
758
|
+
let sourceStats;
|
|
759
|
+
try {
|
|
760
|
+
sourceStats = statSync(sourceDir);
|
|
761
|
+
} catch (error) {
|
|
762
|
+
if (error.code === "ENOENT")
|
|
763
|
+
throw new Error("Customization source is missing.");
|
|
764
|
+
throw error;
|
|
765
|
+
}
|
|
766
|
+
if (!sourceStats.isDirectory())
|
|
767
|
+
throw new Error("Customization source must be a directory.");
|
|
768
|
+
return realpathSync(sourceDir);
|
|
769
|
+
}
|
|
770
|
+
function isNormalizedRelativePosixPath(path) {
|
|
771
|
+
return path.length > 0 && !path.includes("\\") && !posix.isAbsolute(path) && posix.normalize(path) === path && path !== "." && path.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
772
|
+
}
|
|
773
|
+
function assertCustomizationPath(path, label = "Customization path") {
|
|
774
|
+
if (!isNormalizedRelativePosixPath(path))
|
|
775
|
+
throw new Error(`${label} must be a normalized relative POSIX path.`);
|
|
776
|
+
}
|
|
777
|
+
function isWithin(root, candidate) {
|
|
778
|
+
const pathFromRoot = relative(root, candidate);
|
|
779
|
+
return pathFromRoot !== "" && pathFromRoot !== ".." && !pathFromRoot.startsWith(`..${sep}`) && !isAbsolute(pathFromRoot);
|
|
780
|
+
}
|
|
781
|
+
function customizationFile(sourceRoot, configuredPath) {
|
|
782
|
+
assertCustomizationPath(configuredPath);
|
|
783
|
+
const candidate = resolve2(sourceRoot, ...configuredPath.split("/"));
|
|
784
|
+
if (!isWithin(sourceRoot, candidate))
|
|
785
|
+
throw new Error("Customization path escapes its source directory.");
|
|
786
|
+
return candidate;
|
|
787
|
+
}
|
|
788
|
+
function resolvedCustomizationFile(sourceRoot, configuredPath) {
|
|
789
|
+
customizationFile(sourceRoot, configuredPath);
|
|
790
|
+
const parts = configuredPath.split("/");
|
|
791
|
+
let current = sourceRoot;
|
|
792
|
+
for (const [index, part] of parts.entries()) {
|
|
793
|
+
const entry = resolve2(current, part);
|
|
794
|
+
let entryStats;
|
|
795
|
+
try {
|
|
796
|
+
entryStats = lstatSync(entry);
|
|
797
|
+
} catch (error) {
|
|
798
|
+
if (error.code === "ENOENT")
|
|
799
|
+
return;
|
|
800
|
+
throw error;
|
|
801
|
+
}
|
|
802
|
+
current = entryStats.isSymbolicLink() ? realpathSync(entry) : entry;
|
|
803
|
+
if (current !== sourceRoot && !isWithin(sourceRoot, current))
|
|
804
|
+
throw new Error("Customization file escapes its source directory.");
|
|
805
|
+
const last = index === parts.length - 1;
|
|
806
|
+
const resolvedStats = statSync(current);
|
|
807
|
+
if (!last && !resolvedStats.isDirectory())
|
|
808
|
+
throw new Error("Customization path has a non-directory parent.");
|
|
809
|
+
if (last && !resolvedStats.isFile())
|
|
810
|
+
throw new Error("Customization path must identify a regular file.");
|
|
811
|
+
}
|
|
812
|
+
return current;
|
|
813
|
+
}
|
|
814
|
+
function stagedCustomizationPath(paths) {
|
|
815
|
+
return resolve2(paths.root, STAGING_DIRECTORY, `${paths.instance.name}.yaml`);
|
|
816
|
+
}
|
|
817
|
+
var STAGING_DIRECTORY = ".foundation-staging";
|
|
818
|
+
function assertUnchanged(path, platform, tenant) {
|
|
819
|
+
if (!isDeepStrictEqual(platform, tenant))
|
|
820
|
+
throw new Error(`Customization changes platform-owned authorization or capability policy. Field: ${path}.`);
|
|
821
|
+
}
|
|
822
|
+
function protectedConfig(config) {
|
|
823
|
+
const { digestHour: _digestHour, ...todos } = config.capabilities.todos;
|
|
824
|
+
const { defined: _defined, ...routines } = config.capabilities.routines;
|
|
825
|
+
const { outreach: _outreach, ...crm2 } = config.capabilities.crm;
|
|
826
|
+
return {
|
|
827
|
+
...config,
|
|
828
|
+
capabilities: {
|
|
829
|
+
...config.capabilities,
|
|
830
|
+
todos,
|
|
831
|
+
routines,
|
|
832
|
+
crm: crm2
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
function assertProtectedPolicy(platform, tenant) {
|
|
837
|
+
assertUnchanged("config", protectedConfig(platform), protectedConfig(tenant));
|
|
838
|
+
}
|
|
839
|
+
function assertValidTenantRoutines(tenant) {
|
|
840
|
+
for (const routine of tenant.capabilities.routines.defined) {
|
|
841
|
+
const parsed = parseSchedule(routine.schedule);
|
|
842
|
+
if (!parsed.ok)
|
|
843
|
+
throw new Error(`Invalid defined routine "${routine.id}": ${parsed.reason}`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function migrationFallback(required, log, platformConfigPath) {
|
|
847
|
+
if (required)
|
|
848
|
+
throw new Error("Customization file is missing.");
|
|
849
|
+
log("Customization file is absent; retaining the platform runtime configuration during migration.");
|
|
850
|
+
return { status: "migration-fallback", runtimeConfigPath: platformConfigPath };
|
|
851
|
+
}
|
|
852
|
+
function stageCustomization(options) {
|
|
853
|
+
const instance = options.paths.instance;
|
|
854
|
+
const env = options.env ?? process.env;
|
|
855
|
+
const platformConfigPath = options.paths.configPath;
|
|
856
|
+
const stagedPath = stagedCustomizationPath(options.paths);
|
|
857
|
+
if (options.dryRun !== true)
|
|
858
|
+
rmSync(stagedPath, { force: true });
|
|
859
|
+
const customization = instance.customization;
|
|
860
|
+
if (customization === undefined)
|
|
861
|
+
return { status: "not-configured", runtimeConfigPath: platformConfigPath };
|
|
862
|
+
const log = options.log ?? console.log;
|
|
863
|
+
const sourceDir = env[`CODEBUILD_SRC_DIR_${CUSTOMIZATION_ARTIFACT_NAME}`] ?? (customization.required === false ? env.CODEBUILD_SRC_DIR : undefined);
|
|
864
|
+
const sourceRoot = sourceRootFor(sourceDir);
|
|
865
|
+
const sourceFile = resolvedCustomizationFile(sourceRoot, customization.path);
|
|
866
|
+
if (sourceFile === undefined)
|
|
867
|
+
return migrationFallback(customization.required, log, platformConfigPath);
|
|
868
|
+
const yaml = readFileSync2(sourceFile);
|
|
869
|
+
const tenantConfig = parseInstanceConfig(yaml.toString("utf8"));
|
|
870
|
+
const platformConfig = parseInstanceConfig(readFileSync2(platformConfigPath, "utf8"));
|
|
871
|
+
if (tenantConfig.name !== instance.displayName)
|
|
872
|
+
throw new Error("Customization config name must match the selected instance.");
|
|
873
|
+
assertValidTenantRoutines(tenantConfig);
|
|
874
|
+
assertProtectedPolicy(platformConfig, tenantConfig);
|
|
875
|
+
if (options.dryRun !== true) {
|
|
876
|
+
mkdirSync2(dirname(stagedPath), { recursive: true });
|
|
877
|
+
writeFileSync(stagedPath, yaml);
|
|
878
|
+
}
|
|
879
|
+
return {
|
|
880
|
+
status: "staged",
|
|
881
|
+
runtimeConfigPath: options.dryRun === true ? sourceFile : stagedPath
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// src/deploy/deploy.ts
|
|
886
|
+
function buildxCacheFlags(mode, scope) {
|
|
887
|
+
if (mode === "gha")
|
|
888
|
+
return [
|
|
889
|
+
"--cache-from",
|
|
890
|
+
`type=gha,scope=${scope}`,
|
|
891
|
+
"--cache-to",
|
|
892
|
+
`type=gha,scope=${scope},mode=max`
|
|
893
|
+
];
|
|
894
|
+
if (mode === "local")
|
|
895
|
+
return [];
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
async function buildAndPushImage(ctx, opts) {
|
|
899
|
+
const foundationRoot = opts.foundationRoot ?? FOUNDATION_ROOT;
|
|
900
|
+
const repositoryUri = opts.repositoryUri ?? await stackOutput(ctx, ctx.names.agent, "RepositoryUri");
|
|
901
|
+
const registry = registryOf(repositoryUri);
|
|
902
|
+
const image = `${repositoryUri}:${opts.tag}`;
|
|
903
|
+
const exists = await (opts.imageExists ?? ecrImageExists)(ctx, {
|
|
904
|
+
repositoryName: ctx.names.ecrRepo,
|
|
905
|
+
tag: opts.tag
|
|
906
|
+
});
|
|
907
|
+
if (exists) {
|
|
908
|
+
console.log(` image_tag_exists ${image} — skipping the build and push`);
|
|
909
|
+
return image;
|
|
910
|
+
}
|
|
911
|
+
const password = ctx.dryRun === true ? "<ecr-token>" : await aws(ctx, ["ecr", "get-login-password"]);
|
|
912
|
+
await run(["docker", "login", "--username", "AWS", "--password-stdin", registry], {
|
|
913
|
+
stdin: password,
|
|
914
|
+
dryRun: ctx.dryRun
|
|
915
|
+
});
|
|
916
|
+
const { app_id } = ctx.dryRun === true ? { app_id: `<${ctx.names.secretGithubApp}.app_id>` } : await readSecretJson(ctx, ctx.names.secretGithubApp);
|
|
917
|
+
const cacheMode = process.env.FOUNDATION_IMAGE_CACHE ?? "";
|
|
918
|
+
const buildArgs = [
|
|
919
|
+
"--platform",
|
|
920
|
+
"linux/arm64",
|
|
921
|
+
"--build-arg",
|
|
922
|
+
`FOUNDATION_GITHUB_APP_ID=${app_id}`,
|
|
923
|
+
"-f",
|
|
924
|
+
AGENT_DOCKERFILE,
|
|
925
|
+
"-t",
|
|
926
|
+
image
|
|
927
|
+
];
|
|
928
|
+
const cacheFlags = buildxCacheFlags(cacheMode, ctx.names.ecrRepo);
|
|
929
|
+
if (cacheFlags !== undefined) {
|
|
930
|
+
await run(["docker", "buildx", "build", ...buildArgs, ...cacheFlags, "--push", "."], {
|
|
931
|
+
cwd: foundationRoot,
|
|
932
|
+
dryRun: ctx.dryRun
|
|
933
|
+
});
|
|
934
|
+
} else {
|
|
935
|
+
await run(["docker", "build", ...buildArgs, "."], {
|
|
936
|
+
cwd: foundationRoot,
|
|
937
|
+
dryRun: ctx.dryRun
|
|
938
|
+
});
|
|
939
|
+
await run(["docker", "push", image], { dryRun: ctx.dryRun });
|
|
940
|
+
}
|
|
941
|
+
return image;
|
|
942
|
+
}
|
|
943
|
+
function alarmEmailFor(ctx, explicit) {
|
|
944
|
+
return explicit ?? process.env.FOUNDATION_ALARM_EMAIL ?? ctx.instance.aws.alarmEmail ?? "";
|
|
945
|
+
}
|
|
946
|
+
function cdkCommand() {
|
|
947
|
+
try {
|
|
948
|
+
return [process.execPath, createRequire(import.meta.url).resolve("aws-cdk/bin/cdk")];
|
|
949
|
+
} catch {
|
|
950
|
+
return ["bunx", "cdk"];
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function cdkAppCommand(infraRoot = INFRA_ROOT) {
|
|
954
|
+
const source = join3(infraRoot, "bin", "app.ts");
|
|
955
|
+
if (existsSync2(source))
|
|
956
|
+
return `bun run ${source}`;
|
|
957
|
+
return `${process.execPath} ${join3(infraRoot, "dist", "bin", "app.js")}`;
|
|
958
|
+
}
|
|
959
|
+
async function cdkDeploy(ctx, opts) {
|
|
960
|
+
const context = [
|
|
961
|
+
"-c",
|
|
962
|
+
`instanceFile=${ctx.paths.path}`,
|
|
963
|
+
"-c",
|
|
964
|
+
`admins=${opts.admins}`,
|
|
965
|
+
...opts.alarmEmail === "" ? [] : ["-c", `alarmEmail=${opts.alarmEmail}`],
|
|
966
|
+
...opts.phase1 === true ? ["-c", "deployRuntime=false"] : [],
|
|
967
|
+
...opts.tag === undefined || opts.release !== undefined ? [] : ["-c", `agentImageTag=${opts.tag}`],
|
|
968
|
+
...opts.release === undefined ? [] : releaseContext(opts.release)
|
|
969
|
+
];
|
|
970
|
+
await run([
|
|
971
|
+
...cdkCommand(),
|
|
972
|
+
"deploy",
|
|
973
|
+
"--all",
|
|
974
|
+
"--app",
|
|
975
|
+
cdkAppCommand(),
|
|
976
|
+
"--require-approval",
|
|
977
|
+
"never",
|
|
978
|
+
...context
|
|
979
|
+
], {
|
|
980
|
+
cwd: INFRA_ROOT,
|
|
981
|
+
env: cdkEnv(ctx),
|
|
982
|
+
dryRun: ctx.dryRun
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
async function optionalOutput(ctx, stack, key) {
|
|
986
|
+
try {
|
|
987
|
+
return await stackOutput(ctx, stack, key);
|
|
988
|
+
} catch {
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
async function syncKnockOAuthClient(ctx, options = {}) {
|
|
993
|
+
if (!ctx.instance.integrations.knock)
|
|
994
|
+
return "skipped";
|
|
995
|
+
const redirectUri = options.redirectUri ?? (ctx.dryRun === true ? `<${ctx.names.api}.KnockOAuthRedirectUrl>` : await stackOutput(ctx, ctx.names.api, "KnockOAuthRedirectUrl"));
|
|
996
|
+
if (ctx.dryRun === true) {
|
|
997
|
+
await putSecretJson(ctx, ctx.names.secretKnockOauthClient, {
|
|
998
|
+
client_id: "<knock-dynamic-client-id>",
|
|
999
|
+
redirect_uri: redirectUri
|
|
1000
|
+
});
|
|
1001
|
+
return "registered";
|
|
1002
|
+
}
|
|
1003
|
+
try {
|
|
1004
|
+
const existing = parseKnockOAuthClient(JSON.stringify(await readSecretJson(ctx, ctx.names.secretKnockOauthClient)));
|
|
1005
|
+
if (existing.redirect_uri === redirectUri)
|
|
1006
|
+
return "unchanged";
|
|
1007
|
+
} catch {}
|
|
1008
|
+
const client = await registerKnockPublicClient({
|
|
1009
|
+
redirectUri,
|
|
1010
|
+
...options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }
|
|
1011
|
+
});
|
|
1012
|
+
await putSecretJson(ctx, ctx.names.secretKnockOauthClient, {
|
|
1013
|
+
client_id: client.client_id,
|
|
1014
|
+
redirect_uri: client.redirect_uri
|
|
1015
|
+
});
|
|
1016
|
+
return "registered";
|
|
1017
|
+
}
|
|
1018
|
+
async function syncRuntimeSecret(ctx) {
|
|
1019
|
+
const dry = ctx.dryRun === true;
|
|
1020
|
+
const { data, agent, api, secretSlackApp, secretRuntime } = ctx.names;
|
|
1021
|
+
const tableName = dry ? `<${data}.TableName>` : await stackOutput(ctx, data, "TableName");
|
|
1022
|
+
const bucketName = dry ? `<${data}.BucketName>` : await stackOutput(ctx, data, "BucketName");
|
|
1023
|
+
const documentsBucketName = dry ? `<${data}.DocumentsBucketName>` : await optionalOutput(ctx, data, "DocumentsBucketName");
|
|
1024
|
+
const itemsTableName = dry ? `<${data}.ItemsTableName>` : await optionalOutput(ctx, data, "ItemsTableName");
|
|
1025
|
+
const webSearchUrl = dry ? `<${agent}.WebSearchGatewayUrl>` : await optionalOutput(ctx, agent, "WebSearchGatewayUrl");
|
|
1026
|
+
const invokeQueueUrl = dry ? `<${api}.InvokeQueueUrl>` : await optionalOutput(ctx, api, "InvokeQueueUrl");
|
|
1027
|
+
const routineSchedulerRoleArn = dry ? `<${api}.RoutineSchedulerRoleArn>` : await optionalOutput(ctx, api, "RoutineSchedulerRoleArn");
|
|
1028
|
+
const agentRuntimeArn = dry ? `<${agent}.AgentRuntimeArn>` : await optionalOutput(ctx, agent, "AgentRuntimeArn");
|
|
1029
|
+
const readOnlyRoleArn = dry ? `<${agent}.ReadOnlyRoleArn>` : await optionalOutput(ctx, agent, "ReadOnlyRoleArn");
|
|
1030
|
+
const emailProxyFunctionArn = dry ? `<${api}.EmailProxyFunctionArn>` : await optionalOutput(ctx, api, "EmailProxyFunctionArn");
|
|
1031
|
+
const browserProxyFunctionArn = dry ? `<${api}.BrowserProxyFunctionArn>` : await optionalOutput(ctx, api, "BrowserProxyFunctionArn");
|
|
1032
|
+
const crmProxyFunctionArn = ctx.instance.integrations.crm ? dry ? `<${api}.CrmProxyFunctionArn>` : await optionalOutput(ctx, api, "CrmProxyFunctionArn") : undefined;
|
|
1033
|
+
const crmPolicyFingerprint = ctx.instance.integrations.crm ? dry ? `<${api}.CrmPolicyFingerprint>` : await optionalOutput(ctx, api, "CrmPolicyFingerprint") : undefined;
|
|
1034
|
+
const otterProxyFunctionArn = dry ? `<${api}.OtterProxyFunctionArn>` : await optionalOutput(ctx, api, "OtterProxyFunctionArn");
|
|
1035
|
+
const knockProxyFunctionArn = ctx.instance.integrations.knock ? dry ? `<${api}.KnockProxyFunctionArn>` : await optionalOutput(ctx, api, "KnockProxyFunctionArn") : undefined;
|
|
1036
|
+
const upworkProxyFunctionArn = ctx.instance.integrations.upwork ? dry ? `<${api}.UpworkProxyFunctionArn>` : await optionalOutput(ctx, api, "UpworkProxyFunctionArn") : undefined;
|
|
1037
|
+
const routineIngressFunctionArn = dry ? `<${api}.RoutineIngressFunctionArn>` : await optionalOutput(ctx, api, "RoutineIngressFunctionArn");
|
|
1038
|
+
const slackApp = dry ? {
|
|
1039
|
+
bot_token: `<${secretSlackApp}.bot_token>`,
|
|
1040
|
+
bot_user_id: `<${secretSlackApp}.bot_user_id>`
|
|
1041
|
+
} : await readSecretJson(ctx, secretSlackApp);
|
|
1042
|
+
const value = composeRuntimeSecret({
|
|
1043
|
+
instance: ctx.instance,
|
|
1044
|
+
slackApp,
|
|
1045
|
+
tableName,
|
|
1046
|
+
bucketName,
|
|
1047
|
+
documentsBucketName,
|
|
1048
|
+
...itemsTableName === undefined ? {} : { itemsTableName },
|
|
1049
|
+
...webSearchUrl === undefined ? {} : { webSearchUrl },
|
|
1050
|
+
...invokeQueueUrl === undefined ? {} : { invokeQueueUrl },
|
|
1051
|
+
...routineSchedulerRoleArn === undefined ? {} : { routineSchedulerRoleArn },
|
|
1052
|
+
...agentRuntimeArn === undefined ? {} : { agentRuntimeArn },
|
|
1053
|
+
...readOnlyRoleArn === undefined ? {} : { readOnlyRoleArn },
|
|
1054
|
+
...emailProxyFunctionArn === undefined ? {} : { emailProxyFunctionArn },
|
|
1055
|
+
...browserProxyFunctionArn === undefined ? {} : { browserProxyFunctionArn },
|
|
1056
|
+
...crmProxyFunctionArn === undefined ? {} : { crmProxyFunctionArn },
|
|
1057
|
+
...crmPolicyFingerprint === undefined ? {} : { crmPolicyFingerprint },
|
|
1058
|
+
...otterProxyFunctionArn === undefined ? {} : { otterProxyFunctionArn },
|
|
1059
|
+
...knockProxyFunctionArn === undefined ? {} : { knockProxyFunctionArn },
|
|
1060
|
+
...upworkProxyFunctionArn === undefined ? {} : { upworkProxyFunctionArn },
|
|
1061
|
+
...routineIngressFunctionArn === undefined ? {} : { routineIngressFunctionArn }
|
|
1062
|
+
});
|
|
1063
|
+
await putSecretJson(ctx, secretRuntime, value);
|
|
1064
|
+
return Object.keys(value);
|
|
1065
|
+
}
|
|
1066
|
+
async function promoteLive(ctx) {
|
|
1067
|
+
const dry = ctx.dryRun === true;
|
|
1068
|
+
const runner = cliRunner(ctx);
|
|
1069
|
+
const arn = dry ? `<${ctx.names.agent}.AgentRuntimeArn>` : await stackOutput(ctx, ctx.names.agent, "AgentRuntimeArn");
|
|
1070
|
+
const id = dry ? `<${ctx.names.agent}.runtimeId>` : runtimeIdFromArn(arn);
|
|
1071
|
+
const version = dry ? "<version>" : await currentRuntimeVersion(runner, id);
|
|
1072
|
+
await smokeInvoke(runner, {
|
|
1073
|
+
arn,
|
|
1074
|
+
qualifier: "DEFAULT",
|
|
1075
|
+
sessionId: smokeSessionId("deploy-smoke"),
|
|
1076
|
+
dryRun: ctx.dryRun
|
|
1077
|
+
});
|
|
1078
|
+
await promoteEndpoint(runner, { id, name: "live", version, dryRun: ctx.dryRun });
|
|
1079
|
+
await smokeInvoke(runner, {
|
|
1080
|
+
arn,
|
|
1081
|
+
qualifier: "live",
|
|
1082
|
+
sessionId: smokeSessionId("deploy-live"),
|
|
1083
|
+
dryRun: ctx.dryRun
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
async function resolveReleaseFor(ctx, opts) {
|
|
1087
|
+
if (opts.release === undefined)
|
|
1088
|
+
return;
|
|
1089
|
+
return (opts.resolveRelease ?? resolveRelease)(ctx, opts.release);
|
|
1090
|
+
}
|
|
1091
|
+
async function checkLicense(ctx, opts, release) {
|
|
1092
|
+
const verify = opts.verifyLicense ?? (ctx.dryRun === true ? plannedLicenseCheck : verifyLicense);
|
|
1093
|
+
const result = await verify(ctx.instance.license?.key, {
|
|
1094
|
+
instanceId: ctx.instance.name,
|
|
1095
|
+
version: release?.version ?? `v${toolVersion()}`,
|
|
1096
|
+
cache: secretsManagerLicenseCache(ctx)
|
|
1097
|
+
});
|
|
1098
|
+
console.log(`▶ license: ${result.message}`);
|
|
1099
|
+
}
|
|
1100
|
+
function promotesInline(env = process.env) {
|
|
1101
|
+
return env.GITHUB_ACTIONS !== "true" && env.FOUNDATION_SKIP_POST_DEPLOY !== "1";
|
|
1102
|
+
}
|
|
1103
|
+
async function deploy(ctx, opts) {
|
|
1104
|
+
const release = await resolveReleaseFor(ctx, opts);
|
|
1105
|
+
if (release !== undefined)
|
|
1106
|
+
console.log(`▶ release ${release.version}${release.manifest === undefined ? " (dry run: not verified)" : " verified"} — ${release.manifestPath}`);
|
|
1107
|
+
await checkLicense(ctx, opts, release);
|
|
1108
|
+
const customization = stageCustomization({ paths: ctx.paths, dryRun: ctx.dryRun });
|
|
1109
|
+
if (customization.status === "staged")
|
|
1110
|
+
console.log("▶ staged reviewed runtime customization");
|
|
1111
|
+
const admins = adminsCsvFromFile(ctx.paths.configPath);
|
|
1112
|
+
const alarmEmail = alarmEmailFor(ctx, opts.alarmEmail);
|
|
1113
|
+
const phase1 = opts.phase1 === true;
|
|
1114
|
+
const tag = phase1 || release !== undefined ? undefined : opts.tag ?? await currentImageTag(FOUNDATION_ROOT);
|
|
1115
|
+
if (!phase1 && release === undefined && opts.skipImage !== true && tag !== undefined) {
|
|
1116
|
+
console.log(`▶ build + push agent image (${tag})`);
|
|
1117
|
+
console.log(` ${await buildAndPushImage(ctx, { tag })}`);
|
|
1118
|
+
}
|
|
1119
|
+
console.log(`▶ cdk deploy --all${phase1 ? " (phase 1: no runtime)" : release !== undefined ? ` (release ${release.version})` : ` (image ${tag})`}`);
|
|
1120
|
+
await cdkDeploy(ctx, {
|
|
1121
|
+
admins,
|
|
1122
|
+
alarmEmail,
|
|
1123
|
+
tag,
|
|
1124
|
+
phase1,
|
|
1125
|
+
...release === undefined ? {} : { release }
|
|
1126
|
+
});
|
|
1127
|
+
if (ctx.instance.integrations.knock) {
|
|
1128
|
+
console.log(`▶ Knock OAuth public-client registration (${ctx.names.secretKnockOauthClient})`);
|
|
1129
|
+
console.log(` ${await syncKnockOAuthClient(ctx)}`);
|
|
1130
|
+
}
|
|
1131
|
+
console.log(`▶ runtime secret refresh (${ctx.names.secretRuntime})`);
|
|
1132
|
+
console.log(` keys: ${(await syncRuntimeSecret(ctx)).join(", ")}`);
|
|
1133
|
+
console.log("▶ config sync");
|
|
1134
|
+
const bucket = await configSync(ctx, {
|
|
1135
|
+
runtimeConfigPath: customization.runtimeConfigPath,
|
|
1136
|
+
...release === undefined ? {} : { release }
|
|
1137
|
+
});
|
|
1138
|
+
console.log(`config + skills synced to s3://${bucket}/`);
|
|
1139
|
+
if (!phase1 && promotesInline()) {
|
|
1140
|
+
console.log("▶ smoke test, then promote the `live` endpoint");
|
|
1141
|
+
await promoteLive(ctx);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
// src/deploy/github-app-manifest.ts
|
|
1146
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
1147
|
+
import { join as join4 } from "node:path";
|
|
1148
|
+
import { parse as parseYaml } from "yaml";
|
|
1149
|
+
var GITHUB_APP_MANIFEST_PATH = join4(PACKAGE_ASSETS, "github-app-manifest.yml");
|
|
1150
|
+
function manifestUrl(instance) {
|
|
1151
|
+
const repo = instance.github.defaultRepo ?? instanceNames(instance).defaultRepo;
|
|
1152
|
+
return `https://github.com/${repo === "" ? instance.github.org : repo}`;
|
|
1153
|
+
}
|
|
1154
|
+
function renderAppManifest(template, instance) {
|
|
1155
|
+
const values = {
|
|
1156
|
+
displayName: instance.displayName,
|
|
1157
|
+
url: manifestUrl(instance)
|
|
1158
|
+
};
|
|
1159
|
+
return template.replace(/\$\{(\w+)\}/g, (match, key) => {
|
|
1160
|
+
const value = values[key];
|
|
1161
|
+
if (value === undefined)
|
|
1162
|
+
throw new Error(`app-manifest.yml: unknown placeholder ${match}`);
|
|
1163
|
+
return value;
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
function loadAppManifest(instance, template) {
|
|
1167
|
+
const text = template ?? readFileSync3(GITHUB_APP_MANIFEST_PATH, "utf8");
|
|
1168
|
+
const manifest = parseYaml(renderAppManifest(text, instance));
|
|
1169
|
+
if ("default_permissions" in manifest)
|
|
1170
|
+
throw new Error("app-manifest.yml: default_permissions are generated from the capability registry; remove them from the template");
|
|
1171
|
+
manifest.default_permissions = requiredGithubPermissions(["github"], {
|
|
1172
|
+
workflowWrites: (instance.github.workflowWriteRepos?.length ?? 0) > 0
|
|
1173
|
+
});
|
|
1174
|
+
return manifest;
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// src/deploy/github-app-create.ts
|
|
1178
|
+
async function githubAppCreate(options) {
|
|
1179
|
+
const instance = options.paths.instance;
|
|
1180
|
+
const names = instanceNames(instance);
|
|
1181
|
+
const org = options.org ?? instance.github.org;
|
|
1182
|
+
const port = options.port ?? 8765;
|
|
1183
|
+
const secretName = options.secret ?? names.secretGithubApp;
|
|
1184
|
+
const profile = options.profile ?? instance.aws.profile;
|
|
1185
|
+
const region = options.region ?? instance.aws.region;
|
|
1186
|
+
const manifest = loadAppManifest(instance);
|
|
1187
|
+
if (options.dryRun === true) {
|
|
1188
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
const redirectUrl = `http://localhost:${port}/callback`;
|
|
1192
|
+
const state = crypto.randomUUID();
|
|
1193
|
+
const body = JSON.stringify({ ...manifest, redirect_url: redirectUrl });
|
|
1194
|
+
async function writeSecret(value) {
|
|
1195
|
+
const run2 = async (cmd) => {
|
|
1196
|
+
const p = Bun.spawn(["aws", ...cmd, "--profile", profile, "--region", region], {
|
|
1197
|
+
stdin: "pipe",
|
|
1198
|
+
stdout: "pipe",
|
|
1199
|
+
stderr: "pipe"
|
|
1200
|
+
});
|
|
1201
|
+
p.stdin.write(value);
|
|
1202
|
+
p.stdin.end();
|
|
1203
|
+
const code = await p.exited;
|
|
1204
|
+
return { code, err: await new Response(p.stderr).text() };
|
|
1205
|
+
};
|
|
1206
|
+
let r = await run2([
|
|
1207
|
+
"secretsmanager",
|
|
1208
|
+
"create-secret",
|
|
1209
|
+
"--name",
|
|
1210
|
+
secretName,
|
|
1211
|
+
"--description",
|
|
1212
|
+
`${instance.displayName} GitHub App: app_id, installation_id, private_key`,
|
|
1213
|
+
"--secret-string",
|
|
1214
|
+
"file:///dev/stdin"
|
|
1215
|
+
]);
|
|
1216
|
+
if (r.code !== 0 && r.err.includes("ResourceExistsException"))
|
|
1217
|
+
r = await run2([
|
|
1218
|
+
"secretsmanager",
|
|
1219
|
+
"put-secret-value",
|
|
1220
|
+
"--secret-id",
|
|
1221
|
+
secretName,
|
|
1222
|
+
"--secret-string",
|
|
1223
|
+
"file:///dev/stdin"
|
|
1224
|
+
]);
|
|
1225
|
+
if (r.code !== 0)
|
|
1226
|
+
throw new Error(`aws secretsmanager failed: ${r.err.trim()}`);
|
|
1227
|
+
}
|
|
1228
|
+
const server = Bun.serve({
|
|
1229
|
+
port,
|
|
1230
|
+
async fetch(req) {
|
|
1231
|
+
const url = new URL(req.url);
|
|
1232
|
+
if (url.pathname === "/") {
|
|
1233
|
+
const html = `<!doctype html><title>Create ${instance.displayName} GitHub App</title><body style="font-family:system-ui;padding:2rem">
|
|
1234
|
+
<h2>Creating the <b>${instance.displayName}</b> GitHub App on <b>${org}</b>…</h2><p>If nothing happens, click the button.</p>
|
|
1235
|
+
<form id="f" method="post" action="https://github.com/organizations/${org}/settings/apps/new?state=${state}">
|
|
1236
|
+
<input type="hidden" name="manifest" id="m"><button>Create GitHub App</button></form>
|
|
1237
|
+
<script>document.getElementById('m').value=${JSON.stringify(body)};document.getElementById('f').submit();</script></body>`;
|
|
1238
|
+
return new Response(html, { headers: { "content-type": "text/html" } });
|
|
1239
|
+
}
|
|
1240
|
+
if (url.pathname === "/callback") {
|
|
1241
|
+
const code = url.searchParams.get("code");
|
|
1242
|
+
if (url.searchParams.get("state") !== state || code === null)
|
|
1243
|
+
return new Response("bad state/code", { status: 400 });
|
|
1244
|
+
const res = await fetch(`https://api.github.com/app-manifests/${encodeURIComponent(code)}/conversions`, {
|
|
1245
|
+
method: "POST",
|
|
1246
|
+
headers: {
|
|
1247
|
+
Accept: "application/vnd.github+json",
|
|
1248
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1249
|
+
}
|
|
1250
|
+
});
|
|
1251
|
+
if (!res.ok)
|
|
1252
|
+
return new Response(`conversion failed: HTTP ${res.status}`, { status: 502 });
|
|
1253
|
+
const app = await res.json();
|
|
1254
|
+
await writeSecret(JSON.stringify({ app_id: String(app.id), installation_id: "", private_key: app.pem }));
|
|
1255
|
+
const installUrl = `https://github.com/apps/${app.slug}/installations/new`;
|
|
1256
|
+
console.log(JSON.stringify({
|
|
1257
|
+
app_id: app.id,
|
|
1258
|
+
slug: app.slug,
|
|
1259
|
+
html_url: app.html_url,
|
|
1260
|
+
install_url: installUrl,
|
|
1261
|
+
secret: secretName
|
|
1262
|
+
}));
|
|
1263
|
+
setTimeout(() => {
|
|
1264
|
+
server.stop(true);
|
|
1265
|
+
process.exit(0);
|
|
1266
|
+
}, 500);
|
|
1267
|
+
return new Response(`<!doctype html><body style="font-family:system-ui;padding:2rem"><h2>✅ App "${app.slug}" created (id ${app.id}); private key stored in Secrets Manager ${secretName}.</h2><p>Next: <a href="${installUrl}">install it on your repos</a>.</p></body>`, { headers: { "content-type": "text/html" } });
|
|
1268
|
+
}
|
|
1269
|
+
return new Response("not found", { status: 404 });
|
|
1270
|
+
}
|
|
1271
|
+
});
|
|
1272
|
+
console.log(`Open http://localhost:${port}/ to create the app (waiting up to 10 minutes)…`);
|
|
1273
|
+
setTimeout(() => {
|
|
1274
|
+
console.error("timed out");
|
|
1275
|
+
process.exit(1);
|
|
1276
|
+
}, 10 * 60000);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// src/deploy/post-deploy.ts
|
|
1280
|
+
var LIVE = "live";
|
|
1281
|
+
var FS_PROBE_PAYLOAD = '{"_fs_probe":true}';
|
|
1282
|
+
var FS_PROBE_EXPECT = ['"writable":true'];
|
|
1283
|
+
async function postDeploy(ctx, opts = {}) {
|
|
1284
|
+
const dry = ctx.dryRun === true;
|
|
1285
|
+
const log = opts.log ?? ((line) => console.log(line));
|
|
1286
|
+
const runner = opts.runner ?? cliRunner(ctx);
|
|
1287
|
+
const arn = opts.arn ?? (dry ? `<${ctx.names.agent}.AgentRuntimeArn>` : await stackOutput(ctx, ctx.names.agent, "AgentRuntimeArn"));
|
|
1288
|
+
const id = dry && opts.arn === undefined ? `<${ctx.names.agent}.runtimeId>` : runtimeIdFromArn(arn);
|
|
1289
|
+
const version = dry ? "<version>" : await currentRuntimeVersion(runner, id);
|
|
1290
|
+
log(`deployed runtime version ${version}`);
|
|
1291
|
+
log("▶ smoke test — authenticated (new version)");
|
|
1292
|
+
await smokeInvoke(runner, {
|
|
1293
|
+
arn,
|
|
1294
|
+
qualifier: "DEFAULT",
|
|
1295
|
+
sessionId: smokeSessionId("ci-smoke"),
|
|
1296
|
+
dryRun: ctx.dryRun,
|
|
1297
|
+
log
|
|
1298
|
+
});
|
|
1299
|
+
log("▶ smoke test — persistent mount writable (new version)");
|
|
1300
|
+
await smokeInvoke(runner, {
|
|
1301
|
+
arn,
|
|
1302
|
+
qualifier: "DEFAULT",
|
|
1303
|
+
sessionId: smokeSessionId("ci-probe"),
|
|
1304
|
+
payload: FS_PROBE_PAYLOAD,
|
|
1305
|
+
expect: FS_PROBE_EXPECT,
|
|
1306
|
+
dryRun: ctx.dryRun,
|
|
1307
|
+
log
|
|
1308
|
+
});
|
|
1309
|
+
log(`▶ promote version ${version} to the ${LIVE} endpoint`);
|
|
1310
|
+
await promoteEndpoint(runner, { id, name: LIVE, version, dryRun: ctx.dryRun, log });
|
|
1311
|
+
log(`▶ smoke test — ${LIVE} endpoint answers`);
|
|
1312
|
+
await smokeInvoke(runner, {
|
|
1313
|
+
arn,
|
|
1314
|
+
qualifier: LIVE,
|
|
1315
|
+
sessionId: smokeSessionId("ci-live"),
|
|
1316
|
+
expect: ['"authenticated":true'],
|
|
1317
|
+
dryRun: ctx.dryRun,
|
|
1318
|
+
log
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/deploy/setup.ts
|
|
1323
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
1324
|
+
import { resolve as resolve3 } from "node:path";
|
|
1325
|
+
|
|
1326
|
+
// src/deploy/slack-manifest.ts
|
|
1327
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1328
|
+
import { join as join5 } from "node:path";
|
|
1329
|
+
import { parse as parse2 } from "yaml";
|
|
1330
|
+
var SLACK_APP_MANIFEST_PATH = join5(PACKAGE_ASSETS, "slack-app-manifest.yml");
|
|
1331
|
+
function buildSlackManifest(template, instance, env) {
|
|
1332
|
+
const events = env.EVENTS_REQUEST_URL;
|
|
1333
|
+
const commands = env.COMMANDS_REQUEST_URL ?? events?.replace(/\/events$/, "/commands");
|
|
1334
|
+
const interactive = env.INTERACTIVE_REQUEST_URL ?? events?.replace(/\/events$/, "/interactive");
|
|
1335
|
+
const manifest = parse2(template.replaceAll("${DISPLAY_NAME}", instance.displayName).replaceAll("${BOT_NAME}", instance.displayName.toLowerCase()).replaceAll("${COMMAND_PREFIX}", instance.commandPrefix).replaceAll("${EVENTS_REQUEST_URL}", events ?? "").replaceAll("${COMMANDS_REQUEST_URL}", commands ?? "").replaceAll("${INTERACTIVE_REQUEST_URL}", interactive ?? ""));
|
|
1336
|
+
if (manifest.oauth_config?.scopes?.bot !== undefined)
|
|
1337
|
+
throw new Error("app-manifest.yml: bot scopes are generated from the Foundation capability registry; remove them from the template");
|
|
1338
|
+
manifest.oauth_config = { scopes: { bot: requiredSlackScopes(CAPABILITY_IDS) } };
|
|
1339
|
+
const declared = new Set((manifest.features.slash_commands ?? []).map((c) => c.command));
|
|
1340
|
+
for (const command of requiredSlackCommands(CAPABILITY_IDS, instance.commandPrefix))
|
|
1341
|
+
if (!declared.has(command))
|
|
1342
|
+
throw new Error(`app-manifest.yml: missing slash command ${command} required by a capability`);
|
|
1343
|
+
if (events === undefined) {
|
|
1344
|
+
manifest.settings.event_subscriptions = undefined;
|
|
1345
|
+
} else {
|
|
1346
|
+
manifest.settings.event_subscriptions = {
|
|
1347
|
+
request_url: events,
|
|
1348
|
+
bot_events: [...BASE_SLACK_EVENTS]
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
if (commands === undefined || commands === "")
|
|
1352
|
+
manifest.features.slash_commands = undefined;
|
|
1353
|
+
if (interactive === undefined || interactive === "")
|
|
1354
|
+
manifest.settings.interactivity = undefined;
|
|
1355
|
+
return manifest;
|
|
1356
|
+
}
|
|
1357
|
+
function slackManifestFor(instance, env) {
|
|
1358
|
+
return buildSlackManifest(readFileSync4(SLACK_APP_MANIFEST_PATH, "utf8"), { displayName: instance.displayName, commandPrefix: slackCommandPrefix(instance) }, env);
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// src/deploy/tracing.ts
|
|
1362
|
+
function cliTracingRunner(ctx) {
|
|
1363
|
+
return {
|
|
1364
|
+
capture: (args) => aws(ctx, args),
|
|
1365
|
+
mutate: (args) => awsMutate(ctx, args)
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
var CLOUDWATCH_LOGS_DESTINATION = "CloudWatchLogs";
|
|
1369
|
+
var DEFAULT_INDEXING_RULE = "Default";
|
|
1370
|
+
var DEFAULT_SAMPLING_PERCENTAGE = 100;
|
|
1371
|
+
var SPANS_INGESTION_POLICY_NAME = "FoundationSpansIngestion";
|
|
1372
|
+
function spansIngestionPolicy(account, region) {
|
|
1373
|
+
const statement = (sid, group) => ({
|
|
1374
|
+
Sid: sid,
|
|
1375
|
+
Effect: "Allow",
|
|
1376
|
+
Principal: { Service: "xray.amazonaws.com" },
|
|
1377
|
+
Action: ["logs:PutLogEvents", "logs:CreateLogStream"],
|
|
1378
|
+
Resource: group.endsWith("*") ? `arn:aws:logs:${region}:${account}:log-group:${group}` : `arn:aws:logs:${region}:${account}:log-group:${group}:*`,
|
|
1379
|
+
Condition: {
|
|
1380
|
+
StringEquals: { "aws:SourceAccount": account },
|
|
1381
|
+
ArnLike: { "aws:SourceArn": `arn:aws:xray:${region}:${account}:*` }
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
return JSON.stringify({
|
|
1385
|
+
Version: "2012-10-17",
|
|
1386
|
+
Statement: [
|
|
1387
|
+
statement("SpansFromXray", "aws/spans"),
|
|
1388
|
+
statement("ApplicationSignalsFromXray", "/aws/application-signals/data"),
|
|
1389
|
+
statement("AgentCoreRuntimeSpansFromXray", "/aws/bedrock-agentcore/runtimes/*")
|
|
1390
|
+
]
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
function hasSpansIngestionPolicy(describeOutput) {
|
|
1394
|
+
let policies;
|
|
1395
|
+
try {
|
|
1396
|
+
policies = JSON.parse(describeOutput).resourcePolicies;
|
|
1397
|
+
} catch {
|
|
1398
|
+
return false;
|
|
1399
|
+
}
|
|
1400
|
+
if (!Array.isArray(policies))
|
|
1401
|
+
return false;
|
|
1402
|
+
return policies.some((p) => {
|
|
1403
|
+
const doc = String(p.policyDocument ?? "");
|
|
1404
|
+
return doc.includes("aws/spans") && doc.includes("logs:CreateLogStream") && doc.includes("/aws/bedrock-agentcore/runtimes/");
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
function parseDestination(text) {
|
|
1408
|
+
try {
|
|
1409
|
+
return String(JSON.parse(text).Destination ?? "");
|
|
1410
|
+
} catch {
|
|
1411
|
+
return "";
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
function parseSamplingPercentage(text, ruleName) {
|
|
1415
|
+
let rules;
|
|
1416
|
+
try {
|
|
1417
|
+
rules = JSON.parse(text).IndexingRules;
|
|
1418
|
+
} catch {
|
|
1419
|
+
return null;
|
|
1420
|
+
}
|
|
1421
|
+
if (!Array.isArray(rules))
|
|
1422
|
+
return null;
|
|
1423
|
+
for (const rule of rules) {
|
|
1424
|
+
if (rule.Name !== ruleName)
|
|
1425
|
+
continue;
|
|
1426
|
+
const percentage = rule.Rule?.Probabilistic?.DesiredSamplingPercentage;
|
|
1427
|
+
return typeof percentage === "number" ? percentage : null;
|
|
1428
|
+
}
|
|
1429
|
+
return null;
|
|
1430
|
+
}
|
|
1431
|
+
async function ensureTransactionSearch(runner, options = {}) {
|
|
1432
|
+
const percentage = options.samplingPercentage ?? DEFAULT_SAMPLING_PERCENTAGE;
|
|
1433
|
+
const rule = JSON.stringify({ Probabilistic: { DesiredSamplingPercentage: percentage } });
|
|
1434
|
+
if (options.dryRun === true) {
|
|
1435
|
+
if (options.account !== undefined && options.region !== undefined) {
|
|
1436
|
+
await runner.mutate([
|
|
1437
|
+
"logs",
|
|
1438
|
+
"put-resource-policy",
|
|
1439
|
+
"--policy-name",
|
|
1440
|
+
SPANS_INGESTION_POLICY_NAME,
|
|
1441
|
+
"--policy-document",
|
|
1442
|
+
spansIngestionPolicy(options.account, options.region)
|
|
1443
|
+
]);
|
|
1444
|
+
}
|
|
1445
|
+
await runner.mutate([
|
|
1446
|
+
"xray",
|
|
1447
|
+
"update-trace-segment-destination",
|
|
1448
|
+
"--destination",
|
|
1449
|
+
CLOUDWATCH_LOGS_DESTINATION
|
|
1450
|
+
]);
|
|
1451
|
+
await runner.mutate([
|
|
1452
|
+
"xray",
|
|
1453
|
+
"update-indexing-rule",
|
|
1454
|
+
"--name",
|
|
1455
|
+
DEFAULT_INDEXING_RULE,
|
|
1456
|
+
"--rule",
|
|
1457
|
+
rule
|
|
1458
|
+
]);
|
|
1459
|
+
return { destination: "updated", indexing: "updated", ingestionPolicy: "updated" };
|
|
1460
|
+
}
|
|
1461
|
+
let ingestionPolicy = "already-set";
|
|
1462
|
+
if (options.account !== undefined && options.region !== undefined) {
|
|
1463
|
+
if (!hasSpansIngestionPolicy(await runner.capture(["logs", "describe-resource-policies"]))) {
|
|
1464
|
+
await runner.mutate([
|
|
1465
|
+
"logs",
|
|
1466
|
+
"put-resource-policy",
|
|
1467
|
+
"--policy-name",
|
|
1468
|
+
SPANS_INGESTION_POLICY_NAME,
|
|
1469
|
+
"--policy-document",
|
|
1470
|
+
spansIngestionPolicy(options.account, options.region)
|
|
1471
|
+
]);
|
|
1472
|
+
ingestionPolicy = "updated";
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
let destination = "already-set";
|
|
1476
|
+
if (parseDestination(await runner.capture(["xray", "get-trace-segment-destination"])) !== CLOUDWATCH_LOGS_DESTINATION) {
|
|
1477
|
+
await runner.mutate([
|
|
1478
|
+
"xray",
|
|
1479
|
+
"update-trace-segment-destination",
|
|
1480
|
+
"--destination",
|
|
1481
|
+
CLOUDWATCH_LOGS_DESTINATION
|
|
1482
|
+
]);
|
|
1483
|
+
destination = "updated";
|
|
1484
|
+
}
|
|
1485
|
+
let indexing = "already-set";
|
|
1486
|
+
const current = parseSamplingPercentage(await runner.capture(["xray", "get-indexing-rules"]), DEFAULT_INDEXING_RULE);
|
|
1487
|
+
if (current !== percentage) {
|
|
1488
|
+
await runner.mutate([
|
|
1489
|
+
"xray",
|
|
1490
|
+
"update-indexing-rule",
|
|
1491
|
+
"--name",
|
|
1492
|
+
DEFAULT_INDEXING_RULE,
|
|
1493
|
+
"--rule",
|
|
1494
|
+
rule
|
|
1495
|
+
]);
|
|
1496
|
+
indexing = "updated";
|
|
1497
|
+
}
|
|
1498
|
+
return { destination, indexing, ingestionPolicy };
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// src/deploy/setup.ts
|
|
1502
|
+
function defaultCodexFile(instanceRoot) {
|
|
1503
|
+
return resolve3(instanceRoot, ".foundation-local", "codex.json");
|
|
1504
|
+
}
|
|
1505
|
+
function step(n, title) {
|
|
1506
|
+
console.log(`
|
|
1507
|
+
▶ ${n}. ${title}`);
|
|
1508
|
+
}
|
|
1509
|
+
async function syncSetupKnockOAuth(ctx, sync = syncKnockOAuthClient) {
|
|
1510
|
+
return sync(ctx);
|
|
1511
|
+
}
|
|
1512
|
+
function placeholder(ctx, stack, key) {
|
|
1513
|
+
if (key === "RepositoryUri")
|
|
1514
|
+
return `${ctx.instance.aws.account}.dkr.ecr.${ctx.region}.amazonaws.com/${ctx.names.ecrRepo}`;
|
|
1515
|
+
return `<${stack}.${key}>`;
|
|
1516
|
+
}
|
|
1517
|
+
async function output(ctx, stack, key) {
|
|
1518
|
+
if (ctx.dryRun !== true)
|
|
1519
|
+
return stackOutput(ctx, stack, key);
|
|
1520
|
+
try {
|
|
1521
|
+
return await stackOutput(ctx, stack, key);
|
|
1522
|
+
} catch {
|
|
1523
|
+
return placeholder(ctx, stack, key);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
async function installDependencies(ctx) {
|
|
1527
|
+
await run(["bun", "install", "--frozen-lockfile"], {
|
|
1528
|
+
cwd: FOUNDATION_ROOT,
|
|
1529
|
+
dryRun: ctx.dryRun
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
async function preflight(ctx) {
|
|
1533
|
+
await installDependencies(ctx);
|
|
1534
|
+
const expected = ctx.instance.aws.account;
|
|
1535
|
+
const account = await callerAccountId(ctx);
|
|
1536
|
+
if (account !== expected)
|
|
1537
|
+
throw new Error(`profile ${ctx.profile} is account ${account}, expected ${expected} for instance ${ctx.instance.name}`);
|
|
1538
|
+
console.log(` account ${account} via profile ${ctx.profile} (${ctx.region})`);
|
|
1539
|
+
if (ctx.dryRun === true) {
|
|
1540
|
+
await run(["docker", "info"], { dryRun: true });
|
|
1541
|
+
} else {
|
|
1542
|
+
const docker = Bun.spawn(["docker", "info"], { stdout: "ignore", stderr: "ignore" });
|
|
1543
|
+
if (await docker.exited !== 0)
|
|
1544
|
+
throw new Error("docker is not running");
|
|
1545
|
+
console.log(" docker ✓");
|
|
1546
|
+
}
|
|
1547
|
+
for (const id of [
|
|
1548
|
+
ctx.names.secretSlackSigning,
|
|
1549
|
+
ctx.names.secretSlackApp,
|
|
1550
|
+
ctx.names.secretGithubApp
|
|
1551
|
+
]) {
|
|
1552
|
+
if (!await secretExists(ctx, id))
|
|
1553
|
+
throw new Error(`secret ${id} is missing — create it before running setup`);
|
|
1554
|
+
console.log(` secret ${id} ✓`);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
async function bootstrap(ctx) {
|
|
1558
|
+
if (ctx.dryRun !== true && await stackExists(ctx, "CDKToolkit")) {
|
|
1559
|
+
console.log(" CDKToolkit already present — skipping bootstrap");
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
await run([...cdkCommand(), "bootstrap", `aws://${ctx.instance.aws.account}/${ctx.region}`], {
|
|
1563
|
+
cwd: INFRA_ROOT,
|
|
1564
|
+
env: { AWS_PROFILE: ctx.profile, AWS_REGION: ctx.region, CDK_DEFAULT_REGION: ctx.region },
|
|
1565
|
+
dryRun: ctx.dryRun
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
async function writeRuntimeSecret(ctx) {
|
|
1569
|
+
console.log(` keys: ${(await syncRuntimeSecret(ctx)).join(", ")}`);
|
|
1570
|
+
}
|
|
1571
|
+
async function seedCodex(ctx, codexFile) {
|
|
1572
|
+
if (!existsSync3(codexFile))
|
|
1573
|
+
throw new Error(`${codexFile} not found — sign in locally first, or pass --codex-file`);
|
|
1574
|
+
const document = await Bun.file(codexFile).text();
|
|
1575
|
+
JSON.parse(document);
|
|
1576
|
+
await putSecretString(ctx, ctx.names.secretCodex, document);
|
|
1577
|
+
console.log(` ${ctx.names.secretCodex} seeded from ${codexFile}`);
|
|
1578
|
+
}
|
|
1579
|
+
async function reportSlackManifest(ctx) {
|
|
1580
|
+
const { appId, teamId } = ctx.instance.slack;
|
|
1581
|
+
const manifest = slackManifestFor(ctx.instance, {
|
|
1582
|
+
EVENTS_REQUEST_URL: await output(ctx, ctx.names.api, "EventsUrl"),
|
|
1583
|
+
COMMANDS_REQUEST_URL: await output(ctx, ctx.names.api, "CommandsUrl"),
|
|
1584
|
+
INTERACTIVE_REQUEST_URL: await output(ctx, ctx.names.api, "InteractiveUrl")
|
|
1585
|
+
});
|
|
1586
|
+
console.log(` manifest: ${JSON.stringify(manifest)}`);
|
|
1587
|
+
console.log([
|
|
1588
|
+
" apply it from the instance repository, whose .slack/hooks.json runs",
|
|
1589
|
+
" `foundation-deploy slack-manifest --instance <path>`:",
|
|
1590
|
+
` slack manifest diff --app ${appId} --team ${teamId}`,
|
|
1591
|
+
` slack app install --app ${appId} --team ${teamId} --force`,
|
|
1592
|
+
" if scopes changed, re-approve in Slack when prompted"
|
|
1593
|
+
].join(`
|
|
1594
|
+
`));
|
|
1595
|
+
}
|
|
1596
|
+
async function smokeTest(ctx) {
|
|
1597
|
+
const arn = await output(ctx, ctx.names.agent, "AgentRuntimeArn");
|
|
1598
|
+
await smokeInvoke(cliRunner(ctx), {
|
|
1599
|
+
arn,
|
|
1600
|
+
sessionId: smokeSessionId("smoke"),
|
|
1601
|
+
dryRun: ctx.dryRun
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
async function ensureLiveEndpoint(ctx) {
|
|
1605
|
+
const runner = cliRunner(ctx);
|
|
1606
|
+
const arn = await output(ctx, ctx.names.agent, "AgentRuntimeArn");
|
|
1607
|
+
const id = ctx.dryRun === true ? `<${ctx.names.agent}.runtimeId>` : runtimeIdFromArn(arn);
|
|
1608
|
+
const version = ctx.dryRun === true ? "<version>" : await currentRuntimeVersion(runner, id);
|
|
1609
|
+
await ensureEndpoint(runner, { id, name: "live", version, dryRun: ctx.dryRun });
|
|
1610
|
+
}
|
|
1611
|
+
async function enableTransactionSearch(ctx) {
|
|
1612
|
+
const result = await ensureTransactionSearch(cliTracingRunner(ctx), {
|
|
1613
|
+
account: ctx.instance.aws.account,
|
|
1614
|
+
region: ctx.region,
|
|
1615
|
+
...ctx.dryRun === true ? { dryRun: true } : {}
|
|
1616
|
+
});
|
|
1617
|
+
console.log(` ingestion policy: ${result.ingestionPolicy}; destination: ${result.destination}; indexing rule: ${result.indexing}`);
|
|
1618
|
+
}
|
|
1619
|
+
async function connectionStatus(ctx, arn) {
|
|
1620
|
+
try {
|
|
1621
|
+
return await aws(ctx, [
|
|
1622
|
+
"codeconnections",
|
|
1623
|
+
"get-connection",
|
|
1624
|
+
"--connection-arn",
|
|
1625
|
+
arn,
|
|
1626
|
+
"--query",
|
|
1627
|
+
"Connection.ConnectionStatus",
|
|
1628
|
+
"--output",
|
|
1629
|
+
"text"
|
|
1630
|
+
]);
|
|
1631
|
+
} catch {
|
|
1632
|
+
return "UNKNOWN";
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
function connectionConsoleUrl(region) {
|
|
1636
|
+
return `https://${region}.console.aws.amazon.com/codesuite/settings/connections?region=${region}`;
|
|
1637
|
+
}
|
|
1638
|
+
async function deployPipelineStack(ctx) {
|
|
1639
|
+
const { deploy: deploy2 } = ctx.instance;
|
|
1640
|
+
if (deploy2.via !== "codepipeline") {
|
|
1641
|
+
console.log(" skipped — this instance is deployed by GitHub Actions");
|
|
1642
|
+
return;
|
|
1643
|
+
}
|
|
1644
|
+
const arn = deploy2.connectionArn ?? "";
|
|
1645
|
+
const status = ctx.dryRun === true ? "AVAILABLE" : await connectionStatus(ctx, arn);
|
|
1646
|
+
console.log(` connection ${arn}
|
|
1647
|
+
status: ${status}`);
|
|
1648
|
+
if (status !== "AVAILABLE") {
|
|
1649
|
+
console.log(` not deploying ${ctx.names.pipeline}: authorise the connection first, at
|
|
1650
|
+
${connectionConsoleUrl(ctx.region)}
|
|
1651
|
+
then re-run setup.`);
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
await run([
|
|
1655
|
+
...cdkCommand(),
|
|
1656
|
+
"deploy",
|
|
1657
|
+
ctx.names.pipeline,
|
|
1658
|
+
"--require-approval",
|
|
1659
|
+
"never",
|
|
1660
|
+
"-c",
|
|
1661
|
+
`instanceFile=${ctx.paths.path}`
|
|
1662
|
+
], { cwd: INFRA_ROOT, env: cdkEnv(ctx), dryRun: ctx.dryRun });
|
|
1663
|
+
}
|
|
1664
|
+
function deployRoleVariableName(instanceName) {
|
|
1665
|
+
return `${instanceName.toUpperCase().replaceAll("-", "_")}_DEPLOY_ROLE_ARN`;
|
|
1666
|
+
}
|
|
1667
|
+
async function setDeployRoleVariable(ctx) {
|
|
1668
|
+
const arn = await output(ctx, ctx.names.ci, "DeployRoleArn");
|
|
1669
|
+
const name = deployRoleVariableName(ctx.instance.name);
|
|
1670
|
+
const cmd = ["gh", "variable", "set", name, "--repo", ctx.instance.github.repo, "--body", arn];
|
|
1671
|
+
if (ctx.dryRun !== true) {
|
|
1672
|
+
try {
|
|
1673
|
+
await runCapture(["gh", "auth", "status"]);
|
|
1674
|
+
} catch {
|
|
1675
|
+
console.log(` gh is not authenticated — set it yourself: ${cmd.join(" ")}`);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
await run(cmd, { dryRun: ctx.dryRun });
|
|
1680
|
+
console.log(` ${name} = ${arn}`);
|
|
1681
|
+
}
|
|
1682
|
+
async function setup(ctx, opts = {}) {
|
|
1683
|
+
const customization = stageCustomization({ paths: ctx.paths, dryRun: ctx.dryRun });
|
|
1684
|
+
const runtimeConfigPath = customization.runtimeConfigPath;
|
|
1685
|
+
const admins = adminsCsvFromFile(ctx.paths.configPath);
|
|
1686
|
+
const alarmEmail = alarmEmailFor(ctx, opts.alarmEmail);
|
|
1687
|
+
step(1, "preflight: dependencies, credentials, secrets");
|
|
1688
|
+
await preflight(ctx);
|
|
1689
|
+
step(2, "cdk bootstrap");
|
|
1690
|
+
await bootstrap(ctx);
|
|
1691
|
+
step(3, "enable CloudWatch Transaction Search");
|
|
1692
|
+
await enableTransactionSearch(ctx);
|
|
1693
|
+
step(4, "phase 1: deploy stacks without the AgentCore runtime");
|
|
1694
|
+
await cdkDeploy(ctx, { admins, alarmEmail, phase1: true });
|
|
1695
|
+
const knockOAuth = await syncSetupKnockOAuth(ctx);
|
|
1696
|
+
if (knockOAuth !== "skipped")
|
|
1697
|
+
console.log(` Knock OAuth: ${knockOAuth}`);
|
|
1698
|
+
step(5, `write ${ctx.names.secretRuntime}`);
|
|
1699
|
+
await writeRuntimeSecret(ctx);
|
|
1700
|
+
step(6, "sync config + skills to S3");
|
|
1701
|
+
const bucket = await configSync(ctx, {
|
|
1702
|
+
bucket: await output(ctx, ctx.names.data, "BucketName"),
|
|
1703
|
+
runtimeConfigPath
|
|
1704
|
+
});
|
|
1705
|
+
console.log(` s3://${bucket}/`);
|
|
1706
|
+
step(7, "build + push the agent image");
|
|
1707
|
+
const tag = await currentImageTag(FOUNDATION_ROOT);
|
|
1708
|
+
const repositoryUri = await output(ctx, ctx.names.agent, "RepositoryUri");
|
|
1709
|
+
console.log(` ${repositoryUri}:${tag}`);
|
|
1710
|
+
await buildAndPushImage(ctx, { tag, repositoryUri });
|
|
1711
|
+
step(8, "phase 2: deploy the AgentCore runtime");
|
|
1712
|
+
await cdkDeploy(ctx, { admins, alarmEmail, tag });
|
|
1713
|
+
step(9, `refresh ${ctx.names.secretRuntime} with the runtime arn`);
|
|
1714
|
+
await writeRuntimeSecret(ctx);
|
|
1715
|
+
step(10, "create the pinned `live` endpoint");
|
|
1716
|
+
await ensureLiveEndpoint(ctx);
|
|
1717
|
+
step(11, `seed ${ctx.names.secretCodex}`);
|
|
1718
|
+
if (opts.seedCodex === true)
|
|
1719
|
+
await seedCodex(ctx, opts.codexFile ?? defaultCodexFile(ctx.paths.root));
|
|
1720
|
+
else
|
|
1721
|
+
console.log(" skipped (pass --seed-codex, or run `login` in Slack as an admin)");
|
|
1722
|
+
step(12, "tell GitHub Actions which role to assume");
|
|
1723
|
+
await setDeployRoleVariable(ctx);
|
|
1724
|
+
step(13, "deploy the instance's own pipeline, if it has one");
|
|
1725
|
+
await deployPipelineStack(ctx);
|
|
1726
|
+
step(14, "the Slack app manifest for the deployed API");
|
|
1727
|
+
await reportSlackManifest(ctx);
|
|
1728
|
+
step(15, "smoke test the runtime");
|
|
1729
|
+
await smokeTest(ctx);
|
|
1730
|
+
console.log(`
|
|
1731
|
+
Done. Now say \`@${ctx.instance.displayName} hello\` in Slack.`);
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
// src/deploy/cli.ts
|
|
1735
|
+
var COMMANDS = [
|
|
1736
|
+
"deploy",
|
|
1737
|
+
"post-deploy",
|
|
1738
|
+
"config:sync",
|
|
1739
|
+
"setup",
|
|
1740
|
+
"github-app-create",
|
|
1741
|
+
"slack-manifest",
|
|
1742
|
+
"stage-customization"
|
|
1743
|
+
];
|
|
1744
|
+
var USAGE = `usage: foundation-deploy <command> --instance <path> [options]
|
|
1745
|
+
|
|
1746
|
+
commands
|
|
1747
|
+
deploy build and push the agent image, cdk deploy --all, refresh
|
|
1748
|
+
the runtime secret, sync config
|
|
1749
|
+
post-deploy smoke DEFAULT, probe the mount, promote \`live\`, smoke \`live\`
|
|
1750
|
+
config:sync upload the runtime config and product skills to S3
|
|
1751
|
+
setup first-deploy orchestration (idempotent)
|
|
1752
|
+
github-app-create create the instance's GitHub App from the shipped manifest
|
|
1753
|
+
slack-manifest print the instance's Slack app manifest as JSON
|
|
1754
|
+
stage-customization validate and stage a tenant-owned runtime config
|
|
1755
|
+
|
|
1756
|
+
options
|
|
1757
|
+
--instance <path> REQUIRED: path to the deployment's instance YAML
|
|
1758
|
+
(or set FOUNDATION_INSTANCE_FILE)
|
|
1759
|
+
--profile <p> AWS profile (default: the instance's aws.profile; "" or
|
|
1760
|
+
FOUNDATION_NO_PROFILE=1 for the default credential chain)
|
|
1761
|
+
--region <r> AWS region (default: the instance's aws.region)
|
|
1762
|
+
--alarm-email <a> DLQ alarm subscriber (default: the instance's aws.alarmEmail)
|
|
1763
|
+
--dry-run print the commands without running them
|
|
1764
|
+
-h, --help this message
|
|
1765
|
+
|
|
1766
|
+
release options (deploy, config:sync)
|
|
1767
|
+
--release <vX.Y.Z> deploy a published Foundation release: verify its signed
|
|
1768
|
+
manifest, then deploy the artifacts it names. A copy of
|
|
1769
|
+
this tool installed from npm defaults to its OWN version;
|
|
1770
|
+
a Foundation checkout defaults to building locally.
|
|
1771
|
+
--manifest <ref> that release's manifest, as a path or an s3:// URI
|
|
1772
|
+
--release-bucket <b> where releases live (default: the Foundation bucket, or
|
|
1773
|
+
FOUNDATION_RELEASE_BUCKET)
|
|
1774
|
+
|
|
1775
|
+
deploy options
|
|
1776
|
+
--tag <tag> image tag to deploy (default: git short sha, +"-dirty");
|
|
1777
|
+
ignored in release mode, which pins the image by digest
|
|
1778
|
+
--skip-image do not build/push; deploy the stacks against --tag
|
|
1779
|
+
--phase1 bootstrap deploy: stacks only, no AgentCore runtime
|
|
1780
|
+
|
|
1781
|
+
setup options
|
|
1782
|
+
--seed-codex copy a local Codex credential into the instance's secret
|
|
1783
|
+
--codex-file <path> where that credential is (default: beside the instance
|
|
1784
|
+
file, .foundation-local/codex.json)
|
|
1785
|
+
|
|
1786
|
+
github-app-create options
|
|
1787
|
+
--org <org> GitHub org (default: the instance's github.org)
|
|
1788
|
+
--port <n> local callback port (default 8765)
|
|
1789
|
+
--secret <id> Secrets Manager id (default: the instance's github/app)`;
|
|
1790
|
+
function flag(args, name) {
|
|
1791
|
+
const index = args.indexOf(name);
|
|
1792
|
+
return index === -1 ? undefined : args[index + 1];
|
|
1793
|
+
}
|
|
1794
|
+
function contextFor(args) {
|
|
1795
|
+
const paths = loadInstanceContext(resolveInstanceFilePath(args, process.env));
|
|
1796
|
+
return awsContext({
|
|
1797
|
+
paths,
|
|
1798
|
+
profile: flag(args, "--profile"),
|
|
1799
|
+
region: flag(args, "--region"),
|
|
1800
|
+
dryRun: args.includes("--dry-run")
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
async function main(argv2 = process.argv.slice(2)) {
|
|
1804
|
+
const [command, ...args] = argv2;
|
|
1805
|
+
if (command === undefined || command === "--help" || command === "-h") {
|
|
1806
|
+
console.log(USAGE);
|
|
1807
|
+
return command === undefined ? 1 : 0;
|
|
1808
|
+
}
|
|
1809
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1810
|
+
console.log(USAGE);
|
|
1811
|
+
return 0;
|
|
1812
|
+
}
|
|
1813
|
+
if (!COMMANDS.includes(command)) {
|
|
1814
|
+
console.error(`unknown command: ${command}
|
|
1815
|
+
|
|
1816
|
+
${USAGE}`);
|
|
1817
|
+
return 1;
|
|
1818
|
+
}
|
|
1819
|
+
if (command === "slack-manifest") {
|
|
1820
|
+
const paths = loadInstanceContext(resolveInstanceFilePath(args, process.env));
|
|
1821
|
+
console.log(JSON.stringify(slackManifestFor(paths.instance, {
|
|
1822
|
+
...process.env.EVENTS_REQUEST_URL === undefined ? {} : { EVENTS_REQUEST_URL: process.env.EVENTS_REQUEST_URL },
|
|
1823
|
+
...process.env.COMMANDS_REQUEST_URL === undefined ? {} : { COMMANDS_REQUEST_URL: process.env.COMMANDS_REQUEST_URL },
|
|
1824
|
+
...process.env.INTERACTIVE_REQUEST_URL === undefined ? {} : { INTERACTIVE_REQUEST_URL: process.env.INTERACTIVE_REQUEST_URL }
|
|
1825
|
+
})));
|
|
1826
|
+
return 0;
|
|
1827
|
+
}
|
|
1828
|
+
if (command === "github-app-create") {
|
|
1829
|
+
const paths = loadInstanceContext(resolveInstanceFilePath(args, process.env));
|
|
1830
|
+
console.log(instanceBanner(paths));
|
|
1831
|
+
const port = flag(args, "--port");
|
|
1832
|
+
await githubAppCreate({
|
|
1833
|
+
paths,
|
|
1834
|
+
...flag(args, "--org") === undefined ? {} : { org: flag(args, "--org") },
|
|
1835
|
+
...port === undefined ? {} : { port: Number(port) },
|
|
1836
|
+
...flag(args, "--secret") === undefined ? {} : { secret: flag(args, "--secret") },
|
|
1837
|
+
...flag(args, "--profile") === undefined ? {} : { profile: flag(args, "--profile") },
|
|
1838
|
+
...flag(args, "--region") === undefined ? {} : { region: flag(args, "--region") },
|
|
1839
|
+
dryRun: args.includes("--dry-run")
|
|
1840
|
+
});
|
|
1841
|
+
return 0;
|
|
1842
|
+
}
|
|
1843
|
+
const ctx = contextFor(args);
|
|
1844
|
+
console.log(instanceBanner(ctx.paths));
|
|
1845
|
+
if (ctx.dryRun === true)
|
|
1846
|
+
console.log("dry run — no changes will be made");
|
|
1847
|
+
switch (command) {
|
|
1848
|
+
case "deploy": {
|
|
1849
|
+
const tag = flag(args, "--tag");
|
|
1850
|
+
const alarmEmail = flag(args, "--alarm-email");
|
|
1851
|
+
const release = releaseRequest(args);
|
|
1852
|
+
await deploy(ctx, {
|
|
1853
|
+
...tag === undefined ? {} : { tag },
|
|
1854
|
+
skipImage: args.includes("--skip-image"),
|
|
1855
|
+
phase1: args.includes("--phase1"),
|
|
1856
|
+
...alarmEmail === undefined ? {} : { alarmEmail },
|
|
1857
|
+
...release === undefined ? {} : { release }
|
|
1858
|
+
});
|
|
1859
|
+
return 0;
|
|
1860
|
+
}
|
|
1861
|
+
case "post-deploy":
|
|
1862
|
+
await postDeploy(ctx);
|
|
1863
|
+
return 0;
|
|
1864
|
+
case "config:sync": {
|
|
1865
|
+
const customization = stageCustomization({ paths: ctx.paths, dryRun: ctx.dryRun });
|
|
1866
|
+
const bucket = flag(args, "--bucket");
|
|
1867
|
+
const request = releaseRequest(args);
|
|
1868
|
+
const release = request === undefined ? undefined : await resolveRelease(ctx, request);
|
|
1869
|
+
const synced = await configSync(ctx, {
|
|
1870
|
+
...bucket === undefined ? {} : { bucket },
|
|
1871
|
+
...customization.runtimeConfigPath === undefined ? {} : { runtimeConfigPath: customization.runtimeConfigPath },
|
|
1872
|
+
...release === undefined ? {} : { release }
|
|
1873
|
+
});
|
|
1874
|
+
console.log(`config + product skills synced to s3://${synced}/`);
|
|
1875
|
+
return 0;
|
|
1876
|
+
}
|
|
1877
|
+
case "setup": {
|
|
1878
|
+
const codexFile = flag(args, "--codex-file");
|
|
1879
|
+
const alarmEmail = flag(args, "--alarm-email");
|
|
1880
|
+
await setup(ctx, {
|
|
1881
|
+
seedCodex: args.includes("--seed-codex"),
|
|
1882
|
+
...codexFile === undefined ? {} : { codexFile },
|
|
1883
|
+
...alarmEmail === undefined ? {} : { alarmEmail }
|
|
1884
|
+
});
|
|
1885
|
+
return 0;
|
|
1886
|
+
}
|
|
1887
|
+
case "stage-customization": {
|
|
1888
|
+
const result = stageCustomization({ paths: ctx.paths, dryRun: ctx.dryRun });
|
|
1889
|
+
console.log(` ${result.status}: ${result.runtimeConfigPath}`);
|
|
1890
|
+
return 0;
|
|
1891
|
+
}
|
|
1892
|
+
default:
|
|
1893
|
+
console.error(`unhandled command: ${command}
|
|
1894
|
+
|
|
1895
|
+
${USAGE}`);
|
|
1896
|
+
return 1;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
// bin/foundation-deploy.ts
|
|
1901
|
+
try {
|
|
1902
|
+
process.exit(await main());
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1905
|
+
process.exit(1);
|
|
1906
|
+
}
|