@kungfu-tech/buildchain 2.3.1 → 2.4.0-alpha.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.
@@ -9,7 +9,7 @@ const RESERVED_LIFECYCLE_KEYS = new Set(["env", "shell"]);
9
9
  const SUPPORTED_VERSION_FILE_TYPES = new Set(["json", "toml", "regex"]);
10
10
  const SUPPORTED_VERSION_STRATEGIES = new Set(["semver", "anchored"]);
11
11
  const SUPPORTED_VERSION_NEXT = new Set(["auto", "manual"]);
12
- const SUPPORTED_PROJECT_TYPES = new Set(["package", "web-surface"]);
12
+ const SUPPORTED_PROJECT_TYPES = new Set(["package", "web-surface", "infra-contract"]);
13
13
  const SUPPORTED_PUBLISH_MODES = new Set(["publish-final-version", "promote-existing-version"]);
14
14
  const SUPPORTED_PUBLISH_AUTH = new Set(["trusted-publishing", "npm-token"]);
15
15
  const SUPPORTED_PACKAGE_SET_ORDER = new Set(["as-provided", "platforms-first-main-last"]);
@@ -23,6 +23,25 @@ const SUPPORTED_DEPLOY_ADAPTERS = new Set([
23
23
  "aws-elastic-beanstalk",
24
24
  "aws-ecs-service",
25
25
  ]);
26
+ const SUPPORTED_INFRA_ADAPTERS = new Set([
27
+ "manual-observed",
28
+ "aws-cloudformation",
29
+ "terraform",
30
+ "opentofu",
31
+ "pulumi",
32
+ "aws-cdk",
33
+ "aws-cli",
34
+ "custom-command",
35
+ ]);
36
+ const SUPPORTED_INFRA_ADOPTION_MODES = new Set([
37
+ "validate-only",
38
+ "plan-only",
39
+ "observe-only",
40
+ "manual-observed",
41
+ "import-planned",
42
+ "managed-apply",
43
+ ]);
44
+ const SUPPORTED_INFRA_APPLY_MODES = new Set(["disabled", "manual-approval", "environment-approval"]);
26
45
 
