@deployfoundation/foundation-deploy 0.2.34 → 0.2.35
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 +1 -1
- package/dist/bin/app.js +2 -2
- package/dist/bin/foundation-deploy.js +72 -13
- package/dist/bin/release-account.js +2 -2
- package/dist/{chunk-k5wwqj2j.js → chunk-gmdzzrc4.js} +5 -2
- package/dist/{chunk-x093sh8z.js → chunk-hps5vshg.js} +24 -2
- package/dist/{chunk-nn1v23ar.js → chunk-zbw2awg7.js} +18 -1
- package/dist/src/index.js +2 -2
- package/package.json +1 -1
- package/src/deploy/cli.ts +2 -1
- package/src/deploy/config-sync.ts +108 -9
- package/src/deploy/deploy.ts +32 -6
- package/src/deploy/release.ts +8 -0
- package/src/deploy/setup.ts +3 -1
- package/src/stacks/agent-stack.ts +41 -2
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ bun run packages/infra/bin/foundation-deploy.ts <command> --instance <path>
|
|
|
49
49
|
| --- | --- |
|
|
50
50
|
| `deploy` | Build and push the agent image, `cdk deploy --all`, refresh the runtime secret (and `cdk deploy` once more when that changed it, so no container boots on the old one), sync config. |
|
|
51
51
|
| `post-deploy` | Smoke DEFAULT, probe the persistent mount, promote `live`, smoke `live`. |
|
|
52
|
-
| `config:sync` | Upload the runtime config and Foundation's product skills to S3. |
|
|
52
|
+
| `config:sync` | Upload the runtime config (for the deployed Foundation version, which must be this tool's) and Foundation's product skills to S3. |
|
|
53
53
|
| `setup` | First-deploy orchestration, idempotent. |
|
|
54
54
|
| `github-app-create` | Create the instance's GitHub App from the shipped manifest template. |
|
|
55
55
|
| `slack-manifest` | Print the instance's Slack app manifest as JSON (the Slack CLI's `get-manifest` hook contract). |
|
package/dist/bin/app.js
CHANGED
|
@@ -8,13 +8,13 @@ import {
|
|
|
8
8
|
FoundationPages,
|
|
9
9
|
FoundationPipeline,
|
|
10
10
|
NewsletterStack
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-zbw2awg7.js";
|
|
12
12
|
import {
|
|
13
13
|
adminsFor,
|
|
14
14
|
instanceNames,
|
|
15
15
|
loadInstanceFile,
|
|
16
16
|
provisionsIntegration
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-hps5vshg.js";
|
|
18
18
|
|
|
19
19
|
// bin/app.ts
|
|
20
20
|
import { execFileSync } from "node:child_process";
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
callerAccountId,
|
|
13
13
|
cdkEnv,
|
|
14
14
|
createSecretString,
|
|
15
|
+
foundationVersion,
|
|
15
16
|
hasFoundationWorkspace,
|
|
16
17
|
instanceBanner,
|
|
17
18
|
loadInstanceContext,
|
|
@@ -29,7 +30,7 @@ import {
|
|
|
29
30
|
stackExists,
|
|
30
31
|
stackOutput,
|
|
31
32
|
toolVersion
|
|
32
|
-
} from "../chunk-
|
|
33
|
+
} from "../chunk-gmdzzrc4.js";
|
|
33
34
|
import {
|
|
34
35
|
BASE_SLACK_EVENTS,
|
|
35
36
|
CAPABILITY_IDS,
|
|
@@ -44,8 +45,9 @@ import {
|
|
|
44
45
|
requiredSlackCommands,
|
|
45
46
|
requiredSlackScopes,
|
|
46
47
|
skillsKey,
|
|
47
|
-
slackCommandPrefix
|
|
48
|
-
|
|
48
|
+
slackCommandPrefix,
|
|
49
|
+
versionedConfigKey
|
|
50
|
+
} from "../chunk-hps5vshg.js";
|
|
49
51
|
|
|
50
52
|
// src/deploy/config-sync.ts
|
|
51
53
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync } from "node:fs";
|
|
@@ -58,12 +60,52 @@ function productSkillsDir(foundationRoot = FOUNDATION_ROOT) {
|
|
|
58
60
|
const entries = readdirSync(dir).filter((name) => name !== "README.md");
|
|
59
61
|
return entries.length > 0 ? dir : undefined;
|
|
60
62
|
}
|
|
61
|
-
async function
|
|
63
|
+
async function deployedFoundationVersion(ctx) {
|
|
64
|
+
try {
|
|
65
|
+
return await stackOutput(ctx, ctx.names.agent, "FoundationVersion");
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (error instanceof Error && error.message.includes("has no output"))
|
|
68
|
+
return;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function syncVersion(ctx, deployed, tool, release) {
|
|
73
|
+
if (deployed === undefined)
|
|
74
|
+
throw new Error(`config:sync refused: ${ctx.names.agent} records no Foundation version, so its runtime predates per-version config. Deploy ${tool} first; the deploy writes this config itself.`);
|
|
75
|
+
for (const [what, version] of [
|
|
76
|
+
["this foundation-deploy", tool],
|
|
77
|
+
["--release", release]
|
|
78
|
+
]) {
|
|
79
|
+
if (version === undefined || version === deployed)
|
|
80
|
+
continue;
|
|
81
|
+
throw new Error(`config:sync refused: ${what} is ${version}, but ${ctx.names.agent} runs Foundation ${deployed}, whose runtime may refuse a config ${version} accepts. Run config:sync with foundation-deploy@${deployed.slice(1)}, or deploy ${version} first.`);
|
|
82
|
+
}
|
|
83
|
+
return deployed;
|
|
84
|
+
}
|
|
85
|
+
async function dataBucket(ctx, bucket) {
|
|
86
|
+
if (bucket !== undefined)
|
|
87
|
+
return bucket;
|
|
88
|
+
return ctx.dryRun === true ? `<${ctx.names.data}.BucketName>` : await stackOutput(ctx, ctx.names.data, "BucketName");
|
|
89
|
+
}
|
|
90
|
+
async function uploadRuntimeConfig(ctx, opts) {
|
|
62
91
|
if (opts.runtimeConfigPath === undefined && ctx.instance.customization !== undefined)
|
|
63
92
|
throw new Error("customized instances require a freshly validated runtimeConfigPath; run through `deploy` or `config:sync`");
|
|
64
93
|
const source = opts.runtimeConfigPath ?? ctx.paths.configPath;
|
|
65
|
-
const
|
|
66
|
-
await awsMutate(ctx, ["s3", "cp", source, `s3://${bucket}/${
|
|
94
|
+
const key = versionedConfigKey(ctx.names.configKey, opts.version);
|
|
95
|
+
await awsMutate(ctx, ["s3", "cp", source, `s3://${opts.bucket}/${key}`]);
|
|
96
|
+
return key;
|
|
97
|
+
}
|
|
98
|
+
async function configSync(ctx, opts = {}) {
|
|
99
|
+
if (opts.runtimeConfigPath === undefined && ctx.instance.customization !== undefined)
|
|
100
|
+
throw new Error("customized instances require a freshly validated runtimeConfigPath; run through `deploy` or `config:sync`");
|
|
101
|
+
const tool = opts.toolVersion ?? `v${toolVersion()}`;
|
|
102
|
+
const version = opts.version ?? (ctx.dryRun === true ? tool : syncVersion(ctx, await (opts.deployedVersion ?? deployedFoundationVersion)(ctx), tool, opts.release?.version));
|
|
103
|
+
const bucket = await dataBucket(ctx, opts.bucket);
|
|
104
|
+
await uploadRuntimeConfig(ctx, {
|
|
105
|
+
bucket,
|
|
106
|
+
version,
|
|
107
|
+
...opts.runtimeConfigPath === undefined ? {} : { runtimeConfigPath: opts.runtimeConfigPath }
|
|
108
|
+
});
|
|
67
109
|
const skills = opts.release === undefined ? productSkillsDir(opts.foundationRoot) : await (opts.releaseSkillsDir ?? unpackReleaseSkills)(ctx, opts.release);
|
|
68
110
|
if (skills !== undefined)
|
|
69
111
|
await awsMutate(ctx, ["s3", "sync", `${skills}/`, `s3://${bucket}/skills/`, "--delete"]);
|
|
@@ -104,10 +146,11 @@ var MAX_GOOGLE_TOKEN_RESPONSE_BYTES = 128 * 1024;
|
|
|
104
146
|
var GMAIL_LIST_SUMMARY_MAX_BYTES = 1024 * 1024;
|
|
105
147
|
var MAX_GMAIL_ATTACHMENT_BYTES = 3.5 * 1024 * 1024;
|
|
106
148
|
var MAX_GMAIL_ATTACHMENT_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
107
|
-
var
|
|
149
|
+
var GMAIL_PERSONAL_SCOPES = [
|
|
108
150
|
"https://www.googleapis.com/auth/gmail.compose",
|
|
109
151
|
"https://www.googleapis.com/auth/gmail.modify"
|
|
110
152
|
].join(" ");
|
|
153
|
+
var GMAIL_SCOPES = `${GMAIL_PERSONAL_SCOPES} https://www.googleapis.com/auth/gmail.settings.basic`;
|
|
111
154
|
var MAX_PART_TEXT_BYTES = 200 * 1024;
|
|
112
155
|
var MAX_GMAIL_DECODED_TEXT_BYTES = 512 * 1024;
|
|
113
156
|
var MAX_GMAIL_JSON_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
@@ -1175,6 +1218,8 @@ async function cdkDeploy(ctx, opts) {
|
|
|
1175
1218
|
...opts.phase1 === true ? ["-c", "deployRuntime=false"] : [],
|
|
1176
1219
|
...opts.tag === undefined || opts.release !== undefined ? [] : ["-c", `agentImageTag=${opts.tag}`],
|
|
1177
1220
|
...opts.release === undefined ? [] : releaseContext(opts.release),
|
|
1221
|
+
"-c",
|
|
1222
|
+
`foundationVersion=${foundationVersion(opts.release)}`,
|
|
1178
1223
|
...opts.runtimeSecretDigest === undefined ? [] : ["-c", `runtimeSecretDigest=${opts.runtimeSecretDigest}`],
|
|
1179
1224
|
...await pagesDomainContext(ctx)
|
|
1180
1225
|
];
|
|
@@ -1357,7 +1402,7 @@ async function checkLicense(ctx, opts, release) {
|
|
|
1357
1402
|
const verify = opts.verifyLicense ?? (ctx.dryRun === true ? plannedLicenseCheck : verifyLicense);
|
|
1358
1403
|
const result = await verify(ctx.instance.license?.key, {
|
|
1359
1404
|
instanceId: ctx.instance.name,
|
|
1360
|
-
version: release
|
|
1405
|
+
version: foundationVersion(release),
|
|
1361
1406
|
cache: secretsManagerLicenseCache(ctx)
|
|
1362
1407
|
});
|
|
1363
1408
|
console.log(`▶ license: ${result.message}`);
|
|
@@ -1381,6 +1426,17 @@ async function deploy(ctx, opts) {
|
|
|
1381
1426
|
console.log(`▶ build + push agent image (${tag})`);
|
|
1382
1427
|
console.log(` ${await buildAndPushImage(ctx, { tag })}`);
|
|
1383
1428
|
}
|
|
1429
|
+
const version = foundationVersion(release);
|
|
1430
|
+
const bucket = phase1 ? undefined : ctx.dryRun === true ? `<${ctx.names.data}.BucketName>` : await optionalOutput(ctx, ctx.names.data, "BucketName");
|
|
1431
|
+
if (bucket !== undefined) {
|
|
1432
|
+
console.log(`▶ config for ${version}, ahead of the runtime version that reads it`);
|
|
1433
|
+
const key = await uploadRuntimeConfig(ctx, {
|
|
1434
|
+
bucket,
|
|
1435
|
+
version,
|
|
1436
|
+
runtimeConfigPath: customization.runtimeConfigPath
|
|
1437
|
+
});
|
|
1438
|
+
console.log(` s3://${bucket}/${key}`);
|
|
1439
|
+
}
|
|
1384
1440
|
console.log(`▶ cdk deploy --all${phase1 ? " (phase 1: no runtime)" : release !== undefined ? ` (release ${release.version})` : ` (image ${tag})`}`);
|
|
1385
1441
|
const deployed = await currentRuntimeSecretDigest(ctx);
|
|
1386
1442
|
const deployStacks = (runtimeSecretDigest2) => cdkDeploy(ctx, {
|
|
@@ -1403,12 +1459,13 @@ async function deploy(ctx, opts) {
|
|
|
1403
1459
|
digest: async () => phase1 ? undefined : currentRuntimeSecretDigest(ctx),
|
|
1404
1460
|
redeploy: (digest) => deployStacks(digest)
|
|
1405
1461
|
});
|
|
1406
|
-
console.log(
|
|
1407
|
-
const
|
|
1462
|
+
console.log(`▶ config sync (${version})`);
|
|
1463
|
+
const synced = await configSync(ctx, {
|
|
1464
|
+
version,
|
|
1408
1465
|
runtimeConfigPath: customization.runtimeConfigPath,
|
|
1409
1466
|
...release === undefined ? {} : { release }
|
|
1410
1467
|
});
|
|
1411
|
-
console.log(`config + skills synced to s3://${
|
|
1468
|
+
console.log(`config + skills synced to s3://${synced}/`);
|
|
1412
1469
|
if (!phase1 && promotesInline()) {
|
|
1413
1470
|
console.log("▶ smoke test, then promote the `live` endpoint");
|
|
1414
1471
|
await promoteLive(ctx);
|
|
@@ -2033,7 +2090,8 @@ async function setup(ctx, opts = {}) {
|
|
|
2033
2090
|
step(6, "sync config + skills to S3");
|
|
2034
2091
|
const bucket = await configSync(ctx, {
|
|
2035
2092
|
bucket: await output(ctx, ctx.names.data, "BucketName"),
|
|
2036
|
-
runtimeConfigPath
|
|
2093
|
+
runtimeConfigPath,
|
|
2094
|
+
version: foundationVersion()
|
|
2037
2095
|
});
|
|
2038
2096
|
console.log(` s3://${bucket}/`);
|
|
2039
2097
|
step(7, "build + push the agent image");
|
|
@@ -2093,7 +2151,8 @@ commands
|
|
|
2093
2151
|
deploy build and push the agent image, cdk deploy --all, refresh
|
|
2094
2152
|
the runtime secret, sync config
|
|
2095
2153
|
post-deploy smoke DEFAULT, probe the mount, promote \`live\`, smoke \`live\`
|
|
2096
|
-
config:sync upload the runtime config and
|
|
2154
|
+
config:sync upload the deployed version's runtime config and the
|
|
2155
|
+
product skills to S3
|
|
2097
2156
|
setup first-deploy orchestration (idempotent)
|
|
2098
2157
|
github-app-create create the instance's GitHub App from the shipped manifest
|
|
2099
2158
|
slack-manifest print the instance's Slack app manifest as JSON
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
manifestKey,
|
|
7
7
|
releaseCacheDir,
|
|
8
8
|
verifyManifest
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-hps5vshg.js";
|
|
10
10
|
|
|
11
11
|
// src/deploy/release.ts
|
|
12
12
|
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2 } from "node:fs";
|
|
@@ -335,6 +335,9 @@ function toolVersion(infraRoot = INFRA_ROOT) {
|
|
|
335
335
|
const { version } = JSON.parse(readFileSync2(join2(infraRoot, "package.json"), "utf8"));
|
|
336
336
|
return version;
|
|
337
337
|
}
|
|
338
|
+
function foundationVersion(release) {
|
|
339
|
+
return release?.version ?? `v${toolVersion()}`;
|
|
340
|
+
}
|
|
338
341
|
function hasFoundationWorkspace(foundationRoot = FOUNDATION_ROOT) {
|
|
339
342
|
return existsSync2(join2(foundationRoot, "packages", "gateway", "package.json"));
|
|
340
343
|
}
|
|
@@ -436,4 +439,4 @@ function releaseContext(release) {
|
|
|
436
439
|
];
|
|
437
440
|
}
|
|
438
441
|
|
|
439
|
-
export { resolveInstanceFilePath, loadInstanceContext, instanceBanner, run, runCapture, awsContext, argv, cdkEnv, aws, awsMutate, stackOutput, stackExists, secretExists, readSecretString, readSecretJson, putSecretString, createSecretString, putSecretJson, callerAccountId, INFRA_ROOT, FOUNDATION_ROOT, AGENT_DOCKERFILE, PACKAGE_ASSETS, DEFAULT_RELEASE_BUCKET, toolVersion, hasFoundationWorkspace, releaseRequest, resolveRelease, releaseContext };
|
|
442
|
+
export { resolveInstanceFilePath, loadInstanceContext, instanceBanner, run, runCapture, awsContext, argv, cdkEnv, aws, awsMutate, stackOutput, stackExists, secretExists, readSecretString, readSecretJson, putSecretString, createSecretString, putSecretJson, callerAccountId, INFRA_ROOT, FOUNDATION_ROOT, AGENT_DOCKERFILE, PACKAGE_ASSETS, DEFAULT_RELEASE_BUCKET, toolVersion, foundationVersion, hasFoundationWorkspace, releaseRequest, resolveRelease, releaseContext };
|
|
@@ -504,6 +504,7 @@ var RawEmailIdentity = z.object({
|
|
|
504
504
|
channel: z.string().regex(/^[CGD][A-Z0-9]{8,}$/),
|
|
505
505
|
owner: z.string().regex(/^U[A-Z0-9]{8,}$/, "owner must be a Slack U id").optional(),
|
|
506
506
|
signature: z.string().min(1).max(2000),
|
|
507
|
+
organization: z.boolean().optional(),
|
|
507
508
|
sendAsAddresses: z.array(z.string().regex(/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i)).max(20).default([]).refine((addresses) => new Set(addresses.map((address) => address.toLowerCase())).size === addresses.length, "duplicate send-as address")
|
|
508
509
|
}).strict().superRefine((identity, ctx) => {
|
|
509
510
|
if (identity.channel.startsWith("D") && identity.owner === undefined) {
|
|
@@ -513,12 +514,26 @@ var RawEmailIdentity = z.object({
|
|
|
513
514
|
message: "personal email identities in D channels require an owner"
|
|
514
515
|
});
|
|
515
516
|
}
|
|
517
|
+
if (identity.organization === true && identity.owner !== undefined) {
|
|
518
|
+
ctx.addIssue({
|
|
519
|
+
code: z.ZodIssueCode.custom,
|
|
520
|
+
path: ["organization"],
|
|
521
|
+
message: "mailbox organization is shared-only"
|
|
522
|
+
});
|
|
523
|
+
}
|
|
516
524
|
});
|
|
517
525
|
var RawEmailCapability = z.object({
|
|
518
526
|
enabled: z.boolean().default(false),
|
|
519
527
|
identities: z.array(RawEmailIdentity).default([]),
|
|
520
528
|
inboxCheck: z.enum(["hourly", "off"]).default("hourly")
|
|
521
529
|
}).strict().superRefine((value, ctx) => {
|
|
530
|
+
if (value.identities.filter((identity) => identity.organization === true).length > 1) {
|
|
531
|
+
ctx.addIssue({
|
|
532
|
+
code: z.ZodIssueCode.custom,
|
|
533
|
+
path: ["identities"],
|
|
534
|
+
message: "only one shared identity may have mailbox-wide organization access"
|
|
535
|
+
});
|
|
536
|
+
}
|
|
522
537
|
for (const field of ["address", "channel"]) {
|
|
523
538
|
const seen = new Set;
|
|
524
539
|
for (const [index, identity] of value.identities.entries()) {
|
|
@@ -727,7 +742,7 @@ var CAPABILITIES = [
|
|
|
727
742
|
},
|
|
728
743
|
{
|
|
729
744
|
id: "email",
|
|
730
|
-
summary: "Read
|
|
745
|
+
summary: "Read, draft and organize company mail; sending stays with a person.",
|
|
731
746
|
config: RawEmailCapability,
|
|
732
747
|
...NONE,
|
|
733
748
|
secrets: ["google/email", GOOGLE_OAUTH_SECRET],
|
|
@@ -736,6 +751,7 @@ var CAPABILITIES = [
|
|
|
736
751
|
googleScopes: [
|
|
737
752
|
"https://www.googleapis.com/auth/gmail.compose",
|
|
738
753
|
"https://www.googleapis.com/auth/gmail.modify",
|
|
754
|
+
"https://www.googleapis.com/auth/gmail.settings.basic",
|
|
739
755
|
...GOOGLE_IDENTITY_SCOPES
|
|
740
756
|
]
|
|
741
757
|
},
|
|
@@ -1125,6 +1141,12 @@ function newsletterManifestFor(instance) {
|
|
|
1125
1141
|
}))
|
|
1126
1142
|
};
|
|
1127
1143
|
}
|
|
1144
|
+
var FOUNDATION_VERSION_RE = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
1145
|
+
function versionedConfigKey(configKey, version) {
|
|
1146
|
+
if (!FOUNDATION_VERSION_RE.test(version))
|
|
1147
|
+
throw new Error(`not a Foundation version: "${version}" (expected one like v0.2.34)`);
|
|
1148
|
+
return `config/${version}/${configKey.slice(configKey.lastIndexOf("/") + 1)}`;
|
|
1149
|
+
}
|
|
1128
1150
|
function instanceNames(instance) {
|
|
1129
1151
|
const { prefix, secretPrefix, runtimeName } = instance.naming;
|
|
1130
1152
|
const stack = (suffix) => `${prefix}${suffix}`;
|
|
@@ -1864,4 +1886,4 @@ function ecrRepositoryArn(repositoryUri) {
|
|
|
1864
1886
|
return `arn:aws:ecr:${region}:${account}:repository/${name}`;
|
|
1865
1887
|
}
|
|
1866
1888
|
|
|
1867
|
-
export { parseSchedule, BASE_SLACK_EVENTS, CAPABILITY_IDS, requiredGithubPermissions, requiredSlackScopes, requiredSlackCommands, CUSTOMIZATION_ARTIFACT_NAME, newsletterManifestFor, instanceNames, slackCommandPrefix, slackCommandPrefixes, isScheduledJob, isWebhookJob, webhookJobBindings, scheduledJobBindings, jobWebhookKey, capabilityScheduledJobs, crmOutreachConfigured, crmPolicyFingerprint, parseInstanceConfig, loadInstanceFile, configPathFor, capabilityEnabled, requiresAuthenticatedQueue, enabledIntegrations, provisionsIntegration, adminsFor, WEB_SEARCH_TARGET, WEB_SEARCH_CONNECTOR_VERSION, LIVE_ENDPOINT_NAME, RELEASE_VERSION_RE, parseManifest, manifestKey, lambdaKey, skillsKey, buildManifest, manifestSigningPayload, verifyManifest, lambdaBundleContext, BUN_VERSION, LAMBDA_ENTRY_POINTS, skipBundle, RELEASE_BUCKET_ENV, RELEASE_DIR_ENV, releaseCacheDir, releaseSource, lambdaCode, agentImage, agentImageTagRequired, releaseImageRepositoryArn, ecrRepositoryArn };
|
|
1889
|
+
export { parseSchedule, BASE_SLACK_EVENTS, CAPABILITY_IDS, requiredGithubPermissions, requiredSlackScopes, requiredSlackCommands, CUSTOMIZATION_ARTIFACT_NAME, newsletterManifestFor, FOUNDATION_VERSION_RE, versionedConfigKey, instanceNames, slackCommandPrefix, slackCommandPrefixes, isScheduledJob, isWebhookJob, webhookJobBindings, scheduledJobBindings, jobWebhookKey, capabilityScheduledJobs, crmOutreachConfigured, crmPolicyFingerprint, parseInstanceConfig, loadInstanceFile, configPathFor, capabilityEnabled, requiresAuthenticatedQueue, enabledIntegrations, provisionsIntegration, adminsFor, WEB_SEARCH_TARGET, WEB_SEARCH_CONNECTOR_VERSION, LIVE_ENDPOINT_NAME, RELEASE_VERSION_RE, parseManifest, manifestKey, lambdaKey, skillsKey, buildManifest, manifestSigningPayload, verifyManifest, lambdaBundleContext, BUN_VERSION, LAMBDA_ENTRY_POINTS, skipBundle, RELEASE_BUCKET_ENV, RELEASE_DIR_ENV, releaseCacheDir, releaseSource, lambdaCode, agentImage, agentImageTagRequired, releaseImageRepositoryArn, ecrRepositoryArn };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CUSTOMIZATION_ARTIFACT_NAME,
|
|
3
|
+
FOUNDATION_VERSION_RE,
|
|
3
4
|
LIVE_ENDPOINT_NAME,
|
|
4
5
|
WEB_SEARCH_TARGET,
|
|
5
6
|
agentImage,
|
|
@@ -16,11 +17,12 @@ import {
|
|
|
16
17
|
parseSchedule,
|
|
17
18
|
provisionsIntegration,
|
|
18
19
|
releaseImageRepositoryArn,
|
|
20
|
+
releaseSource,
|
|
19
21
|
requiresAuthenticatedQueue,
|
|
20
22
|
scheduledJobBindings,
|
|
21
23
|
slackCommandPrefixes,
|
|
22
24
|
webhookJobBindings
|
|
23
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-hps5vshg.js";
|
|
24
26
|
|
|
25
27
|
// src/stacks/agent-stack.ts
|
|
26
28
|
import * as cdk from "aws-cdk-lib";
|
|
@@ -85,6 +87,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
85
87
|
if (!imageTag && agentImageTagRequired(this)) {
|
|
86
88
|
throw new Error(`${id}: -c agentImageTag=<tag> is required when deploying the runtime. Pass the tag of an image already pushed to the ${names.ecrRepo} repository, deploy a release with -c release=<version>, or run the bootstrap phase with -c deployRuntime=false.`);
|
|
87
89
|
}
|
|
90
|
+
const foundationVersion = foundationVersionContext(this, id);
|
|
88
91
|
const explicitSubnets = String(this.node.tryGetContext("agentSubnetIds") ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
89
92
|
const runtimeSubnets = explicitSubnets.length > 0 ? explicitSubnets : props.vpc.selectSubnets(props.egressSubnets).subnetIds;
|
|
90
93
|
const mountTargets = runtimeSubnets.map((subnetId, index) => new cdk.CfnResource(this, `MountTarget${index}`, {
|
|
@@ -129,6 +132,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
129
132
|
RUNTIME_SECRET_ID: props.secrets.runtime.secretName,
|
|
130
133
|
AWS_REGION: this.region,
|
|
131
134
|
FOUNDATION_WORKSPACE_ROOT: "/workspace",
|
|
135
|
+
FOUNDATION_VERSION: foundationVersion,
|
|
132
136
|
...secretDigest === undefined ? {} : { FOUNDATION_RUNTIME_SECRET_DIGEST: secretDigest }
|
|
133
137
|
},
|
|
134
138
|
Tags: { project: props.instance.naming.secretPrefix }
|
|
@@ -140,6 +144,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
140
144
|
runtime.node.addDependency(mountTarget);
|
|
141
145
|
this.agentRuntimeArn = runtime.getAtt("AgentRuntimeArn").toString();
|
|
142
146
|
this.outputs();
|
|
147
|
+
new cdk.CfnOutput(this, "FoundationVersion", { value: foundationVersion });
|
|
143
148
|
}
|
|
144
149
|
outputs() {
|
|
145
150
|
new cdk.CfnOutput(this, "AgentRuntimeArn", { value: this.agentRuntimeArn });
|
|
@@ -783,6 +788,18 @@ class FoundationAgent extends cdk.Stack {
|
|
|
783
788
|
function secretArnPattern(secret) {
|
|
784
789
|
return secret.secretFullArn ?? `${secret.secretArn}-??????`;
|
|
785
790
|
}
|
|
791
|
+
function foundationVersionContext(scope, id) {
|
|
792
|
+
const explicit = String(scope.node.tryGetContext("foundationVersion") ?? "");
|
|
793
|
+
const release = releaseSource(scope)?.version;
|
|
794
|
+
if (explicit !== "" && release !== undefined && explicit !== release)
|
|
795
|
+
throw new Error(`${id}: -c foundationVersion=${explicit} but the release being deployed is ${release}`);
|
|
796
|
+
const version = explicit !== "" ? explicit : release ?? "";
|
|
797
|
+
if (version === "")
|
|
798
|
+
throw new Error(`${id}: -c foundationVersion=<vX.Y.Z> is required when deploying the runtime: it reads only its own version's config. foundation-deploy passes it.`);
|
|
799
|
+
if (!FOUNDATION_VERSION_RE.test(version))
|
|
800
|
+
throw new Error(`${id}: foundationVersion must look like v0.2.34, got "${version}"`);
|
|
801
|
+
return version;
|
|
802
|
+
}
|
|
786
803
|
function runtimeSecretDigestContext(value) {
|
|
787
804
|
if (value === undefined || value === "")
|
|
788
805
|
return;
|
package/dist/src/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
FoundationPipeline,
|
|
11
11
|
NewsletterStack,
|
|
12
12
|
deployStatements
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-zbw2awg7.js";
|
|
14
14
|
import {
|
|
15
15
|
BUN_VERSION,
|
|
16
16
|
LAMBDA_ENTRY_POINTS,
|
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
skillsKey,
|
|
44
44
|
skipBundle,
|
|
45
45
|
verifyManifest
|
|
46
|
-
} from "../chunk-
|
|
46
|
+
} from "../chunk-hps5vshg.js";
|
|
47
47
|
export {
|
|
48
48
|
BUILD_TIMEOUT,
|
|
49
49
|
BUN_VERSION,
|
package/package.json
CHANGED
package/src/deploy/cli.ts
CHANGED
|
@@ -42,7 +42,8 @@ commands
|
|
|
42
42
|
deploy build and push the agent image, cdk deploy --all, refresh
|
|
43
43
|
the runtime secret, sync config
|
|
44
44
|
post-deploy smoke DEFAULT, probe the mount, promote \`live\`, smoke \`live\`
|
|
45
|
-
config:sync upload the runtime config and
|
|
45
|
+
config:sync upload the deployed version's runtime config and the
|
|
46
|
+
product skills to S3
|
|
46
47
|
setup first-deploy orchestration (idempotent)
|
|
47
48
|
github-app-create create the instance's GitHub App from the shipped manifest
|
|
48
49
|
slack-manifest print the instance's Slack app manifest as JSON
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* redeploy — except for `admins`, which is also baked into the gateway
|
|
8
8
|
* Lambda's environment.
|
|
9
9
|
*
|
|
10
|
+
* The config goes to ONE Foundation version's object (`versionedConfigKey`),
|
|
11
|
+
* which only that version's runtime reads. `deploy` names the version it
|
|
12
|
+
* deploys. A standalone `config:sync` writes for the version the Agent stack
|
|
13
|
+
* last deployed, and refuses unless it is that version itself.
|
|
14
|
+
*
|
|
10
15
|
* Product skills are part of the product, not of the deployment: they come
|
|
11
16
|
* from the release's `skills.tar.gz` when one is being deployed, and from this
|
|
12
17
|
* checkout's `skills/` otherwise. A company's own skills are a source in its
|
|
@@ -15,10 +20,11 @@
|
|
|
15
20
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync } from "node:fs";
|
|
16
21
|
import { tmpdir } from "node:os";
|
|
17
22
|
import { join, resolve } from "node:path";
|
|
23
|
+
import { versionedConfigKey } from "@deployfoundation/foundation-core/instance";
|
|
18
24
|
import { skillsKey } from "../release/manifest.ts";
|
|
19
25
|
import { type AwsContext, awsMutate, stackOutput } from "./aws.ts";
|
|
20
26
|
import { FOUNDATION_ROOT } from "./paths.ts";
|
|
21
|
-
import type
|
|
27
|
+
import { type ResolvedRelease, toolVersion } from "./release.ts";
|
|
22
28
|
import { run } from "./sh.ts";
|
|
23
29
|
|
|
24
30
|
/**
|
|
@@ -43,22 +49,115 @@ export interface ConfigSyncOptions {
|
|
|
43
49
|
release?: ResolvedRelease;
|
|
44
50
|
/** Test seam for the download-and-unpack above. */
|
|
45
51
|
releaseSkillsDir?: (ctx: AwsContext, release: ResolvedRelease) => Promise<string>;
|
|
52
|
+
/**
|
|
53
|
+
* The Foundation version whose config this writes. `deploy` and `setup`
|
|
54
|
+
* pass the version they deploy. Absent, it is the deployed version, which
|
|
55
|
+
* {@link syncVersion} checks against this tool's own.
|
|
56
|
+
*/
|
|
57
|
+
version?: string;
|
|
58
|
+
/** Test seam: the Agent stack's `FoundationVersion` output. */
|
|
59
|
+
deployedVersion?: (ctx: AwsContext) => Promise<string | undefined>;
|
|
60
|
+
/** Test seam: this tool's own version, as `vX.Y.Z`. */
|
|
61
|
+
toolVersion?: string;
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The Foundation version the Agent stack last deployed, from its
|
|
66
|
+
* `FoundationVersion` output. `undefined` when the stack has none: a runtime
|
|
67
|
+
* deployed before config was kept per version.
|
|
68
|
+
*/
|
|
69
|
+
export async function deployedFoundationVersion(ctx: AwsContext): Promise<string | undefined> {
|
|
70
|
+
try {
|
|
71
|
+
return await stackOutput(ctx, ctx.names.agent, "FoundationVersion");
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error instanceof Error && error.message.includes("has no output")) return undefined;
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The version a standalone `config:sync` may write for: the deployed one, and
|
|
80
|
+
* only when this tool (and the release it was asked for, if any) is that same
|
|
81
|
+
* version. The tool validated the config with its own schema; the deployed
|
|
82
|
+
* runtime parses it with its own, and those must be one schema.
|
|
83
|
+
*/
|
|
84
|
+
export function syncVersion(
|
|
85
|
+
ctx: AwsContext,
|
|
86
|
+
deployed: string | undefined,
|
|
87
|
+
tool: string,
|
|
88
|
+
release?: string,
|
|
89
|
+
): string {
|
|
90
|
+
if (deployed === undefined)
|
|
91
|
+
throw new Error(
|
|
92
|
+
`config:sync refused: ${ctx.names.agent} records no Foundation version, so its runtime predates per-version config. Deploy ${tool} first; the deploy writes this config itself.`,
|
|
93
|
+
);
|
|
94
|
+
for (const [what, version] of [
|
|
95
|
+
["this foundation-deploy", tool],
|
|
96
|
+
["--release", release],
|
|
97
|
+
] as const) {
|
|
98
|
+
if (version === undefined || version === deployed) continue;
|
|
99
|
+
throw new Error(
|
|
100
|
+
`config:sync refused: ${what} is ${version}, but ${ctx.names.agent} runs Foundation ${deployed}, whose runtime may refuse a config ${version} accepts. Run config:sync with foundation-deploy@${deployed.slice(1)}, or deploy ${version} first.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return deployed;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The data bucket. A dry run has no credentials to read a stack output with,
|
|
108
|
+
* and printing the placeholder is what every other dry-run path here does.
|
|
109
|
+
*/
|
|
110
|
+
async function dataBucket(ctx: AwsContext, bucket: string | undefined): Promise<string> {
|
|
111
|
+
if (bucket !== undefined) return bucket;
|
|
112
|
+
return ctx.dryRun === true
|
|
113
|
+
? `<${ctx.names.data}.BucketName>`
|
|
114
|
+
: await stackOutput(ctx, ctx.names.data, "BucketName");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Upload the runtime config to `version`'s own object, and nothing else.
|
|
119
|
+
*
|
|
120
|
+
* Safe at any point of a deploy, because no runtime of another version reads
|
|
121
|
+
* this object. `deploy` therefore also calls it BEFORE `cdk deploy`, so the
|
|
122
|
+
* new version's pre-warmed containers find their config at boot.
|
|
123
|
+
*/
|
|
124
|
+
export async function uploadRuntimeConfig(
|
|
125
|
+
ctx: AwsContext,
|
|
126
|
+
opts: { bucket: string; version: string; runtimeConfigPath?: string },
|
|
127
|
+
): Promise<string> {
|
|
49
128
|
if (opts.runtimeConfigPath === undefined && ctx.instance.customization !== undefined)
|
|
50
129
|
throw new Error(
|
|
51
130
|
"customized instances require a freshly validated runtimeConfigPath; run through `deploy` or `config:sync`",
|
|
52
131
|
);
|
|
53
132
|
const source = opts.runtimeConfigPath ?? ctx.paths.configPath;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
133
|
+
const key = versionedConfigKey(ctx.names.configKey, opts.version);
|
|
134
|
+
await awsMutate(ctx, ["s3", "cp", source, `s3://${opts.bucket}/${key}`]);
|
|
135
|
+
return key;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function configSync(ctx: AwsContext, opts: ConfigSyncOptions = {}): Promise<string> {
|
|
139
|
+
if (opts.runtimeConfigPath === undefined && ctx.instance.customization !== undefined)
|
|
140
|
+
throw new Error(
|
|
141
|
+
"customized instances require a freshly validated runtimeConfigPath; run through `deploy` or `config:sync`",
|
|
142
|
+
);
|
|
143
|
+
const tool = opts.toolVersion ?? `v${toolVersion()}`;
|
|
144
|
+
// A dry run cannot read the stack output either, so it plans this tool's.
|
|
145
|
+
const version =
|
|
146
|
+
opts.version ??
|
|
58
147
|
(ctx.dryRun === true
|
|
59
|
-
?
|
|
60
|
-
:
|
|
61
|
-
|
|
148
|
+
? tool
|
|
149
|
+
: syncVersion(
|
|
150
|
+
ctx,
|
|
151
|
+
await (opts.deployedVersion ?? deployedFoundationVersion)(ctx),
|
|
152
|
+
tool,
|
|
153
|
+
opts.release?.version,
|
|
154
|
+
));
|
|
155
|
+
const bucket = await dataBucket(ctx, opts.bucket);
|
|
156
|
+
await uploadRuntimeConfig(ctx, {
|
|
157
|
+
bucket,
|
|
158
|
+
version,
|
|
159
|
+
...(opts.runtimeConfigPath === undefined ? {} : { runtimeConfigPath: opts.runtimeConfigPath }),
|
|
160
|
+
});
|
|
62
161
|
const skills =
|
|
63
162
|
opts.release === undefined
|
|
64
163
|
? productSkillsDir(opts.foundationRoot)
|
package/src/deploy/deploy.ts
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
readSecretString,
|
|
29
29
|
stackOutput,
|
|
30
30
|
} from "./aws.ts";
|
|
31
|
-
import { configSync } from "./config-sync.ts";
|
|
31
|
+
import { configSync, uploadRuntimeConfig } from "./config-sync.ts";
|
|
32
32
|
import { adminsCsvFromFile } from "./config.ts";
|
|
33
33
|
import {
|
|
34
34
|
cliRunner,
|
|
@@ -57,9 +57,9 @@ import { AGENT_DOCKERFILE, FOUNDATION_ROOT, INFRA_ROOT } from "./paths.ts";
|
|
|
57
57
|
import {
|
|
58
58
|
type ReleaseRequest,
|
|
59
59
|
type ResolvedRelease,
|
|
60
|
+
foundationVersion,
|
|
60
61
|
releaseContext,
|
|
61
62
|
resolveRelease,
|
|
62
|
-
toolVersion,
|
|
63
63
|
} from "./release.ts";
|
|
64
64
|
import { type SlackAppSecret, composeRuntimeSecret } from "./runtime-secret.ts";
|
|
65
65
|
import { run } from "./sh.ts";
|
|
@@ -243,6 +243,9 @@ export async function cdkDeploy(ctx: AwsContext, opts: CdkDeployOptions): Promis
|
|
|
243
243
|
? []
|
|
244
244
|
: ["-c", `agentImageTag=${opts.tag}`]),
|
|
245
245
|
...(opts.release === undefined ? [] : releaseContext(opts.release)),
|
|
246
|
+
// The runtime's own version, and so the one config object it reads.
|
|
247
|
+
"-c",
|
|
248
|
+
`foundationVersion=${foundationVersion(opts.release)}`,
|
|
246
249
|
...(opts.runtimeSecretDigest === undefined
|
|
247
250
|
? []
|
|
248
251
|
: ["-c", `runtimeSecretDigest=${opts.runtimeSecretDigest}`]),
|
|
@@ -604,7 +607,7 @@ async function checkLicense(
|
|
|
604
607
|
const verify = opts.verifyLicense ?? (ctx.dryRun === true ? plannedLicenseCheck : verifyLicense);
|
|
605
608
|
const result = await verify(ctx.instance.license?.key, {
|
|
606
609
|
instanceId: ctx.instance.name,
|
|
607
|
-
version: release
|
|
610
|
+
version: foundationVersion(release),
|
|
608
611
|
cache: secretsManagerLicenseCache(ctx),
|
|
609
612
|
});
|
|
610
613
|
console.log(`▶ license: ${result.message}`);
|
|
@@ -666,6 +669,28 @@ export async function deploy(ctx: AwsContext, opts: DeployOptions): Promise<void
|
|
|
666
669
|
console.log(`▶ build + push agent image (${tag})`);
|
|
667
670
|
console.log(` ${await buildAndPushImage(ctx, { tag })}`);
|
|
668
671
|
}
|
|
672
|
+
|
|
673
|
+
// The config of the version going in, written BEFORE `cdk deploy` creates
|
|
674
|
+
// that version, so its pre-warmed containers boot with it rather than with
|
|
675
|
+
// the fallback. Nothing else reads this object: the version serving now
|
|
676
|
+
// reads its own, which this deploy never touches. A brand-new instance has
|
|
677
|
+
// no bucket yet; the config sync after `cdk deploy` covers it.
|
|
678
|
+
const version = foundationVersion(release);
|
|
679
|
+
const bucket = phase1
|
|
680
|
+
? undefined
|
|
681
|
+
: ctx.dryRun === true
|
|
682
|
+
? `<${ctx.names.data}.BucketName>`
|
|
683
|
+
: await optionalOutput(ctx, ctx.names.data, "BucketName");
|
|
684
|
+
if (bucket !== undefined) {
|
|
685
|
+
console.log(`▶ config for ${version}, ahead of the runtime version that reads it`);
|
|
686
|
+
const key = await uploadRuntimeConfig(ctx, {
|
|
687
|
+
bucket,
|
|
688
|
+
version,
|
|
689
|
+
runtimeConfigPath: customization.runtimeConfigPath,
|
|
690
|
+
});
|
|
691
|
+
console.log(` s3://${bucket}/${key}`);
|
|
692
|
+
}
|
|
693
|
+
|
|
669
694
|
console.log(
|
|
670
695
|
`▶ cdk deploy --all${
|
|
671
696
|
phase1
|
|
@@ -700,12 +725,13 @@ export async function deploy(ctx: AwsContext, opts: DeployOptions): Promise<void
|
|
|
700
725
|
redeploy: (digest) => deployStacks(digest),
|
|
701
726
|
});
|
|
702
727
|
|
|
703
|
-
console.log(
|
|
704
|
-
const
|
|
728
|
+
console.log(`▶ config sync (${version})`);
|
|
729
|
+
const synced = await configSync(ctx, {
|
|
730
|
+
version,
|
|
705
731
|
runtimeConfigPath: customization.runtimeConfigPath,
|
|
706
732
|
...(release === undefined ? {} : { release }),
|
|
707
733
|
});
|
|
708
|
-
console.log(`config + skills synced to s3://${
|
|
734
|
+
console.log(`config + skills synced to s3://${synced}/`);
|
|
709
735
|
|
|
710
736
|
if (!phase1 && promotesInline()) {
|
|
711
737
|
console.log("▶ smoke test, then promote the `live` endpoint");
|
package/src/deploy/release.ts
CHANGED
|
@@ -93,6 +93,14 @@ export function toolVersion(infraRoot: string = INFRA_ROOT): string {
|
|
|
93
93
|
return version;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The Foundation version a deploy puts in: the release's, or this tool's own
|
|
98
|
+
* for a local build, whose image is built from this same checkout.
|
|
99
|
+
*/
|
|
100
|
+
export function foundationVersion(release?: { version: string }): string {
|
|
101
|
+
return release?.version ?? `v${toolVersion()}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
96
104
|
/**
|
|
97
105
|
* Is there a Foundation workspace here to build Lambda bundles from?
|
|
98
106
|
*
|
package/src/deploy/setup.ts
CHANGED
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
} from "./endpoint.ts";
|
|
48
48
|
import { currentImageTag } from "./image.ts";
|
|
49
49
|
import { FOUNDATION_ROOT, INFRA_ROOT } from "./paths.ts";
|
|
50
|
-
import { hasFoundationWorkspace } from "./release.ts";
|
|
50
|
+
import { foundationVersion, hasFoundationWorkspace } from "./release.ts";
|
|
51
51
|
import { run, runCapture } from "./sh.ts";
|
|
52
52
|
import { slackManifestFor } from "./slack-manifest.ts";
|
|
53
53
|
import { stageCustomization } from "./stage-customization.ts";
|
|
@@ -374,6 +374,8 @@ export async function setup(
|
|
|
374
374
|
const bucket = await configSync(ctx, {
|
|
375
375
|
bucket: await output(ctx, ctx.names.data, "BucketName"),
|
|
376
376
|
runtimeConfigPath,
|
|
377
|
+
// Setup builds the image from this checkout, so it is this tool's version.
|
|
378
|
+
version: foundationVersion(),
|
|
377
379
|
});
|
|
378
380
|
console.log(` s3://${bucket}/`);
|
|
379
381
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FOUNDATION_VERSION_RE } from "@deployfoundation/foundation-core/instance";
|
|
1
2
|
import * as cdk from "aws-cdk-lib";
|
|
2
3
|
import type * as dynamodb from "aws-cdk-lib/aws-dynamodb";
|
|
3
4
|
import type * as ec2 from "aws-cdk-lib/aws-ec2";
|
|
@@ -8,7 +9,12 @@ import * as logs from "aws-cdk-lib/aws-logs";
|
|
|
8
9
|
import type * as s3 from "aws-cdk-lib/aws-s3";
|
|
9
10
|
import type * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
|
|
10
11
|
import type { Construct } from "constructs";
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
agentImage,
|
|
14
|
+
agentImageTagRequired,
|
|
15
|
+
releaseImageRepositoryArn,
|
|
16
|
+
releaseSource,
|
|
17
|
+
} from "../artifacts.ts";
|
|
12
18
|
import {
|
|
13
19
|
type Instance,
|
|
14
20
|
type InstanceNames,
|
|
@@ -177,6 +183,7 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
177
183
|
`${id}: -c agentImageTag=<tag> is required when deploying the runtime. Pass the tag of an image already pushed to the ${names.ecrRepo} repository, deploy a release with -c release=<version>, or run the bootstrap phase with -c deployRuntime=false.`,
|
|
178
184
|
);
|
|
179
185
|
}
|
|
186
|
+
const foundationVersion = foundationVersionContext(this, id);
|
|
180
187
|
|
|
181
188
|
const explicitSubnets = String(this.node.tryGetContext("agentSubnetIds") ?? "")
|
|
182
189
|
.split(",")
|
|
@@ -242,6 +249,11 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
242
249
|
RUNTIME_SECRET_ID: props.secrets.runtime.secretName,
|
|
243
250
|
AWS_REGION: this.region,
|
|
244
251
|
FOUNDATION_WORKSPACE_ROOT: "/workspace",
|
|
252
|
+
// Which Foundation this version runs, and so the one config object
|
|
253
|
+
// it reads (`config/<version>/…`). Here and not in the runtime
|
|
254
|
+
// secret: that secret is shared by every version still serving,
|
|
255
|
+
// while this map belongs to one runtime version.
|
|
256
|
+
FOUNDATION_VERSION: foundationVersion,
|
|
245
257
|
// Which runtime secret the containers of this version boot with.
|
|
246
258
|
// Nothing reads it: it is here so that a deploy whose secret refresh
|
|
247
259
|
// changed the secret can cut a new version through CloudFormation
|
|
@@ -260,6 +272,9 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
260
272
|
|
|
261
273
|
this.agentRuntimeArn = runtime.getAtt("AgentRuntimeArn").toString();
|
|
262
274
|
this.outputs();
|
|
275
|
+
// What `config:sync` writes for, and refuses to write past: the version of
|
|
276
|
+
// the runtime this stack last deployed.
|
|
277
|
+
new cdk.CfnOutput(this, "FoundationVersion", { value: foundationVersion });
|
|
263
278
|
}
|
|
264
279
|
|
|
265
280
|
private outputs(): void {
|
|
@@ -999,7 +1014,9 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
999
1014
|
}
|
|
1000
1015
|
// Config and curated skills are deployment-owned: direct S3 API writes are
|
|
1001
1016
|
// limited to memory/*. The separate fs/* persistent mount remains writable
|
|
1002
|
-
// through S3 Files for workspace state and learned skills.
|
|
1017
|
+
// through S3 Files for workspace state and learned skills. Reads are not
|
|
1018
|
+
// scoped to one version's `config/<version>/`: this one role serves every
|
|
1019
|
+
// runtime version still running, the outgoing one included.
|
|
1003
1020
|
role.addToPolicy(
|
|
1004
1021
|
new iam.PolicyStatement({
|
|
1005
1022
|
sid: "ConfigAndSkills",
|
|
@@ -1201,6 +1218,28 @@ function secretArnPattern(secret: secretsmanager.ISecret): string {
|
|
|
1201
1218
|
return secret.secretFullArn ?? `${secret.secretArn}-??????`;
|
|
1202
1219
|
}
|
|
1203
1220
|
|
|
1221
|
+
/**
|
|
1222
|
+
* The Foundation version this runtime version runs, which picks the one config
|
|
1223
|
+
* object it reads (`versionedConfigKey`). `-c foundationVersion=vX.Y.Z` comes
|
|
1224
|
+
* from the deploy tool; a release synth may omit it and use the release's own.
|
|
1225
|
+
*/
|
|
1226
|
+
function foundationVersionContext(scope: Construct, id: string): string {
|
|
1227
|
+
const explicit = String(scope.node.tryGetContext("foundationVersion") ?? "");
|
|
1228
|
+
const release = releaseSource(scope)?.version;
|
|
1229
|
+
if (explicit !== "" && release !== undefined && explicit !== release)
|
|
1230
|
+
throw new Error(
|
|
1231
|
+
`${id}: -c foundationVersion=${explicit} but the release being deployed is ${release}`,
|
|
1232
|
+
);
|
|
1233
|
+
const version = explicit !== "" ? explicit : (release ?? "");
|
|
1234
|
+
if (version === "")
|
|
1235
|
+
throw new Error(
|
|
1236
|
+
`${id}: -c foundationVersion=<vX.Y.Z> is required when deploying the runtime: it reads only its own version's config. foundation-deploy passes it.`,
|
|
1237
|
+
);
|
|
1238
|
+
if (!FOUNDATION_VERSION_RE.test(version))
|
|
1239
|
+
throw new Error(`${id}: foundationVersion must look like v0.2.34, got "${version}"`);
|
|
1240
|
+
return version;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1204
1243
|
/**
|
|
1205
1244
|
* `-c runtimeSecretDigest=<16 hex>`, from `runtimeSecretDigest()` in the
|
|
1206
1245
|
* deploy tool. Absent means "not known", which a first `cdk deploy` of a
|