@kungfu-tech/buildchain 4.0.2-alpha.6 → 4.0.2-alpha.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.
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import assert from "node:assert/strict";
4
+ import crypto from "node:crypto";
4
5
  import fs from "node:fs";
5
6
  import path from "node:path";
6
- import { fileURLToPath } from "node:url";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
8
 
8
9
  import {
9
10
  parseWorkflowDocument,
@@ -17,6 +18,71 @@ function read(relative) {
17
18
  return fs.readFileSync(path.join(root, relative), "utf8");
18
19
  }
19
20
 
21
+ const LOCAL_MODULE_EXTENSIONS = ["", ".js", ".mjs", ".cjs"];
22
+ const PRIVILEGED_ENTRYPOINTS = [
23
+ "actions/v4-release-candidate-promote/index.js",
24
+ ];
25
+
26
+ function localModuleSpecifiers(source) {
27
+ const specifiers = [];
28
+ const expression =
29
+ /(?:\bfrom\s+|\bimport\s*\(\s*|\bimport\s+)["'](\.[^"']+)["']/gu;
30
+ for (const match of source.matchAll(expression)) specifiers.push(match[1]);
31
+ return [...new Set(specifiers)].sort();
32
+ }
33
+
34
+ function resolveLocalModule(importer, specifier) {
35
+ const unresolved = path.posix.normalize(
36
+ path.posix.join(path.posix.dirname(importer), specifier),
37
+ );
38
+ assert.ok(
39
+ !unresolved.startsWith("../") && !path.posix.isAbsolute(unresolved),
40
+ `local module import escapes the repository: ${importer} -> ${specifier}`,
41
+ );
42
+ const candidates = [
43
+ ...LOCAL_MODULE_EXTENSIONS.map((extension) => `${unresolved}${extension}`),
44
+ ...LOCAL_MODULE_EXTENSIONS.slice(1).map((extension) =>
45
+ path.posix.join(unresolved, `index${extension}`),
46
+ ),
47
+ ];
48
+ const matches = candidates.filter((relative) => {
49
+ const absolute = path.join(root, relative);
50
+ return fs.existsSync(absolute) && fs.statSync(absolute).isFile();
51
+ });
52
+ assert.equal(
53
+ matches.length,
54
+ 1,
55
+ `local module import must resolve exactly once: ${importer} -> ${specifier}`,
56
+ );
57
+ return matches[0];
58
+ }
59
+
60
+ export function discoverStaticModuleClosure(entrypoints) {
61
+ const pending = [...entrypoints];
62
+ const visited = new Set();
63
+ while (pending.length > 0) {
64
+ const relative = pending.pop();
65
+ if (visited.has(relative)) continue;
66
+ assert.ok(
67
+ fs.statSync(path.join(root, relative)).isFile(),
68
+ `privileged entry is not a file: ${relative}`,
69
+ );
70
+ visited.add(relative);
71
+ for (const specifier of localModuleSpecifiers(read(relative))) {
72
+ const dependency = resolveLocalModule(relative, specifier);
73
+ if (!visited.has(dependency)) pending.push(dependency);
74
+ }
75
+ }
76
+ return [...visited].sort();
77
+ }
78
+
79
+ function rootedPathSet(paths) {
80
+ return `sha256:${crypto
81
+ .createHash("sha256")
82
+ .update(JSON.stringify([...paths].sort()))
83
+ .digest("hex")}`;
84
+ }
85
+
20
86
  function productionFiles(relative, extensions) {
21
87
  const absolute = path.join(root, relative);
22
88
  if (!fs.existsSync(absolute)) return [];
@@ -61,6 +127,17 @@ export function discoverV4ReleaseAuthorityClosure() {
61
127
  [".yml", ".yaml", ".js", ".mjs", ".cjs"],
62
128
  /(?:createV4ReleaseReceipt|release-receipt\.json|V4_RELEASE_RECEIPT_CONTRACT)/u,
63
129
  );
130
+ const privilegedModules = discoverStaticModuleClosure(PRIVILEGED_ENTRYPOINTS);
131
+ const legacyEngineModules = productionFiles(
132
+ "actions/promote-buildchain-ref",
133
+ [".js", ".mjs", ".cjs"],
134
+ )
135
+ .filter((relative) =>
136
+ /(?:^|\/)(?:lib|promote-(?:alpha|release|major)-channel|durable-transaction-operations)\.js$/u.test(
137
+ relative,
138
+ ),
139
+ )
140
+ .sort();
64
141
  return {
65
142
  providerAdapters: [
66
143
  ...new Set([
@@ -71,6 +148,12 @@ export function discoverV4ReleaseAuthorityClosure() {
71
148
  runtimeSelectors,
72
149
  runtimeEngines: ["actions/v4-release-candidate-promote/index.js"],
73
150
  terminalProjections,
151
+ privilegedExecutableClosure: {
152
+ entrypoints: PRIVILEGED_ENTRYPOINTS,
153
+ modules: privilegedModules,
154
+ root: rootedPathSet(privilegedModules),
155
+ },
156
+ legacyEngineModules,
74
157
  };
75
158
  }
76
159
 
@@ -274,6 +357,7 @@ function assertAuthorityClosure(ledger) {
274
357
  "runtimeSelectors",
275
358
  "runtimeEngines",
276
359
  "terminalProjections",
360
+ "legacyEngineModules",
277
361
  ]) {
278
362
  assert.ok(
279
363
  Array.isArray(closure[className]) && closure[className].length > 0,
@@ -292,6 +376,24 @@ function assertAuthorityClosure(ledger) {
292
376
  discovered[className],
293
377
  `authority closure class drifted: ${className}`,
294
378
  );
379
+ assert.deepEqual(
380
+ closure.privilegedExecutableClosure,
381
+ discovered.privilegedExecutableClosure,
382
+ "privileged executable transitive closure drifted",
383
+ );
384
+ assert.match(
385
+ closure.privilegedExecutableClosure.root,
386
+ /^sha256:[0-9a-f]{64}$/u,
387
+ );
388
+ const reachableLegacyEngines = closure.legacyEngineModules.filter(
389
+ (relative) =>
390
+ closure.privilegedExecutableClosure.modules.includes(relative),
391
+ );
392
+ assert.deepEqual(
393
+ reachableLegacyEngines,
394
+ [],
395
+ `legacy release engines remain reachable from APPLY: ${reachableLegacyEngines.join(", ")}`,
396
+ );
295
397
  assert.deepEqual(closure.runtimeEngines, [
296
398
  "actions/v4-release-candidate-promote/index.js",
297
399
  ]);
@@ -305,7 +407,7 @@ function assertAuthorityClosure(ledger) {
305
407
  );
306
408
  const engineSurface = [
307
409
  ".github/workflows/.release-candidate-promote.yml",
308
- ...closure.runtimeEngines,
410
+ ...closure.privilegedExecutableClosure.modules,
309
411
  ]
310
412
  .map(read)
311
413
  .join("\n");
@@ -361,23 +463,28 @@ export function checkV4ReleaseTopology() {
361
463
  return actual;
362
464
  }
363
465
 
364
- if (process.argv.includes("--print-closure")) {
365
- process.stdout.write(
366
- `${JSON.stringify(discoverV4ReleaseAuthorityClosure(), null, 2)}\n`,
367
- );
368
- } else if (process.argv.includes("--print")) {
369
- const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
370
- process.stdout.write(
371
- `${JSON.stringify(
372
- discoverV4ReleaseTopology(
373
- ledger.closedWorld.workflowPaths,
374
- ledger.semanticScope.workflowPaths,
375
- ),
376
- null,
377
- 2,
378
- )}\n`,
379
- );
380
- } else {
381
- checkV4ReleaseTopology();
382
- process.stdout.write("v4 release topology: ok\n");
466
+ if (
467
+ process.argv[1] &&
468
+ import.meta.url === pathToFileURL(process.argv[1]).href
469
+ ) {
470
+ if (process.argv.includes("--print-closure")) {
471
+ process.stdout.write(
472
+ `${JSON.stringify(discoverV4ReleaseAuthorityClosure(), null, 2)}\n`,
473
+ );
474
+ } else if (process.argv.includes("--print")) {
475
+ const ledger = JSON.parse(fs.readFileSync(ledgerPath, "utf8"));
476
+ process.stdout.write(
477
+ `${JSON.stringify(
478
+ discoverV4ReleaseTopology(
479
+ ledger.closedWorld.workflowPaths,
480
+ ledger.semanticScope.workflowPaths,
481
+ ),
482
+ null,
483
+ 2,
484
+ )}\n`,
485
+ );
486
+ } else {
487
+ checkV4ReleaseTopology();
488
+ process.stdout.write("v4 release topology: ok\n");
489
+ }
383
490
  }
@@ -13,7 +13,6 @@ import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
13
13
  import { v4PublicationQualificationRoot, validateV4PublicationQualificationReceipt } from "../packages/core/v4-publication-qualification.js";
14
14
 
15
15
  const DEFAULT_WORKFLOW_FILE = "build-surface-fixture.yml";
16
-
17
16
  function env(name, fallback = "") {
18
17
  return process.env[name] || fallback;
19
18
  }
@@ -21,7 +20,6 @@ function env(name, fallback = "") {
21
20
  export function releaseCandidateDownloadEnabled(value = "true") {
22
21
  return String(value || "true").trim().toLowerCase() !== "false";
23
22
  }
24
-
25
23
  function splitRepository(repository) {
26
24
  const match = String(repository || "").trim().match(/^([^/\s]+)\/([^/\s]+)$/);
27
25
  if (!match) {
@@ -44,7 +42,9 @@ function assertSha(value, label = "sha") {
44
42
 
45
43
  export const releaseCandidateRuntimeSha = (passport) =>
46
44
  assertSha(passport?.buildchain?.sha, "release candidate Passport Buildchain runtime SHA").toLowerCase();
47
-
45
+ export const resolveFreshPublicationVersion = ({ sealedBundle, candidateVersion = "" } = {}) =>
46
+ String(sealedBundle?.manifest?.npm?.version || candidateVersion || "").trim();
47
+ const optionalText = (value) => String(value || "");
48
48
  function githubHeaders(token) {
49
49
  const headers = {
50
50
  accept: "application/vnd.github+json",
@@ -740,19 +740,17 @@ export async function resolveReleaseCandidateArtifacts({
740
740
  releaseAssetPaths,
741
741
  })
742
742
  : undefined;
743
- const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8")));
743
+ const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8"))), publicationVersion = resolveFreshPublicationVersion({ sealedBundle, candidateVersion: passport.target?.version });
744
744
  const generatedRequiredArtifacts = generatePublishRequiredArtifacts({
745
745
  manifests,
746
- version: [sealedBundle?.manifest?.npm?.version, passport.target?.version, ""].find(Boolean),
746
+ version: publicationVersion,
747
747
  kind: publishArtifactKind,
748
748
  tarballPaths: npmTarballPaths,
749
749
  mainPackage: publishPackageMain,
750
750
  });
751
751
  const requiredArtifactsPath = path.join(resolvedOutput, "publish-required-artifacts.json");
752
752
  fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(generatedRequiredArtifacts, null, 2)}\n`);
753
- const sealedBundleManifestPath = sealedBundle
754
- ? path.join(resolvedOutput, "sealed-bundle.json")
755
- : "";
753
+ const sealedBundleManifestPath = sealedBundle ? path.join(resolvedOutput, "sealed-bundle.json") : "";
756
754
  if (sealedBundleManifestPath) {
757
755
  fs.writeFileSync(sealedBundleManifestPath, `${JSON.stringify(sealedBundle.manifest, null, 2)}\n`);
758
756
  }
@@ -772,6 +770,7 @@ export async function resolveReleaseCandidateArtifacts({
772
770
  sealedBundleManifest: sealedBundleManifestPath ? outputPath(sealedBundleManifestPath) : "",
773
771
  },
774
772
  version: passport.target?.version || "",
773
+ publicationVersion,
775
774
  candidateHash: passport.candidateHash || "",
776
775
  payloadCount: payloadArtifacts.length,
777
776
  platformManifestCount: platformManifestPaths.length,
@@ -811,6 +810,7 @@ export async function resolveReleaseCandidateArtifactsCli() {
811
810
  "release-candidate-publication-qualification-path": result.paths?.publicationQualification || "",
812
811
  "release-candidate-publication-qualification-root": result.publicationQualificationRoot || "",
813
812
  "release-candidate-version": result.version || "",
813
+ "release-candidate-publication-version": optionalText(result.publicationVersion),
814
814
  "release-candidate-source-sha": result.artifacts?.sourceSha || "",
815
815
  "release-candidate-artifact": result.artifacts?.passport || "",
816
816
  "release-candidate-build-summary-artifact": result.artifacts?.summary || "",
@@ -843,7 +843,6 @@ export async function resolveReleaseCandidateArtifactsCli() {
843
843
  console.log(JSON.stringify(result, null, 2));
844
844
  return result;
845
845
  }
846
-
847
846
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
848
847
  try {
849
848
  await resolveReleaseCandidateArtifactsCli();
@@ -821,7 +821,7 @@ export async function resolveRecoveryTransaction({
821
821
  transactionId,
822
822
  publicationVersion,
823
823
  }) {
824
- let transaction = publicationVersion
824
+ let transaction = transactionId && publicationVersion
825
825
  ? await readExistingTransaction({
826
826
  repoInfo,
827
827
  apiUrl,
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ import { spawnSyncCommand } from "../packages/core/spawn-command.js";
8
+ import { selectV4ProductPublicationIntent } from "../packages/core/v4-product-publication.js";
9
+ import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
10
+
11
+ function env(name, required = false) {
12
+ const value = String(process.env[name] || "").trim();
13
+ if (required && !value) throw new Error(`${name} is required`);
14
+ return value;
15
+ }
16
+
17
+ function readJson(file) {
18
+ return JSON.parse(fs.readFileSync(path.resolve(file), "utf8"));
19
+ }
20
+
21
+ function packageName() {
22
+ const declared = env("BUILDCHAIN_PUBLISH_PACKAGE_MAIN");
23
+ if (declared) return declared;
24
+ const manifestPath = env("BUILDCHAIN_SEALED_BUNDLE_MANIFEST", true);
25
+ const manifest = readJson(manifestPath);
26
+ const selected = String(manifest?.npm?.name || "").trim();
27
+ if (!selected)
28
+ throw new Error("sealed bundle manifest does not declare npm.name");
29
+ return selected;
30
+ }
31
+
32
+ function observedVersions(name) {
33
+ const result = spawnSyncCommand(
34
+ "npm",
35
+ [
36
+ "view",
37
+ name,
38
+ "versions",
39
+ "--json",
40
+ "--registry=https://registry.npmjs.org/",
41
+ ],
42
+ { encoding: "utf8" },
43
+ );
44
+ if (result.error) throw result.error;
45
+ if (result.status !== 0) {
46
+ const output = `${result.stdout || ""}\n${result.stderr || ""}`;
47
+ if (/\bE404\b|404 Not Found|is not in this registry/iu.test(output))
48
+ return [];
49
+ throw new Error(`npm version discovery failed: ${output.trim()}`);
50
+ }
51
+ const parsed = JSON.parse(String(result.stdout || "[]"));
52
+ return Array.isArray(parsed) ? parsed : [parsed].filter(Boolean);
53
+ }
54
+
55
+ function writeOutput(name, value) {
56
+ const output = env("GITHUB_OUTPUT");
57
+ if (!output) return;
58
+ fs.appendFileSync(output, `${name}=${String(value)}\n`);
59
+ }
60
+
61
+ export function resolveV4ProductPublicationIntent() {
62
+ const name = packageName();
63
+ const sourceSha = env("BUILDCHAIN_SOURCE_SHA", true);
64
+ const manifest = readJson(env("BUILDCHAIN_SEALED_BUNDLE_MANIFEST", true));
65
+ const requiredArtifacts = readJson(
66
+ env("BUILDCHAIN_REQUIRED_ARTIFACTS_PATH", true),
67
+ );
68
+ const channel = env("BUILDCHAIN_CHANNEL", true);
69
+ const intent = selectV4ProductPublicationIntent({
70
+ channel,
71
+ targetRef: env("BUILDCHAIN_TARGET_REF", true),
72
+ sourceSha,
73
+ sourceTimestamp: env("BUILDCHAIN_SOURCE_TIMESTAMP", true),
74
+ repository: env("BUILDCHAIN_REPOSITORY", true),
75
+ packageName: name,
76
+ distTag:
77
+ env("BUILDCHAIN_PUBLISH_DIST_TAG") ||
78
+ (channel === "alpha" ? "alpha" : "latest"),
79
+ sealedBundleRoot: manifest.root,
80
+ requiredArtifactsRoot: v4ContentRoot(
81
+ "v4-product-required-artifacts",
82
+ requiredArtifacts,
83
+ ),
84
+ candidateVersion: env("BUILDCHAIN_CANDIDATE_VERSION", true),
85
+ recoveredVersion: env("BUILDCHAIN_RECOVERED_PUBLICATION_VERSION"),
86
+ observedVersions: observedVersions(name),
87
+ });
88
+ const outputPath = path.resolve(
89
+ env("BUILDCHAIN_PRODUCT_PUBLICATION_INTENT_PATH") ||
90
+ ".buildchain/release-candidate/v4-product-publication-intent.json",
91
+ );
92
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
93
+ fs.writeFileSync(outputPath, `${JSON.stringify(intent, null, 2)}\n`);
94
+ writeOutput("version", intent.version);
95
+ writeOutput("exact-tag", intent.exactTag);
96
+ writeOutput("intent-path", outputPath);
97
+ writeOutput("intent-root", intent.intentRoot);
98
+ return { name, intent, outputPath };
99
+ }
100
+
101
+ if (
102
+ process.argv[1] &&
103
+ import.meta.url === pathToFileURL(process.argv[1]).href
104
+ ) {
105
+ try {
106
+ const result = resolveV4ProductPublicationIntent();
107
+ process.stdout.write(
108
+ `v4 product publication intent: ${result.name}@${result.intent.version} (${result.intent.mode})\n`,
109
+ );
110
+ } catch (error) {
111
+ console.error(`v4-product-publication-intent: ${error.message}`);
112
+ process.exitCode = 1;
113
+ }
114
+ }