27
46
  function posixPath(value) {
28
47
  return String(value || "").split(path.sep).join("/");
@@ -117,6 +136,12 @@ export function normalizeBuildchainConfig(config) {
117
136
  if (normalized.deploy !== undefined) {
118
137
  normalized.deploy = normalizeDeploySection(normalized.deploy, normalized.project);
119
138
  }
139
+ if (normalized.infra !== undefined) {
140
+ normalized.infra = normalizeInfraSection(normalized.infra);
141
+ }
142
+ if (normalized.consumers !== undefined) {
143
+ normalized.consumers = normalizeConsumersSection(normalized.consumers);
144
+ }
120
145
  if (normalized.surfaces !== undefined) {
121
146
  normalized.surfaces = normalizeSurfacesSection(normalized.surfaces);
122
147
  }
@@ -130,6 +155,7 @@ export function normalizeBuildchainConfig(config) {
130
155
  normalized.diagnostics = normalizeDiagnosticsSection(normalized.diagnostics);
131
156
  }
132
157
  validateWebSurfaceConfig(normalized);
158
+ validateInfraContractConfig(normalized);
133
159
  return normalized;
134
160
  }
135
161
 
@@ -211,7 +237,7 @@ function normalizeProjectSection(project) {
211
237
  assertPlainObject(project, "project");
212
238
  const type = assertString(project.type, "project.type");
213
239
  if (!SUPPORTED_PROJECT_TYPES.has(type)) {
214
- throw new Error("project.type must be one of package or web-surface");
240
+ throw new Error("project.type must be one of package, web-surface, or infra-contract");
215
241
  }
216
242
  const normalized = { type };
217
243
  for (const key of ["name", "site"]) {
@@ -222,6 +248,73 @@ function normalizeProjectSection(project) {
222
248
  return normalized;
223
249
  }
224
250
 
251
+ function normalizeInfraSection(infra) {
252
+ assertPlainObject(infra, "infra");
253
+ const adapter = assertString(infra.adapter, "infra.adapter");
254
+ if (!SUPPORTED_INFRA_ADAPTERS.has(adapter)) {
255
+ throw new Error(
256
+ "infra.adapter must be one of manual-observed, aws-cloudformation, terraform, opentofu, pulumi, aws-cdk, aws-cli, or custom-command",
257
+ );
258
+ }
259
+ const adoptionMode = infra.adoption_mode === undefined
260
+ ? (adapter === "manual-observed" ? "manual-observed" : "validate-only")
261
+ : assertString(infra.adoption_mode, "infra.adoption_mode");
262
+ if (!SUPPORTED_INFRA_ADOPTION_MODES.has(adoptionMode)) {
263
+ throw new Error(
264
+ "infra.adoption_mode must be one of validate-only, plan-only, observe-only, manual-observed, import-planned, or managed-apply",
265
+ );
266
+ }
267
+ const applyMode = infra.apply === undefined
268
+ ? "disabled"
269
+ : assertString(infra.apply, "infra.apply");
270
+ if (!SUPPORTED_INFRA_APPLY_MODES.has(applyMode)) {
271
+ throw new Error("infra.apply must be one of disabled, manual-approval, or environment-approval");
272
+ }
273
+ const normalized = {
274
+ adapter,
275
+ adoptionMode,
276
+ applyMode,
277
+ environment: infra.environment === undefined ? "" : assertString(infra.environment, "infra.environment"),
278
+ identityRef: infra.identity_ref === undefined ? "" : assertString(infra.identity_ref, "infra.identity_ref"),
279
+ desired: normalizeStringArray(infra.desired, "infra.desired").map(posixPath),
280
+ contract: normalizeStringArray(infra.contract, "infra.contract").map(posixPath),
281
+ secretRefs: normalizeStringArray(infra.secret_refs, "infra.secret_refs"),
282
+ };
283
+ if (infra.commands !== undefined) {
284
+ normalized.commands = normalizeInfraCommands(infra.commands);
285
+ }
286
+ assertNoInlineSecretValues(infra, "infra", new Set(["secret_refs", "commands"]));
287
+ return normalized;
288
+ }
289
+
290
+ function normalizeInfraCommands(commands) {
291
+ assertPlainObject(commands, "infra.commands");
292
+ const normalized = {};
293
+ for (const stage of ["validate", "plan", "apply", "observe"]) {
294
+ if (commands[stage] !== undefined) {
295
+ normalized[stage] = assertString(commands[stage], `infra.commands.${stage}`);
296
+ }
297
+ }
298
+ return normalized;
299
+ }
300
+
301
+ function normalizeConsumersSection(consumers) {
302
+ if (!Array.isArray(consumers)) {
303
+ throw new Error("consumers must be an array of tables");
304
+ }
305
+ return consumers.map((consumer, index) => normalizeConsumerConfig(consumer, index));
306
+ }
307
+
308
+ function normalizeConsumerConfig(consumer, index) {
309
+ assertPlainObject(consumer, `consumers[${index}]`);
310
+ return {
311
+ repo: assertString(consumer.repo, `consumers[${index}].repo`),
312
+ path: posixPath(assertString(consumer.path, `consumers[${index}].path`)),
313
+ source: posixPath(assertString(consumer.source, `consumers[${index}].source`)),
314
+ branch: consumer.branch === undefined ? "" : assertString(consumer.branch, `consumers[${index}].branch`),
315
+ };
316
+ }
317
+
225
318
  function normalizeChannelsSection(channels) {
226
319
  assertPlainObject(channels, "channels");
227
320
  return Object.fromEntries(
@@ -531,6 +624,36 @@ function validateWebSurfaceConfig(config) {
531
624
  validateWebSurfaceSurfaces(config);
532
625
  }
533
626
 
627
+ function validateInfraContractConfig(config) {
628
+ if (config.project?.type !== "infra-contract") {
629
+ return;
630
+ }
631
+ if (!config.infra) {
632
+ throw new Error('project.type = "infra-contract" requires [infra]');
633
+ }
634
+ if (config.infra.desired.length === 0) {
635
+ throw new Error('project.type = "infra-contract" requires infra.desired');
636
+ }
637
+ if (config.infra.contract.length === 0) {
638
+ throw new Error('project.type = "infra-contract" requires infra.contract');
639
+ }
640
+ if (!Array.isArray(config.consumers) || config.consumers.length === 0) {
641
+ throw new Error('project.type = "infra-contract" requires at least one [[consumers]] entry');
642
+ }
643
+ if (config.infra.applyMode !== "disabled" && config.infra.adoptionMode !== "managed-apply") {
644
+ throw new Error("infra.apply requires infra.adoption_mode = managed-apply");
645
+ }
646
+ if (config.infra.applyMode !== "disabled" && !config.infra.environment) {
647
+ throw new Error("infra.apply requires infra.environment");
648
+ }
649
+ if (config.infra.applyMode !== "disabled" && !config.infra.identityRef) {
650
+ throw new Error("infra.apply requires infra.identity_ref");
651
+ }
652
+ if (config.infra.adapter === "manual-observed" && config.infra.applyMode !== "disabled") {
653
+ throw new Error('infra.adapter = "manual-observed" requires infra.apply = "disabled"');
654
+ }
655
+ }
656
+
534
657
  function validateWebSurfaceSurfaces(config) {
535
658
  const surfaces = config.surfaces || {};
536
659
  for (const [name, surface] of Object.entries(surfaces)) {
@@ -891,6 +1014,20 @@ export function validateBuildchainConfig(
891
1014
  ]),
892
1015
  )
893
1016
  : undefined,
1017
+ infra: loadedConfig.config.infra
1018
+ ? {
1019
+ adapter: loadedConfig.config.infra.adapter,
1020
+ adoptionMode: loadedConfig.config.infra.adoptionMode,
1021
+ applyMode: loadedConfig.config.infra.applyMode,
1022
+ environment: loadedConfig.config.infra.environment,
1023
+ identityRef: loadedConfig.config.infra.identityRef,
1024
+ desired: loadedConfig.config.infra.desired,
1025
+ contract: loadedConfig.config.infra.contract,
1026
+ secretRefs: loadedConfig.config.infra.secretRefs,
1027
+ commands: loadedConfig.config.infra.commands,
1028
+ }
1029
+ : undefined,
1030
+ consumers: loadedConfig.config.consumers,
894
1031
  surfaces: loadedConfig.config.surfaces
895
1032
  ? Object.fromEntries(
896
1033
  Object.entries(loadedConfig.config.surfaces).map(([name, surface]) => [
@@ -52,6 +52,7 @@ function buildSiteBundle() {
52
52
  docEntry("release-flow", "Release flow", "docs/release-flow.md", "verify"),
53
53
  docEntry("versioning", "Versioning", "docs/versioning.md", "why"),
54
54
  docEntry("web-surface-deployments", "Web surface deployments", "docs/web-surface-deployments.md", "use"),
55
+ docEntry("infra-contract", "Infra Contract", "docs/infra-contract.md", "use"),
55
56
  ];
56
57
 
57
58
  const cliRegistry = {
@@ -61,16 +62,18 @@ function buildSiteBundle() {
61
62
  npmPackage: packageJson.name,
62
63
  commands: [
63
64
  { id: "version", usage: "buildchain version", purpose: "Print the package or embedded binary version." },
64
- { id: "init", usage: "buildchain init [--type package|native|web-surface|anchored-package]", purpose: "Bootstrap a repository with Buildchain configuration and caller workflow files." },
65
+ { id: "init", usage: "buildchain init [--type package|native|web-surface|infra-contract|anchored-package]", purpose: "Bootstrap a repository with Buildchain configuration and caller workflow files." },
65
66
  { id: "validate", usage: "buildchain validate [--require-version-state]", purpose: "Validate buildchain.toml and declared lifecycle surfaces." },
66
67
  { id: "doctor", usage: "buildchain doctor [--json]", purpose: "Report local integration readiness." },
67
68
  { id: "lifecycle", usage: "buildchain lifecycle run <stage>", purpose: "Run configured lifecycle commands and write deterministic artifact manifests." },
68
69
  { id: "release-dry-run", usage: "buildchain release --dry-run --target-ref <ref>", purpose: "Explain what a channel merge would publish before the PR is merged." },
69
70
  { id: "collect-github-release", usage: "buildchain collect github-release --tag <tag>", purpose: "Collect release assets into a release passport." },
70
71
  { id: "verify-release-passport", usage: "buildchain verify release-passport <file-or-url>", purpose: "Fail closed unless a release passport and its evidence are complete." },
72
+ { id: "verify-infra-contract-evidence-bundle", usage: "buildchain verify infra-contract-evidence-bundle <file>", purpose: "Fail closed unless an infra-contract lifecycle evidence bundle is complete, hash-bound, and validation-consistent." },
71
73
  { id: "logging", usage: "buildchain log|mark|span|verify observability-log", purpose: "Emit timestamped build events, summarize logs, and enforce required phases." },
72
74
  { id: "diagnostics-summary", usage: "buildchain diagnostics summary <diagnostics.json>...", purpose: "Summarize small diagnostics artifacts into JSON and a cross-platform lifecycle timing table." },
73
75
  { id: "npm-dry-run", usage: "buildchain npm dry-run --json", purpose: "Verify npm publish shape before a release transaction." },
76
+ { id: "infra-contract", usage: "buildchain infra-contract --mode validate|ci|plan|contract|propagation-plan|propagation-apply|apply|evidence-bundle", purpose: "Validate and publish provider-neutral infrastructure contract evidence with a mutation-free CI evidence chain, provider command plans, configured provider command execution, saved-plan apply gates, dry-run-first propagation, and lifecycle evidence bundles." },
74
77
  ],
75
78
  };
76
79
 
@@ -135,6 +138,11 @@ function buildSiteBundle() {
135
138
  "release-provenance.json",
136
139
  "agent-index.json",
137
140
  ],
141
+ infraContract: [
142
+ "infra-contract-plan.json",
143
+ "buildchain.infra-contract.json",
144
+ "infra-contract-propagation.json",
145
+ ],
138
146
  };
139
147
 
140
148
  const productMechanism = {