@deployfoundation/foundation-deploy 0.2.6 → 0.2.8
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 +1 -1
- package/dist/bin/foundation-deploy.js +48 -6
- package/dist/{chunk-my0awr1g.js → chunk-revzxtba.js} +11 -1
- package/dist/src/index.js +1 -1
- package/package.json +1 -1
- package/src/deploy/deploy.ts +94 -9
- package/src/deploy/setup.ts +17 -2
- package/src/stacks/agent-stack.ts +20 -0
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ bun run packages/infra/bin/foundation-deploy.ts <command> --instance <path>
|
|
|
47
47
|
|
|
48
48
|
| Command | What it does |
|
|
49
49
|
| --- | --- |
|
|
50
|
-
| `deploy` | Build and push the agent image, `cdk deploy --all`, refresh the runtime secret, sync config. |
|
|
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
52
|
| `config:sync` | Upload the runtime config and Foundation's product skills to S3. |
|
|
53
53
|
| `setup` | First-deploy orchestration, idempotent. |
|
package/dist/bin/app.js
CHANGED
|
@@ -87,6 +87,7 @@ async function extractSkillsTarball(tarball, dir, opts = {}) {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
// src/deploy/deploy.ts
|
|
90
|
+
import { createHash } from "node:crypto";
|
|
90
91
|
import { existsSync as existsSync2 } from "node:fs";
|
|
91
92
|
import { createRequire } from "node:module";
|
|
92
93
|
import { join as join3 } from "node:path";
|
|
@@ -1030,7 +1031,8 @@ async function cdkDeploy(ctx, opts) {
|
|
|
1030
1031
|
...opts.alarmEmail === "" ? [] : ["-c", `alarmEmail=${opts.alarmEmail}`],
|
|
1031
1032
|
...opts.phase1 === true ? ["-c", "deployRuntime=false"] : [],
|
|
1032
1033
|
...opts.tag === undefined || opts.release !== undefined ? [] : ["-c", `agentImageTag=${opts.tag}`],
|
|
1033
|
-
...opts.release === undefined ? [] : releaseContext(opts.release)
|
|
1034
|
+
...opts.release === undefined ? [] : releaseContext(opts.release),
|
|
1035
|
+
...opts.runtimeSecretDigest === undefined ? [] : ["-c", `runtimeSecretDigest=${opts.runtimeSecretDigest}`]
|
|
1034
1036
|
];
|
|
1035
1037
|
await run([
|
|
1036
1038
|
...cdkCommand(),
|
|
@@ -1130,6 +1132,28 @@ async function syncRuntimeSecret(ctx) {
|
|
|
1130
1132
|
await putSecretJson(ctx, secretRuntime, value);
|
|
1131
1133
|
return Object.keys(value);
|
|
1132
1134
|
}
|
|
1135
|
+
function runtimeSecretDigest(value) {
|
|
1136
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
1137
|
+
}
|
|
1138
|
+
async function currentRuntimeSecretDigest(ctx) {
|
|
1139
|
+
if (ctx.dryRun === true)
|
|
1140
|
+
return;
|
|
1141
|
+
try {
|
|
1142
|
+
return runtimeSecretDigest(await readSecretString(ctx, ctx.names.secretRuntime));
|
|
1143
|
+
} catch {
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
async function refreshRuntimeSecret(before, refresh) {
|
|
1148
|
+
const log = refresh.log ?? ((line) => console.log(line));
|
|
1149
|
+
log(` keys: ${(await refresh.sync()).join(", ")}`);
|
|
1150
|
+
const after = await refresh.digest();
|
|
1151
|
+
if (after === undefined || after === before)
|
|
1152
|
+
return "unchanged";
|
|
1153
|
+
log("▶ the runtime secret changed: cdk deploy once more, so no container boots on the old one");
|
|
1154
|
+
await refresh.redeploy(after);
|
|
1155
|
+
return "rolled";
|
|
1156
|
+
}
|
|
1133
1157
|
async function promoteLive(ctx) {
|
|
1134
1158
|
const dry = ctx.dryRun === true;
|
|
1135
1159
|
const runner = cliRunner(ctx);
|
|
@@ -1184,19 +1208,26 @@ async function deploy(ctx, opts) {
|
|
|
1184
1208
|
console.log(` ${await buildAndPushImage(ctx, { tag })}`);
|
|
1185
1209
|
}
|
|
1186
1210
|
console.log(`▶ cdk deploy --all${phase1 ? " (phase 1: no runtime)" : release !== undefined ? ` (release ${release.version})` : ` (image ${tag})`}`);
|
|
1187
|
-
await
|
|
1211
|
+
const deployed = await currentRuntimeSecretDigest(ctx);
|
|
1212
|
+
const deployStacks = (runtimeSecretDigest2) => cdkDeploy(ctx, {
|
|
1188
1213
|
admins,
|
|
1189
1214
|
alarmEmail,
|
|
1190
1215
|
tag,
|
|
1191
1216
|
phase1,
|
|
1192
|
-
...release === undefined ? {} : { release }
|
|
1217
|
+
...release === undefined ? {} : { release },
|
|
1218
|
+
...runtimeSecretDigest2 === undefined ? {} : { runtimeSecretDigest: runtimeSecretDigest2 }
|
|
1193
1219
|
});
|
|
1220
|
+
await deployStacks(deployed);
|
|
1194
1221
|
if (ctx.instance.integrations.knock) {
|
|
1195
1222
|
console.log(`▶ Knock OAuth public-client registration (${ctx.names.secretKnockOauthClient})`);
|
|
1196
1223
|
console.log(` ${await syncKnockOAuthClient(ctx)}`);
|
|
1197
1224
|
}
|
|
1198
1225
|
console.log(`▶ runtime secret refresh (${ctx.names.secretRuntime})`);
|
|
1199
|
-
|
|
1226
|
+
await refreshRuntimeSecret(phase1 ? undefined : deployed, {
|
|
1227
|
+
sync: () => syncRuntimeSecret(ctx),
|
|
1228
|
+
digest: async () => phase1 ? undefined : currentRuntimeSecretDigest(ctx),
|
|
1229
|
+
redeploy: (digest) => deployStacks(digest)
|
|
1230
|
+
});
|
|
1200
1231
|
console.log("▶ config sync");
|
|
1201
1232
|
const bucket = await configSync(ctx, {
|
|
1202
1233
|
runtimeConfigPath: customization.runtimeConfigPath,
|
|
@@ -1819,9 +1850,20 @@ async function setup(ctx, opts = {}) {
|
|
|
1819
1850
|
console.log(` ${repositoryUri}:${tag}`);
|
|
1820
1851
|
await buildAndPushImage(ctx, { tag, repositoryUri });
|
|
1821
1852
|
step(8, "phase 2: deploy the AgentCore runtime");
|
|
1822
|
-
|
|
1853
|
+
const phase2 = (runtimeSecretDigest2) => cdkDeploy(ctx, {
|
|
1854
|
+
admins,
|
|
1855
|
+
alarmEmail,
|
|
1856
|
+
tag,
|
|
1857
|
+
...runtimeSecretDigest2 === undefined ? {} : { runtimeSecretDigest: runtimeSecretDigest2 }
|
|
1858
|
+
});
|
|
1859
|
+
const written = await currentRuntimeSecretDigest(ctx);
|
|
1860
|
+
await phase2(written);
|
|
1823
1861
|
step(9, `refresh ${ctx.names.secretRuntime} with the runtime arn`);
|
|
1824
|
-
await
|
|
1862
|
+
await refreshRuntimeSecret(written, {
|
|
1863
|
+
sync: () => syncRuntimeSecret(ctx),
|
|
1864
|
+
digest: () => currentRuntimeSecretDigest(ctx),
|
|
1865
|
+
redeploy: phase2
|
|
1866
|
+
});
|
|
1825
1867
|
step(10, "create the pinned `live` endpoint");
|
|
1826
1868
|
await ensureLiveEndpoint(ctx);
|
|
1827
1869
|
step(11, `seed ${ctx.names.secretCodex}`);
|
|
@@ -41,6 +41,7 @@ class FoundationAgent extends cdk.Stack {
|
|
|
41
41
|
const display = props.instance.displayName;
|
|
42
42
|
const deployRuntime = String(this.node.tryGetContext("deployRuntime") ?? "true") !== "false";
|
|
43
43
|
const imageTag = this.node.tryGetContext("agentImageTag");
|
|
44
|
+
const secretDigest = runtimeSecretDigestContext(this.node.tryGetContext("runtimeSecretDigest"));
|
|
44
45
|
this.repository = new ecr.Repository(this, "AgentRepository", {
|
|
45
46
|
repositoryName: names.ecrRepo,
|
|
46
47
|
imageScanOnPush: true,
|
|
@@ -124,7 +125,8 @@ class FoundationAgent extends cdk.Stack {
|
|
|
124
125
|
EnvironmentVariables: {
|
|
125
126
|
RUNTIME_SECRET_ID: props.secrets.runtime.secretName,
|
|
126
127
|
AWS_REGION: this.region,
|
|
127
|
-
FOUNDATION_WORKSPACE_ROOT: "/workspace"
|
|
128
|
+
FOUNDATION_WORKSPACE_ROOT: "/workspace",
|
|
129
|
+
...secretDigest === undefined ? {} : { FOUNDATION_RUNTIME_SECRET_DIGEST: secretDigest }
|
|
128
130
|
},
|
|
129
131
|
Tags: { project: props.instance.naming.secretPrefix }
|
|
130
132
|
}
|
|
@@ -661,6 +663,14 @@ class FoundationAgent extends cdk.Stack {
|
|
|
661
663
|
function secretArnPattern(secret) {
|
|
662
664
|
return secret.secretFullArn ?? `${secret.secretArn}-??????`;
|
|
663
665
|
}
|
|
666
|
+
function runtimeSecretDigestContext(value) {
|
|
667
|
+
if (value === undefined || value === "")
|
|
668
|
+
return;
|
|
669
|
+
const digest = String(value);
|
|
670
|
+
if (!/^[0-9a-f]{16}$/.test(digest))
|
|
671
|
+
throw new Error(`runtimeSecretDigest must be 16 lowercase hex characters, got "${digest}"`);
|
|
672
|
+
return digest;
|
|
673
|
+
}
|
|
664
674
|
|
|
665
675
|
// src/stacks/api-stack.ts
|
|
666
676
|
import { readFileSync } from "node:fs";
|
package/dist/src/index.js
CHANGED
package/package.json
CHANGED
package/src/deploy/deploy.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* `--phase1` deploys everything except the runtime (`-c deployRuntime=false`);
|
|
12
12
|
* a plain `deploy` afterwards builds, pushes and creates it.
|
|
13
13
|
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
14
15
|
import { existsSync } from "node:fs";
|
|
15
16
|
import { createRequire } from "node:module";
|
|
16
17
|
import { join } from "node:path";
|
|
@@ -18,7 +19,15 @@ import {
|
|
|
18
19
|
parseKnockOAuthClient,
|
|
19
20
|
registerKnockPublicClient,
|
|
20
21
|
} from "@deployfoundation/foundation-connectors";
|
|
21
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
type AwsContext,
|
|
24
|
+
aws,
|
|
25
|
+
cdkEnv,
|
|
26
|
+
putSecretJson,
|
|
27
|
+
readSecretJson,
|
|
28
|
+
readSecretString,
|
|
29
|
+
stackOutput,
|
|
30
|
+
} from "./aws.ts";
|
|
22
31
|
import { configSync } from "./config-sync.ts";
|
|
23
32
|
import { adminsCsvFromFile } from "./config.ts";
|
|
24
33
|
import {
|
|
@@ -206,6 +215,8 @@ export interface CdkDeployOptions {
|
|
|
206
215
|
phase1?: boolean;
|
|
207
216
|
/** A verified release; its manifest is what the app reads its artifacts from. */
|
|
208
217
|
release?: ResolvedRelease;
|
|
218
|
+
/** {@link runtimeSecretDigest} of the runtime secret the new version should boot with. */
|
|
219
|
+
runtimeSecretDigest?: string;
|
|
209
220
|
}
|
|
210
221
|
|
|
211
222
|
export async function cdkDeploy(ctx: AwsContext, opts: CdkDeployOptions): Promise<void> {
|
|
@@ -225,6 +236,9 @@ export async function cdkDeploy(ctx: AwsContext, opts: CdkDeployOptions): Promis
|
|
|
225
236
|
? []
|
|
226
237
|
: ["-c", `agentImageTag=${opts.tag}`]),
|
|
227
238
|
...(opts.release === undefined ? [] : releaseContext(opts.release)),
|
|
239
|
+
...(opts.runtimeSecretDigest === undefined
|
|
240
|
+
? []
|
|
241
|
+
: ["-c", `runtimeSecretDigest=${opts.runtimeSecretDigest}`]),
|
|
228
242
|
];
|
|
229
243
|
await run(
|
|
230
244
|
[
|
|
@@ -410,6 +424,68 @@ export async function syncRuntimeSecret(ctx: AwsContext): Promise<string[]> {
|
|
|
410
424
|
return Object.keys(value);
|
|
411
425
|
}
|
|
412
426
|
|
|
427
|
+
/**
|
|
428
|
+
* A short, one-way digest of the runtime secret as stored. It ends up in the
|
|
429
|
+
* runtime's environment (see the Agent stack), so it is truncated SHA-256 of
|
|
430
|
+
* a document full of high-entropy tokens, never anything reversible.
|
|
431
|
+
*/
|
|
432
|
+
export function runtimeSecretDigest(value: string): string {
|
|
433
|
+
return createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** The digest of the runtime secret now, or undefined when there is none yet (or in a dry run). */
|
|
437
|
+
export async function currentRuntimeSecretDigest(ctx: AwsContext): Promise<string | undefined> {
|
|
438
|
+
if (ctx.dryRun === true) return undefined;
|
|
439
|
+
try {
|
|
440
|
+
return runtimeSecretDigest(await readSecretString(ctx, ctx.names.secretRuntime));
|
|
441
|
+
} catch {
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export interface RuntimeSecretRefresh {
|
|
447
|
+
/** Rewrite the secret from the stack outputs; returns the key names. */
|
|
448
|
+
sync: () => Promise<string[]>;
|
|
449
|
+
/** The secret's digest as it stands now. */
|
|
450
|
+
digest: () => Promise<string | undefined>;
|
|
451
|
+
/** `cdk deploy` again with this digest, which cuts a new runtime version. */
|
|
452
|
+
redeploy: (digest: string) => Promise<void>;
|
|
453
|
+
log?: (line: string) => void;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Refresh the runtime secret, and when that changed it, roll the runtime.
|
|
458
|
+
*
|
|
459
|
+
* A container reads the secret once, at boot, and AgentCore boots a pool of
|
|
460
|
+
* them ahead of demand as soon as a version exists. The secret is composed from
|
|
461
|
+
* stack outputs (the runtime's own ARN among them), so it can only be written
|
|
462
|
+
* AFTER `cdk deploy` created the version, by which time that pool has already
|
|
463
|
+
* booted on the old value: a first deploy's placeholder, or an upgrade's stale
|
|
464
|
+
* outputs. Those containers answer with the fallback config until they
|
|
465
|
+
* recycle, and every session they serve in the meantime is broken. Found by
|
|
466
|
+
* migrating Sky, where a dozen of them failed smoke after smoke.
|
|
467
|
+
*
|
|
468
|
+
* Writing first is impossible, and `update-agent-runtime` from the CLI would
|
|
469
|
+
* resend an incomplete definition (the CLI does not carry the S3 Files mount),
|
|
470
|
+
* so the new version comes from CloudFormation: `cdk deploy` once more with the
|
|
471
|
+
* new digest in the runtime's environment. An upgrade whose outputs did not
|
|
472
|
+
* change produces the same digest and costs nothing.
|
|
473
|
+
*
|
|
474
|
+
* `before` is the digest the version just deployed was given.
|
|
475
|
+
*/
|
|
476
|
+
export async function refreshRuntimeSecret(
|
|
477
|
+
before: string | undefined,
|
|
478
|
+
refresh: RuntimeSecretRefresh,
|
|
479
|
+
): Promise<"unchanged" | "rolled"> {
|
|
480
|
+
const log = refresh.log ?? ((line: string) => console.log(line));
|
|
481
|
+
log(` keys: ${(await refresh.sync()).join(", ")}`);
|
|
482
|
+
const after = await refresh.digest();
|
|
483
|
+
if (after === undefined || after === before) return "unchanged";
|
|
484
|
+
log("▶ the runtime secret changed: cdk deploy once more, so no container boots on the old one");
|
|
485
|
+
await refresh.redeploy(after);
|
|
486
|
+
return "rolled";
|
|
487
|
+
}
|
|
488
|
+
|
|
413
489
|
/**
|
|
414
490
|
* What the workflow does after a deploy, for a deploy run from a laptop: smoke
|
|
415
491
|
* the version just created on DEFAULT, move the pinned `live` endpoint onto
|
|
@@ -543,20 +619,29 @@ export async function deploy(ctx: AwsContext, opts: DeployOptions): Promise<void
|
|
|
543
619
|
: ` (image ${tag})`
|
|
544
620
|
}`,
|
|
545
621
|
);
|
|
546
|
-
await
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
622
|
+
const deployed = await currentRuntimeSecretDigest(ctx);
|
|
623
|
+
const deployStacks = (runtimeSecretDigest: string | undefined) =>
|
|
624
|
+
cdkDeploy(ctx, {
|
|
625
|
+
admins,
|
|
626
|
+
alarmEmail,
|
|
627
|
+
tag,
|
|
628
|
+
phase1,
|
|
629
|
+
...(release === undefined ? {} : { release }),
|
|
630
|
+
...(runtimeSecretDigest === undefined ? {} : { runtimeSecretDigest }),
|
|
631
|
+
});
|
|
632
|
+
await deployStacks(deployed);
|
|
553
633
|
if (ctx.instance.integrations.knock) {
|
|
554
634
|
console.log(`▶ Knock OAuth public-client registration (${ctx.names.secretKnockOauthClient})`);
|
|
555
635
|
console.log(` ${await syncKnockOAuthClient(ctx)}`);
|
|
556
636
|
}
|
|
557
637
|
|
|
558
638
|
console.log(`▶ runtime secret refresh (${ctx.names.secretRuntime})`);
|
|
559
|
-
|
|
639
|
+
await refreshRuntimeSecret(phase1 ? undefined : deployed, {
|
|
640
|
+
sync: () => syncRuntimeSecret(ctx),
|
|
641
|
+
// Phase 1 has no runtime to roll.
|
|
642
|
+
digest: async () => (phase1 ? undefined : currentRuntimeSecretDigest(ctx)),
|
|
643
|
+
redeploy: (digest) => deployStacks(digest),
|
|
644
|
+
});
|
|
560
645
|
|
|
561
646
|
console.log("▶ config sync");
|
|
562
647
|
const bucket = await configSync(ctx, {
|
package/src/deploy/setup.ts
CHANGED
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
buildAndPushImage,
|
|
32
32
|
cdkCommand,
|
|
33
33
|
cdkDeploy,
|
|
34
|
+
currentRuntimeSecretDigest,
|
|
35
|
+
refreshRuntimeSecret,
|
|
34
36
|
syncKnockOAuthClient,
|
|
35
37
|
syncRuntimeSecret,
|
|
36
38
|
} from "./deploy.ts";
|
|
@@ -375,12 +377,25 @@ export async function setup(
|
|
|
375
377
|
await buildAndPushImage(ctx, { tag, repositoryUri });
|
|
376
378
|
|
|
377
379
|
step(8, "phase 2: deploy the AgentCore runtime");
|
|
378
|
-
|
|
380
|
+
const phase2 = (runtimeSecretDigest: string | undefined) =>
|
|
381
|
+
cdkDeploy(ctx, {
|
|
382
|
+
admins,
|
|
383
|
+
alarmEmail,
|
|
384
|
+
tag,
|
|
385
|
+
...(runtimeSecretDigest === undefined ? {} : { runtimeSecretDigest }),
|
|
386
|
+
});
|
|
387
|
+
const written = await currentRuntimeSecretDigest(ctx);
|
|
388
|
+
await phase2(written);
|
|
379
389
|
|
|
380
390
|
// Only FOUNDATION_AGENT_RUNTIME_ARN needs phase 2, but it is what a routine's
|
|
381
391
|
// schedule targets, so the secret is composed once more now that it exists.
|
|
392
|
+
// That changes it, and phase 2's containers booted without it: roll them.
|
|
382
393
|
step(9, `refresh ${ctx.names.secretRuntime} with the runtime arn`);
|
|
383
|
-
await
|
|
394
|
+
await refreshRuntimeSecret(written, {
|
|
395
|
+
sync: () => syncRuntimeSecret(ctx),
|
|
396
|
+
digest: () => currentRuntimeSecretDigest(ctx),
|
|
397
|
+
redeploy: phase2,
|
|
398
|
+
});
|
|
384
399
|
|
|
385
400
|
step(10, "create the pinned `live` endpoint");
|
|
386
401
|
await ensureLiveEndpoint(ctx);
|
|
@@ -108,6 +108,7 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
108
108
|
|
|
109
109
|
const deployRuntime = String(this.node.tryGetContext("deployRuntime") ?? "true") !== "false";
|
|
110
110
|
const imageTag: string | undefined = this.node.tryGetContext("agentImageTag");
|
|
111
|
+
const secretDigest = runtimeSecretDigestContext(this.node.tryGetContext("runtimeSecretDigest"));
|
|
111
112
|
|
|
112
113
|
this.repository = new ecr.Repository(this, "AgentRepository", {
|
|
113
114
|
repositoryName: names.ecrRepo,
|
|
@@ -237,6 +238,12 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
237
238
|
RUNTIME_SECRET_ID: props.secrets.runtime.secretName,
|
|
238
239
|
AWS_REGION: this.region,
|
|
239
240
|
FOUNDATION_WORKSPACE_ROOT: "/workspace",
|
|
241
|
+
// Which runtime secret the containers of this version boot with.
|
|
242
|
+
// Nothing reads it: it is here so that a deploy whose secret refresh
|
|
243
|
+
// changed the secret can cut a new version through CloudFormation
|
|
244
|
+
// (the only path that carries every property, the mount included),
|
|
245
|
+
// and no pre-warmed container keeps serving the old value.
|
|
246
|
+
...(secretDigest === undefined ? {} : { FOUNDATION_RUNTIME_SECRET_DIGEST: secretDigest }),
|
|
240
247
|
},
|
|
241
248
|
// The App-level tag aspect cannot reach a raw CfnResource.
|
|
242
249
|
Tags: { project: props.instance.naming.secretPrefix },
|
|
@@ -1004,3 +1011,16 @@ export class FoundationAgent extends cdk.Stack {
|
|
|
1004
1011
|
function secretArnPattern(secret: secretsmanager.ISecret): string {
|
|
1005
1012
|
return secret.secretFullArn ?? `${secret.secretArn}-??????`;
|
|
1006
1013
|
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* `-c runtimeSecretDigest=<16 hex>`, from `runtimeSecretDigest()` in the
|
|
1017
|
+
* deploy tool. Absent means "not known", which a first `cdk deploy` of a
|
|
1018
|
+
* brand-new instance is: its secret does not exist yet.
|
|
1019
|
+
*/
|
|
1020
|
+
function runtimeSecretDigestContext(value: unknown): string | undefined {
|
|
1021
|
+
if (value === undefined || value === "") return undefined;
|
|
1022
|
+
const digest = String(value);
|
|
1023
|
+
if (!/^[0-9a-f]{16}$/.test(digest))
|
|
1024
|
+
throw new Error(`runtimeSecretDigest must be 16 lowercase hex characters, got "${digest}"`);
|
|
1025
|
+
return digest;
|
|
1026
|
+
}
|