@continuous-excellence/ze-great-dashboard-aws 0.7.0 → 0.8.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 CHANGED
@@ -57,10 +57,13 @@ npm exec -- ze-great-dashboard-aws doctor \
57
57
 
58
58
  npm exec -- ze-great-dashboard-aws package \
59
59
  --board-config board.yaml \
60
+ --parameters aws-dashboard-parameters.json \
60
61
  --output aws-dashboard-release
61
62
  ```
62
63
 
63
- The deployment guide covers the S3 upload, CloudFormation command, gateway handoff, and CI example.
64
+ `package` writes the complete release-specific `parameters.json` and machine-readable
65
+ `deployment.json` alongside the ZIP and template. The deployment guide covers executing the emitted
66
+ S3 upload and CloudFormation commands, gateway handoff, and CI example.
64
67
  Changing `board.yaml` or upgrading the pinned package uses this same path.
65
68
 
66
69
  ## Package boundaries
package/dist/cli.js CHANGED
@@ -2075,34 +2075,6 @@ async function publishClientAssets(options) {
2075
2075
  ]);
2076
2076
  return assetPath;
2077
2077
  }
2078
- async function deployLambda(options) {
2079
- const artifactDir = resolve2(options.artifactDir);
2080
- const assetsDir = resolve2(options.assetsDir);
2081
- await readFile3(join2(artifactDir, "lambda.zip"));
2082
- await readFile3(join2(assetsDir, "index.html"));
2083
- if (options.dryRun) return;
2084
- const assetPath = await publishClientAssets(options);
2085
- await run("aws", [
2086
- "lambda",
2087
- "update-function-code",
2088
- "--function-name",
2089
- options.functionName,
2090
- "--zip-file",
2091
- `fileb://${join2(artifactDir, "lambda.zip")}`,
2092
- "--no-cli-pager"
2093
- ]);
2094
- await run("aws", ["lambda", "wait", "function-updated", "--function-name", options.functionName]);
2095
- await run("aws", [
2096
- "lambda",
2097
- "update-function-configuration",
2098
- "--function-name",
2099
- options.functionName,
2100
- "--environment",
2101
- `Variables={ASSET_PATH=${assetPath},BOARD_CONFIG_URL=./board.yaml,HOST=0.0.0.0}`,
2102
- "--no-cli-pager"
2103
- ]);
2104
- await run("aws", ["lambda", "wait", "function-updated", "--function-name", options.functionName]);
2105
- }
2106
2078
  async function cloudFormationTemplate() {
2107
2079
  return readFile3(fileURLToPath2(new URL("../template.yml", import.meta.url)), "utf8");
2108
2080
  }
