@kungfu-tech/buildchain 4.1.3-alpha.2 → 4.1.3-alpha.4

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.
Files changed (80) hide show
  1. package/architecture/action-taxonomy.json +8 -0
  2. package/architecture/agent-change-map.md +23 -0
  3. package/architecture/ci-lane-change-budget.json +85 -0
  4. package/architecture/decisions/0009-pipeline-version-preparation.md +186 -0
  5. package/architecture/internal-capabilities.json +32 -2
  6. package/architecture/maintainability-debt.json +10 -4
  7. package/architecture/maintainability-policy.json +7 -7
  8. package/architecture/minimal-consumer-migration.json +9 -0
  9. package/architecture/product-upstreams.json +20 -0
  10. package/architecture/release-topology.json +63 -25
  11. package/architecture/universal-workflow-bootstrap.json +2 -0
  12. package/architecture/universal-workflow-capability-policy.json +1 -1
  13. package/architecture/workflow-taxonomy.json +11 -0
  14. package/dist/readers/business-attempt.cjs +6 -2
  15. package/dist/site/buildchain-contract.json +4 -4
  16. package/dist/site/buildchain-site.json +9 -9
  17. package/dist/site/capability-registry.json +2 -2
  18. package/dist/site/kfd-claims.json +82 -8
  19. package/dist/site/kfd-upstream-aggregate.json +1 -1
  20. package/dist/site/manual-registry.json +1 -1
  21. package/dist/site/node-api-registry.json +18 -18
  22. package/dist/site/page-registry.json +4 -4
  23. package/dist/site/public-surface-audit.json +64 -7
  24. package/dist/site/publication-authority-registry.json +21 -1
  25. package/dist/site/publication-registry.json +4 -4
  26. package/dist/site/site-manifest.json +5 -5
  27. package/dist/site/workflow-registry.json +113 -7
  28. package/docs/node-api-reference.md +34 -34
  29. package/docs/workflow-catalog.md +1 -0
  30. package/package.json +1 -1
  31. package/packages/core/build/standalone/build.js +7 -1
  32. package/packages/core/consumer/buildchain-config.js +2 -21
  33. package/packages/core/consumer/contract/plan.js +53 -3
  34. package/packages/core/consumer/contract/products.js +20 -2
  35. package/packages/core/consumer/contract/reader.js +47 -0
  36. package/packages/core/dev-delivery/commands/project-cut-merge-queue-admission.mjs +47 -20
  37. package/packages/core/governance/buildchain-publication-authority.js +1 -0
  38. package/packages/core/providers/github/discussions/materials.js +12 -4
  39. package/packages/core/providers/github/pipeline-entry-runs.js +203 -0
  40. package/packages/core/providers/github/pipeline-policy.js +44 -17
  41. package/packages/core/providers/github/pipeline-release-evidence.js +111 -0
  42. package/packages/core/providers/github/pipeline-stable-source.js +354 -0
  43. package/packages/core/providers/github/pipeline-version-artifacts.js +91 -0
  44. package/packages/core/providers/github/pipeline-version-readback.js +117 -0
  45. package/packages/core/providers/github/pipeline-version.js +10 -12
  46. package/packages/core/publication/pipeline/actions.js +17 -1
  47. package/packages/core/publication/pipeline/apply.js +23 -17
  48. package/packages/core/publication/pipeline/development-pr.js +28 -1
  49. package/packages/core/publication/pipeline/documents.js +37 -1
  50. package/packages/core/publication/pipeline/next-development.js +1 -0
  51. package/packages/core/publication/pipeline/pack.js +7 -1
  52. package/packages/core/publication/pipeline/plan.js +22 -0
  53. package/packages/core/publication/pipeline/prepare.js +44 -56
  54. package/packages/core/publication/pipeline/qualification.js +5 -0
  55. package/packages/core/publication/pipeline/stable-entry.js +215 -0
  56. package/packages/core/publication/pipeline/stable-products.js +317 -0
  57. package/packages/core/publication/pipeline/stable-wait.js +170 -0
  58. package/packages/core/publication/pipeline/stable.js +82 -0
  59. package/packages/core/publication/pipeline/version-actions.js +93 -0
  60. package/packages/core/publication/pipeline/version-build.js +130 -0
  61. package/packages/core/publication/pipeline/version-context.js +143 -0
  62. package/packages/core/publication/pipeline/version-material.js +36 -0
  63. package/packages/core/publication/pipeline/version-preparation.js +69 -0
  64. package/packages/core/publication/pipeline/version-regeneration.js +162 -0
  65. package/packages/core/publication/pipeline/version-source-guard.js +69 -0
  66. package/packages/core/publication/pipeline/worker.js +72 -0
  67. package/packages/core/release/stable-release-gate.js +11 -0
  68. package/packages/core/workflow/attempt/identity.js +7 -2
  69. package/packages/core/workflow/pipeline/actions.js +14 -1
  70. package/packages/core/workflow/pipeline/build-result.js +8 -0
  71. package/packages/core/workflow/pipeline/build.js +49 -2
  72. package/packages/core/workflow/pipeline/channel-control.js +10 -1
  73. package/packages/core/workflow/pipeline/channel-source.js +53 -0
  74. package/packages/core/workflow/pipeline/group-control.js +2 -0
  75. package/packages/core/workflow/pipeline/host.js +24 -16
  76. package/packages/core/workflow/pipeline/selection.js +9 -4
  77. package/scripts/check-pipeline-publication-topology.mjs +12 -0
  78. package/scripts/check-release-topology.mjs +1 -0
  79. package/scripts/generate-site-bundle.mjs +1 -1
  80. package/scripts/verify-standalone-product.mjs +42 -0