@@ -2284,7 +2256,7 @@ var requiredOption = (name, fallback) => {
2284
2256
  function parameter(key, value2) {
2285
2257
  return { ParameterKey: key, ParameterValue: value2 };
2286
2258
  }
2287
- async function existingParameters(path) {
2259
+ async function existingParameters(path, allowMissing = false) {
2288
2260
  try {
2289
2261
  const parsed = JSON.parse(await readFile5(path, "utf8"));
2290
2262
  if (!Array.isArray(parsed)) throw new Error(`${path} must contain a JSON parameter array`);
@@ -2299,7 +2271,8 @@ async function existingParameters(path) {
2299
2271
  if (new Set(keys).size !== keys.length) throw new Error(`${path} contains duplicate parameters`);
2300
2272
  return values;
2301
2273
  } catch (error) {
2302
- if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return [];
2274
+ if (allowMissing && error && typeof error === "object" && "code" in error && error.code === "ENOENT")
2275
+ return [];
2303
2276
  throw error;
2304
2277
  }
2305
2278
  }
@@ -2373,23 +2346,86 @@ async function templateParameters() {
2373
2346
  );
2374
2347
  return { definitions: parsed.Parameters, packageManaged };
2375
2348
  }
2349
+ function validateConsumerParameters(supplied, template) {
2350
+ const values = Object.fromEntries(
2351
+ supplied.map(({ ParameterKey, ParameterValue }) => [ParameterKey, ParameterValue])
2352
+ );
2353
+ const templateKeys = Object.keys(template.definitions);
2354
+ const unknown = Object.keys(values).filter((key) => !templateKeys.includes(key));
2355
+ if (unknown.length)
2356
+ throw new Error(`Parameter file contains unknown parameters: ${unknown.join(", ")}`);
2357
+ const overridden = [...template.packageManaged].filter((key) => Object.hasOwn(values, key));
2358
+ if (overridden.length)
2359
+ throw new Error(
2360
+ `Parameter file must not set package-managed parameters: ${overridden.join(", ")}`
2361
+ );
2362
+ const missing = templateKeys.filter(
2363
+ (key) => !template.packageManaged.has(key) && !Object.hasOwn(values, key) && !Object.hasOwn(template.definitions[key] ?? {}, "Default")
2364
+ );
2365
+ if (missing.length)
2366
+ throw new Error(`Parameter file is missing required consumer values: ${missing.join(", ")}`);
2367
+ return values;
2368
+ }
2369
+ function resolvedReleaseParameters(supplied, template, managedValues) {
2370
+ const templateKeys = Object.keys(template.definitions);
2371
+ return templateKeys.map((key) => {
2372
+ const value2 = template.packageManaged.has(key) ? managedValues[key] : supplied[key] ?? template.definitions[key]?.Default;
2373
+ if (value2 === void 0) throw new Error(`No value for CloudFormation parameter ${key}`);
2374
+ return parameter(key, String(value2));
2375
+ });
2376
+ }
2377
+ function deploymentHandoff(input) {
2378
+ const prefix = input.outputDir.replace(/\/+$/, "");
2379
+ const path = (name) => `${prefix}/${name}`;
2380
+ const parameterFile = path("parameters.json");
2381
+ return {
2382
+ template: "template.yml",
2383
+ lambdaZip: "lambda.zip",
2384
+ releaseFile: "release.json",
2385
+ artifact: { bucket: input.artifactBucket, key: input.artifactKey },
2386
+ release: input.release,
2387
+ parameters: "parameters.json",
2388
+ commands: {
2389
+ upload: [
2390
+ "aws",
2391
+ "s3",
2392
+ "cp",
2393
+ path("lambda.zip"),
2394
+ `s3://${input.artifactBucket}/${input.artifactKey}`,
2395
+ "--region",
2396
+ "$AWS_REGION"
2397
+ ],
2398
+ deploy: [
2399
+ "aws",
2400
+ "cloudformation",
2401
+ "deploy",
2402
+ "--stack-name",
2403
+ "$STACK_NAME",
2404
+ "--template-file",
2405
+ path("template.yml"),
2406
+ "--role-arn",
2407
+ "$AWS_CLOUDFORMATION_EXECUTION_ROLE_ARN",
2408
+ "--region",
2409
+ "$AWS_REGION",
2410
+ "--capabilities",
2411
+ "CAPABILITY_NAMED_IAM",
2412
+ "--parameter-overrides",
2413
+ `file://${parameterFile}`,
2414
+ "--no-fail-on-empty-changeset",
2415
+ "--no-cli-pager"
2416
+ ]
2417
+ }
2418
+ };
2419
+ }
2420
+ function copyableCommand(command) {
2421
+ return command.map(
2422
+ (argument) => /^\$[A-Z][A-Z0-9_]*$/.test(argument) ? argument : `'${argument.replaceAll("'", `'\\"'\\"'`)}'`
2423
+ ).join(" ");
2424
+ }
2376
2425
  try {
2377
2426
  const packageVersion = await installedPackageVersion();
2378
2427
  const bundledAssets = fileURLToPath3(new URL("../client", import.meta.url));
2379
- if (args[0] === "deploy") {
2380
- const artifactDir = requiredOption("--artifact-dir");
2381
- const version = requiredOption("--version", packageVersion);
2382
- await deployLambda({
2383
- artifactDir,
2384
- assetsDir: option("--assets-dir", bundledAssets) ?? bundledAssets,
2385
- assetsBucket: requiredOption("--assets-bucket"),
2386
- assetsBaseUrl: requiredOption("--assets-base-url"),
2387
- functionName: requiredOption("--function-name"),
2388
- version,
2389
- dryRun: args.includes("--dry-run")
2390
- });
2391
- console.log(JSON.stringify({ deployed: true, version }));
2392
- } else if (args[0] === "publish-assets") {
2428
+ if (args[0] === "publish-assets") {
2393
2429
  const version = requiredOption("--version", packageVersion);
2394
2430
  const assetPath = await publishClientAssets({
2395
2431
  assetsDir: option("--assets-dir", bundledAssets) ?? bundledAssets,
@@ -2427,7 +2463,7 @@ try {
2427
2463
  if (checks.some(({ ok }) => !ok)) process.exitCode = 1;
2428
2464
  } else if (args[0] === "parameters") {
2429
2465
  const output = option("--output", "aws-dashboard-parameters.json") ?? "aws-dashboard-parameters.json";
2430
- const existing = await existingParameters(output);
2466
+ const existing = await existingParameters(output, true);
2431
2467
  const consumerConfigPath = option("--bootstrap-config");
2432
2468
  const consumerConfig = consumerConfigPath ? await readBootstrapConfig(consumerConfigPath) : void 0;
2433
2469
  const existingValues = Object.fromEntries(
@@ -2713,7 +2749,7 @@ ${plan.notes.join("\n")}`);
2713
2749
  }
2714
2750
  } else if (args[0] !== "package")
2715
2751
  throw new Error(
2716
- "Usage: ze-great-dashboard-aws package|parameters|bootstrap|publish-assets|deploy|doctor [options]"
2752
+ "Usage: ze-great-dashboard-aws package|parameters|bootstrap|publish-assets|doctor [options]"
2717
2753
  );
2718
2754
  else {
2719
2755
  const boardConfig = option("--board-config");
@@ -2721,13 +2757,41 @@ ${plan.notes.join("\n")}`);
2721
2757
  if (!boardConfig) throw new Error("--board-config is required");
2722
2758
  if (!version)
2723
2759
  throw new Error("Unable to determine the package version; pass --version explicitly");
2760
+ const outputDir = option("--output", "aws-dashboard-release") ?? "aws-dashboard-release";
2761
+ const parametersPath = option("--parameters", "aws-dashboard-parameters.json") ?? "aws-dashboard-parameters.json";
2762
+ const template = await templateParameters();
2763
+ const consumerParameters = validateConsumerParameters(
2764
+ await existingParameters(parametersPath),
2765
+ template
2766
+ );
2724
2767
  const metadata = await packageLambda({
2725
2768
  boardConfigPath: boardConfig,
2726
- outputDir: option("--output", "aws-release") ?? "aws-release",
2769
+ outputDir,
2727
2770
  version,
2728
2771
  assetDomain: option("--asset-domain")
2729
2772
  });
2730
- console.log(JSON.stringify(metadata));
2773
+ const resolvedParameters = resolvedReleaseParameters(consumerParameters, template, {
2774
+ LambdaArtifactKey: metadata.artifactKey,
2775
+ DashboardVersion: metadata.dashboardVersion
2776
+ });
2777
+ await writeFile3(
2778
+ `${outputDir}/parameters.json`,
2779
+ `${JSON.stringify(resolvedParameters, null, 2)}
2780
+ `
2781
+ );
2782
+ const handoff = deploymentHandoff({
2783
+ outputDir,
2784
+ artifactBucket: resolvedParameters.find(({ ParameterKey }) => ParameterKey === "LambdaArtifactBucket")?.ParameterValue ?? "",
2785
+ artifactKey: metadata.artifactKey,
2786
+ release: metadata
2787
+ });
2788
+ await writeFile3(`${outputDir}/deployment.json`, `${JSON.stringify(handoff, null, 2)}
2789
+ `);
2790
+ console.log(`Packaged ${outputDir}/lambda.zip and ${outputDir}/template.yml`);
2791
+ console.log(`Complete parameters: ${outputDir}/parameters.json`);
2792
+ console.log(`Deployment handoff: ${outputDir}/deployment.json`);
2793
+ console.log(`Upload: ${copyableCommand(handoff.commands.upload)}`);
2794
+ console.log(`Deploy: ${copyableCommand(handoff.commands.deploy)}`);
2731
2795
  }
2732
2796
  } catch (error) {
2733
2797
  console.error(error instanceof Error ? error.message : error);
package/dist/index.d.ts CHANGED
@@ -22,15 +22,6 @@ export type LambdaPackageOptions = {
22
22
  assetDomain?: string;
23
23
  };
24
24
  export declare function packageLambda(options: LambdaPackageOptions): Promise<PackagedRelease>;
25
- export type DeployLambdaOptions = {
26
- artifactDir: string;
27
- assetsDir: string;
28
- assetsBucket: string;
29
- assetsBaseUrl: string;
30
- functionName: string;
31
- version: string;
32
- dryRun?: boolean;
33
- };
34
25
  export type PublishClientAssetsOptions = {
35
26
  assetsDir: string;
36
27
  assetsBucket: string;
@@ -39,6 +30,4 @@ export type PublishClientAssetsOptions = {
39
30
  };
40
31
  /** Publishes the versioned client half of a provider-managed release. */
41
32
  export declare function publishClientAssets(options: PublishClientAssetsOptions): Promise<string>;
42
- /** Performs the AWS-specific half of a release using explicit deployment outputs. */
43
- export declare function deployLambda(options: DeployLambdaOptions): Promise<void>;
44
33
  export declare function cloudFormationTemplate(): Promise<string>;
package/dist/index.js CHANGED
@@ -2062,34 +2062,6 @@ async function publishClientAssets(options) {
2062
2062
  ]);
2063
2063
  return assetPath;
2064
2064
  }
2065
- async function deployLambda(options) {
2066
- const artifactDir = resolve2(options.artifactDir);
2067
- const assetsDir = resolve2(options.assetsDir);
2068
- await readFile3(join2(artifactDir, "lambda.zip"));
2069
- await readFile3(join2(assetsDir, "index.html"));
2070
- if (options.dryRun) return;
2071
- const assetPath = await publishClientAssets(options);
2072
- await run("aws", [
2073
- "lambda",
2074
- "update-function-code",
2075
- "--function-name",
2076
- options.functionName,
2077
- "--zip-file",
2078
- `fileb://${join2(artifactDir, "lambda.zip")}`,
2079
- "--no-cli-pager"
2080
- ]);
2081
- await run("aws", ["lambda", "wait", "function-updated", "--function-name", options.functionName]);
2082
- await run("aws", [
2083
- "lambda",
2084
- "update-function-configuration",
2085
- "--function-name",
2086
- options.functionName,
2087
- "--environment",
2088
- `Variables={ASSET_PATH=${assetPath},BOARD_CONFIG_URL=./board.yaml,HOST=0.0.0.0}`,
2089
- "--no-cli-pager"
2090
- ]);
2091
- await run("aws", ["lambda", "wait", "function-updated", "--function-name", options.functionName]);
2092
- }
2093
2065
  async function cloudFormationTemplate() {
2094
2066
  return readFile3(fileURLToPath2(new URL("../template.yml", import.meta.url)), "utf8");
2095
2067
  }
@@ -2106,7 +2078,6 @@ export {
2106
2078
  checkBootstrap,
2107
2079
  cloudFormationTemplate,
2108
2080
  coreBootstrapOutputs,
2109
- deployLambda,
2110
2081
  deployedBootstrapStack,
2111
2082
  formatBootstrapCheckText,
2112
2083
  githubOidcProvider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@continuous-excellence/ze-great-dashboard-aws",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "AWS Lambda and CloudFormation adapter for Ze Great Dashboard.",
6
6
  "keywords": [