@@ -184,8 +184,19 @@ export function evaluateStableReleaseGate({
184
184
  "version-bound impact declares a summary and at least one surface",
185
185
  { summary: string(impact?.summary), surfaceIds: surfaceImpacts.map((entry) => string(entry?.id)).filter(Boolean) },
186
186
  ));
187
+ checks.push(check(
188
+ string(impact?.release?.version) === candidateTag.replace(/^v/, ""),
189
+ "stable.impact_version",
190
+ "product impact belongs to the exact candidate version",
191
+ { expectedVersion: candidateTag.replace(/^v/, ""), observedVersion: string(impact?.release?.version) },
192
+ ));
187
193
 
188
194
  const canaryById = new Map(canaries.map((canary) => [string(canary?.id), canary]));
195
+ checks.push(check(
196
+ canaryById.size === canaries.length,
197
+ "canary.evidence_inventory",
198
+ "each canary has one unambiguous observation",
199
+ ));
189
200
  const canaryCompletionTimes = [];
190
201
  for (const required of policy.requiredCanaries) {
191
202
  const evidence = canaryById.get(required.id);
@@ -67,7 +67,7 @@ export function pipelineIntent({
67
67
  });
68
68
  }
69
69
 
70
- export function sourceGeneration(intent, source, baseCommit) {
70
+ export function validateConsumerSource(source, repository) {
71
71
  object(
72
72
  source,
73
73
  [
@@ -86,13 +86,18 @@ export function sourceGeneration(intent, source, baseCommit) {
86
86
  if (
87
87
  source.schema !== "buildchain.consumer-source/v1" ||
88
88
  source.contract !== CONSUMER_CONTRACT ||
89
- source.repository !== intent.repository
89
+ source.repository !== repository
90
90
  )
91
91
  throw new Error("Generation requires the admitted consumer source");
92
92
  for (const key of ["commit", "tree", "configBlob"])
93
93
  text(source[key], `source.${key}`, /^[0-9a-f]{40}$/u);
94
94
  relativePath(source.configPath, "source.configPath");
95
95
  text(source.configDigest, "source.configDigest", ROOT);
96
+ return source;
97
+ }
98
+
99
+ export function sourceGeneration(intent, source, baseCommit) {
100
+ validateConsumerSource(source, intent.repository);
96
101
  text(baseCommit, "baseCommit", /^[0-9a-f]{40}$/u);
97
102
  const value = {
98
103
  schema: SOURCE_GENERATION,
@@ -6,6 +6,7 @@ import { pipelineHost } from "./host.js";
6
6
  import { controlPipeline } from "./controller.js";
7
7
  import { buildPipelineSource } from "./build.js";
8
8
  import { recordPipelineBuild } from "./build-control.js";
9
+ import { wakePipelineStableWait } from "../../publication/pipeline/stable-wait.js";
9
10
  import {
10
11
  RECOVERY_BUILD_CONTEXT,
11
12
  recordRecoveryBuild,
@@ -57,6 +58,18 @@ export async function recordPipelineBuildAction(core, env) {
57
58
  }
58
59
 
59
60
  export async function wakePipelineAction(core, env) {
60
- const host = await pipelineHost(core, env, "Record delivery attempt");
61
+ const waiting = core.getInput("stable-wait");
62
+ const host = await pipelineHost(
63
+ core,
64
+ env,
65
+ waiting ? "Recheck stable publication" : "Record delivery attempt",
66
+ );
67
+ if (waiting) {
68
+ const wait = JSON.parse(waiting);
69
+ if (wait.attempt !== core.getInput("attempt", { required: true }))
70
+ throw new Error("Stable wake changed its exact business attempt");
71
+ core.info(JSON.stringify(await wakePipelineStableWait(wait, host)));
72
+ return;
73
+ }
61
74
  await host.wake(core.getInput("attempt", { required: true }));
62
75
  }
@@ -37,6 +37,14 @@ export async function retainPipelineBuildResult(
37
37
  session,
38
38
  host,
39
39
  ) {
40
+ if (
41
+ readback.outcome === "success" &&
42
+ session.intent.expectedNodes.includes("publish")
43
+ )
44
+ await host.qualifyChannel(
45
+ context.source,
46
+ session.intent.source.targetBranch,
47
+ );
40
48
  const reference = await resultReference(context, readback, session, host);
41
49
  await session.progress.progress({
42
50
  attempt: context.attempt,
@@ -1,12 +1,57 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { execFileSync } from "node:child_process";
4
+ import { createHash } from "node:crypto";
4
5
  import { bindConsumerSource } from "../../consumer/contract/identity.js";
5
6
  import { recordDigest } from "../../release/discussion/envelope.js";
6
7
  import { consumerCommandSession } from "../../runtime/consumer-shell.js";
7
8
  import { createNativeChildEnvironment } from "../../dev-delivery/native/execution.js";
8
9
 
9
- function sourceCheckout(cwd, expected) {
10
+ function inspectTrackedBytes(cwd, tree) {
11
+ const root = fs.realpathSync(cwd);
12
+ const entries = execFileSync(
13
+ "git",
14
+ ["-C", root, "ls-tree", "-r", "-z", tree],
15
+ {
16
+ encoding: "utf8",
17
+ maxBuffer: 16 * 1024 * 1024,
18
+ },
19
+ )
20
+ .split("\0")
21
+ .filter(Boolean);
22
+ for (const entry of entries) {
23
+ const match =
24
+ /^(100644|100755|120000) blob ([0-9a-f]{40})\t([\s\S]+)$/u.exec(entry);
25
+ if (!match) throw new Error("Build source requires resolved tracked files");
26
+ const [, mode, blob, file] = match;
27
+ const absolute = path.join(root, file);
28
+ if (fs.realpathSync(path.dirname(absolute)) !== path.dirname(absolute))
29
+ throw new Error(`Build source parent became a symbolic link: ${file}`);
30
+ const stat = fs.lstatSync(absolute);
31
+ const link = mode === "120000";
32
+ if (link ? !stat.isSymbolicLink() : !stat.isFile())
33
+ throw new Error(`Build tracked source kind changed: ${file}`);
34
+ if (
35
+ !link &&
36
+ process.platform !== "win32" &&
37
+ Boolean(stat.mode & 0o111) !== (mode === "100755")
38
+ )
39
+ throw new Error(`Build tracked source executable mode changed: ${file}`);
40
+ const bytes = link
41
+ ? fs.readlinkSync(absolute, { encoding: "buffer" })
42
+ : fs.readFileSync(absolute);
43
+ const actual = createHash("sha1")
44
+ .update(`blob ${bytes.length}\0`)
45
+ .update(bytes)
46
+ .digest("hex");
47
+ if (actual !== blob)
48
+ throw new Error(
49
+ `Build tracked source bytes differ from the admitted tree: ${file}`,
50
+ );
51
+ }
52
+ }
53
+
54
+ export function inspectPipelineSource(cwd, expected) {
10
55
  const git = (...args) =>
11
56
  execFileSync("git", ["-C", cwd, ...args], {
12
57
  encoding: "utf8",
@@ -36,6 +81,8 @@ function sourceCheckout(cwd, expected) {
36
81
  throw new Error(
37
82
  "Build source has tracked modifications at its verification boundary",
38
83
  );
84
+ // Index flags and local Git filters cannot qualify different product bytes.
85
+ inspectTrackedBytes(cwd, expected.tree);
39
86
  return plan;
40
87
  }
41
88
 
@@ -54,7 +101,7 @@ function productDirectory(cwd, product) {
54
101
  // jobs must bind provider completion and exact artifacts before granting reuse.
55
102
  export async function buildPipelineSource(
56
103
  { cwd, source, platform, environment = process.env },
57
- { inspect = sourceCheckout, session = consumerCommandSession } = {},
104
+ { inspect = inspectPipelineSource, session = consumerCommandSession } = {},
58
105
  ) {
59
106
  const plan = inspect(cwd, source);
60
107
  const result = await buildPipelineProducts(
@@ -68,6 +68,10 @@ export async function controlPipelineChannel(session, admission, inputs, host) {
68
68
  }
69
69
  if (host.terminalOnly)
70
70
  return { operation: "wait", reason: "terminal-event-cannot-execute" };
71
+ const qualification = await host.qualifyChannel(
72
+ live.source,
73
+ live.targetBranch,
74
+ );
71
75
  const build = await pipelineBuildEvidence(session, host);
72
76
  if (!build) return { operation: "build", admission };
73
77
  if (build.run.status !== "completed" || build.run.conclusion !== "success")
@@ -97,7 +101,12 @@ export async function controlPipelineChannel(session, admission, inputs, host) {
97
101
  session,
98
102
  "merge",
99
103
  "waiting",
100
- { schema: "buildchain.pipeline-channel-enqueue/v1", policy, queue },
104
+ {
105
+ schema: "buildchain.pipeline-channel-enqueue/v1",
106
+ qualification,
107
+ policy,
108
+ queue,
109
+ },
101
110
  host,
102
111
  );
103
112
  const again = await host.source.observeIntent(
@@ -0,0 +1,53 @@
1
+ import { recordDigest } from "../../release/discussion/envelope.js";
2
+ import { selectProductPublicationIntent } from "../../release/product-publication.js";
3
+ import { pipelineExpectedProducts } from "../../publication/pipeline/plan.js";
4
+ import { githubPipelineVersion } from "../../providers/github/pipeline-version.js";
5
+
6
+ // This read-only qualification must precede both required checks and queue
7
+ // admission. Publication repeats the Rust-owned decision after protected merge.
8
+ export async function qualifyPipelineChannelSource(source, branch, host) {
9
+ if (branch.startsWith("dev/")) return null;
10
+ const admitted = await host.source.source(source.commit, source.configPath);
11
+ if (recordDigest(admitted.identity) !== recordDigest(source))
12
+ throw new Error("Channel qualification source identity drift");
13
+ const { plan } = admitted;
14
+ const routes = plan.channels.filter(
15
+ (route) => route.to === branch && route.operation !== "develop",
16
+ );
17
+ if (routes.length !== 1)
18
+ throw new Error("Channel qualification requires one publication route");
19
+ const route = routes[0];
20
+ const from = `${route.operation === "alpha" ? "dev" : "alpha"}/${branch.split("/").slice(1).join("/")}`;
21
+ if (["alpha", "stable"].includes(route.operation) && route.from !== from)
22
+ throw new Error("Channel source and target version lines must match");
23
+ const version = await githubPipelineVersion(
24
+ host.request,
25
+ host.repository,
26
+ ).inspect(source, plan.version);
27
+ const selection = selectProductPublicationIntent({
28
+ channel: route.operation === "alpha" ? "alpha" : "stable",
29
+ targetRef: branch,
30
+ sourceSha: source.commit,
31
+ sourceTimestamp: version.sourceTimestamp,
32
+ repository: source.repository,
33
+ artifactKind: "custom",
34
+ requiredArtifactsRoot: recordDigest(pipelineExpectedProducts(plan)),
35
+ candidateVersion: version.version,
36
+ observedVersions: [],
37
+ });
38
+ if (
39
+ plan.version.strategy === "anchored" &&
40
+ selection.version !== version.version
41
+ )
42
+ throw new Error("Anchored channel source requires a materialized version");
43
+ const body = {
44
+ schema: "buildchain.pipeline-channel-source-qualification/v1",
45
+ source,
46
+ route,
47
+ contractRoot: recordDigest(plan),
48
+ version: version.version,
49
+ versionFilesRoot: recordDigest(version.files),
50
+ selection,
51
+ };
52
+ return { ...body, root: recordDigest(body) };
53
+ }
@@ -36,6 +36,8 @@ export async function recordPipelineGroup(context, host) {
36
36
  context.source,
37
37
  context.platforms.map((platform) => platform.platform),
38
38
  );
39
+ if (readback.outcome === "success")
40
+ await host.qualifyChannel(context.source, context.branch);
39
41
  await host.request(`/repos/${host.repository}/check-runs`, {
40
42
  method: "POST",
41
43
  body: {
@@ -17,6 +17,7 @@ import { GitHubClient } from "../../dev-delivery/admission/github-client.js";
17
17
  import { githubPipelineIntegration } from "../../providers/github/pipeline-integration.js";
18
18
  import { recordPipelineGroup } from "./group-control.js";
19
19
  import { controlPipelineChannel } from "./channel-control.js";
20
+ import { qualifyPipelineChannelSource } from "./channel-source.js";
20
21
  import { settlePipeline } from "./settlement.js";
21
22
  import { pipelineProjection } from "./projection.js";
22
23
  import { resumePipelineNotifications } from "./notifications.js";
@@ -48,6 +49,24 @@ export async function pipelineHost(core, env, jobName) {
48
49
  const writer = await runs.writer(runId, runAttempt, jobName);
49
50
  const source = githubPipelineSource(request, repository);
50
51
  const [owner, repo] = repository.split("/");
52
+ const archives = new Map();
53
+ function archive(session) {
54
+ if (!archives.has(session.intent.id))
55
+ archives.set(
56
+ session.intent.id,
57
+ discussionMaterials({
58
+ octokit: github,
59
+ repository,
60
+ intentId: session.intent.id,
61
+ // Storage is partitioned by verified provider job, not attempt authority.
62
+ // Journal references still select exact immutable asset IDs and digests.
63
+ partition: `${writer.runId}-${writer.runAttempt}-${writer.jobId}`,
64
+ authorityDescription:
65
+ "The canonical Buildchain intent Git journal owns transaction state.",
66
+ }),
67
+ );
68
+ return archives.get(session.intent.id);
69
+ }
51
70
  const host = {
52
71
  repository,
53
72
  token,
@@ -70,6 +89,8 @@ export async function pipelineHost(core, env, jobName) {
70
89
  groupRecord: (context) => recordPipelineGroup(context, host),
71
90
  channel: (session, admission, inputs) =>
72
91
  controlPipelineChannel(session, admission, inputs, host),
92
+ qualifyChannel: (source, branch) =>
93
+ qualifyPipelineChannelSource(source, branch, host),
73
94
  settle: (session, fresh, delivery) =>
74
95
  settlePipeline(session, fresh, delivery, host),
75
96
  project: (session) =>
@@ -81,24 +102,11 @@ export async function pipelineHost(core, env, jobName) {
81
102
  recoverPublication: (session, admitted) =>
82
103
  inspectRecoveryPublication(session, admitted, host),
83
104
  materialStore: (session) =>
84
- pipelineMaterials(
85
- discussionMaterials({
86
- octokit: github,
87
- repository,
88
- intentId: session.intent.id,
89
- authorityDescription:
90
- "The canonical Buildchain intent Git journal owns transaction state.",
91
- }),
92
- { repository, attempt: session.observed.history.at(-1).identity },
93
- ),
94
- productArchive: (session) =>
95
- discussionMaterials({
96
- octokit: github,
105
+ pipelineMaterials(archive(session), {
97
106
  repository,
98
- intentId: session.intent.id,
99
- authorityDescription:
100
- "The canonical Buildchain intent Git journal owns transaction state.",
107
+ attempt: session.observed.history.at(-1).identity,
101
108
  }),
109
+ productArchive: archive,
102
110
  wake: async (attempt) => {
103
111
  await index.resolve(attempt);
104
112
  await request(`/repos/${repository}/dispatches`, {
@@ -10,8 +10,12 @@ async function successorSelection(
10
10
  inputs,
11
11
  host,
12
12
  ) {
13
+ const failedBuild =
14
+ session.observed.status === "failure" &&
15
+ session.observed.phases.build?.payload.state === "failure";
13
16
  if (
14
- !["superseded", "cancelled"].includes(session.observed.status) ||
17
+ (!failedBuild &&
18
+ !["superseded", "cancelled"].includes(session.observed.status)) ||
15
19
  event.terminalOnly ||
16
20
  admission.live.state !== "open" ||
17
21
  admission.live.targetBranch !== session.intent.source.targetBranch
@@ -27,9 +31,10 @@ async function successorSelection(
27
31
  .id !== session.observed.generation;
28
32
  if (
29
33
  !changed &&
30
- !["reopened", "enqueued", "ready_for_review", "labeled"].includes(
31
- payload.action,
32
- )
34
+ (failedBuild ||
35
+ !["reopened", "enqueued", "ready_for_review", "labeled"].includes(
36
+ payload.action,
37
+ ))
33
38
  )
34
39
  return { session, admission };
35
40
  await host.notifyTerminal?.(session);
@@ -12,6 +12,7 @@ export function assertPipelinePublicationTopology(
12
12
  ".github/workflows/public-ops-pipeline.yml",
13
13
  ".github/workflows/public-ops-recover.yml",
14
14
  ".github/workflows/.ops-pipeline-execute.yml",
15
+ ".github/workflows/.release-pipeline-version.yml",
15
16
  ]);
16
17
  const pipelineTopology = discoverReleaseTopology(
17
18
  pipeline.workflowPaths,
@@ -33,4 +34,15 @@ export function assertPipelinePublicationTopology(
33
34
  module,
34
35
  ),
35
36
  );
37
+ const version = ".github/workflows/.release-pipeline-version.yml";
38
+ assert.ok(
39
+ inspectWorkflowJob(version, "build", root).modules.has(
40
+ "packages/core/publication/pipeline/version-build.js",
41
+ ),
42
+ );
43
+ assert.ok(
44
+ inspectWorkflowJob(version, "materialize", root).modules.has(
45
+ "packages/core/publication/pipeline/version-context.js",
46
+ ),
47
+ );
36
48
  }
@@ -30,6 +30,7 @@ const PRIVILEGED_ENTRYPOINTS = [
30
30
  "packages/core/publication/oci/preview-actions.js",
31
31
  "packages/core/publication/settlement/actions.js",
32
32
  "packages/core/publication/pipeline/actions.js",
33
+ "packages/core/publication/pipeline/version-actions.js",
33
34
  ];
34
35
 
35
36
  export function localModuleSpecifiers(source) {
@@ -1343,7 +1343,7 @@ function buildSiteBundle() {
1343
1343
  "badge-endpoint-registry.json": badgeEndpointRegistry,
1344
1344
  "publication-registry.json": publicationRegistry,
1345
1345
  "buildchain-contract.json": createBuildchainContractWorld({ root, controllerRegistry }),
1346
- "kfd-upstream-aggregate.json": collectKfdUpstreamFacts({ cwd: root, includeOwn: false }),
1346
+ "kfd-upstream-aggregate.json": collectKfdUpstreamFacts({ cwd: root, components: readJson("architecture/product-upstreams.json"), includeOwn: false }),
1347
1347
  "kfd-claims.json": createBuildchainKfdClaimRegistry({ root }),
1348
1348
  "product-mechanism.json": productMechanism,
1349
1349
  "release-provenance.json": releaseProvenance,
@@ -0,0 +1,42 @@
1
+ import assert from "node:assert/strict";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { execFileSync } from "node:child_process";
5
+ import { createHash } from "node:crypto";
6
+ import {
7
+ platformTriple,
8
+ packageVersion,
9
+ sourceSha,
10
+ } from "../packages/core/build/standalone/identity.js";
11
+
12
+ const cwd = process.cwd();
13
+ const manifest = JSON.parse(
14
+ fs.readFileSync(
15
+ path.join(cwd, "dist/binary", `buildchain-${platformTriple()}.json`),
16
+ "utf8",
17
+ ),
18
+ );
19
+ const version = packageVersion(cwd);
20
+ assert.equal(manifest.version, version);
21
+ assert.equal(manifest.sourceSha, sourceSha(cwd));
22
+ assert.equal(manifest.platform, platformTriple());
23
+ const binary = path.join(
24
+ cwd,
25
+ "dist/binary",
26
+ process.platform === "win32" ? "buildchain.exe" : "buildchain",
27
+ );
28
+ assert.equal(
29
+ createHash("sha256").update(fs.readFileSync(binary)).digest("hex"),
30
+ manifest.sha256,
31
+ );
32
+ assert.equal(
33
+ execFileSync(binary, ["--version"], { encoding: "utf8" }).trim(),
34
+ version,
35
+ );
36
+ assert.match(
37
+ execFileSync(binary, ["help"], { encoding: "utf8" }),
38
+ /^Usage:\s+buildchain --help/mu,
39
+ );
40
+ process.stdout.write(
41
+ `Verified standalone product ${platformTriple()} at ${version}\n`,
42
+ );