@kungfu-tech/buildchain 2.14.14-alpha.0 → 2.14.14-alpha.2

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 (33) hide show
  1. package/actions/promote-buildchain-ref/README.md +6 -4
  2. package/dist/site/buildchain-contract.json +75 -21
  3. package/dist/site/buildchain-site.json +26 -25
  4. package/dist/site/controller-registry.json +47 -1
  5. package/dist/site/kfd-claims.json +10 -5
  6. package/dist/site/kfd-upstream-aggregate.json +1 -1
  7. package/dist/site/manual-registry.json +5 -5
  8. package/dist/site/node-api-registry.json +5 -5
  9. package/dist/site/page-registry.json +12 -12
  10. package/dist/site/public-surface-audit.json +9 -4
  11. package/dist/site/publication-authority-registry.json +3 -2
  12. package/dist/site/publication-registry.json +4 -4
  13. package/dist/site/release-model.json +4 -2
  14. package/dist/site/site-manifest.json +9 -9
  15. package/dist/site/workflow-registry.json +7 -2
  16. package/docs/binary-distribution.md +22 -8
  17. package/docs/release-candidate.md +4 -2
  18. package/docs/release-governance.md +5 -3
  19. package/docs/site-bundle-contract.md +4 -0
  20. package/docs/web-surface-deployments.md +6 -1
  21. package/package.json +1 -1
  22. package/packages/core/buildchain-contract.js +2 -1
  23. package/packages/core/controller-evidence.js +8 -0
  24. package/packages/core/issue-reporting.js +9 -4
  25. package/packages/core/publication-authority.js +75 -0
  26. package/scripts/assemble-binary-release-admission.mjs +201 -0
  27. package/scripts/check-inventory.mjs +1 -1
  28. package/scripts/ensure-github-release.mjs +20 -6
  29. package/scripts/generate-channel-promotion-workflow.mjs +3 -3
  30. package/scripts/generate-site-bundle.mjs +8 -4
  31. package/scripts/runtime-ref-core.mjs +12 -0
  32. package/scripts/site-bundle-homepage.mjs +39 -0
  33. package/scripts/web-surface-cloudfront-rewrite.mjs +100 -53
@@ -861,7 +861,7 @@ for (const forbiddenSnippet of [
861
861
  }
862
862
  for (const requiredSnippet of [
863
863
  "id-token: write",
864
- "actions: read",
864
+ "actions: write",
865
865
  "uses: ./.github/workflows/.release-candidate-promote.yml",
866
866
  "github.event.workflow_run.event == 'push'",
867
867
  "!startsWith(github.event.workflow_run.display_title, 'chore(release): prepare v')",
@@ -1,13 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import { pathToFileURL } from "node:url";
3
3
 
4
- export function classifyReleaseTag(tag) {
4
+ function classifyPublicationChannel(channel = "") {
5
+ const normalized = String(channel || "").trim().toLowerCase();
6
+ if (!normalized) return undefined;
7
+ if (normalized === "alpha") return true;
8
+ if (["release", "stable", "major"].includes(normalized)) return false;
9
+ throw new Error(`Unsupported GitHub Release publication channel: ${channel}`);
10
+ }
11
+
12
+ export function classifyReleaseTag(tag, { channel = "" } = {}) {
5
13
  const normalized = String(tag || "").trim();
6
14
  const match = normalized.match(/^v\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?$/);
7
15
  if (!match) {
8
16
  throw new Error(`Unsupported semver release tag: ${tag}`);
9
17
  }
10
- const prerelease = normalized.includes("-");
18
+ const channelPrerelease = classifyPublicationChannel(channel);
19
+ const prerelease = channelPrerelease ?? normalized.includes("-");
11
20
  return {
12
21
  tag: normalized,
13
22
  prerelease,
@@ -23,6 +32,7 @@ function parseArgs(argv) {
23
32
  title: "",
24
33
  notes: "",
25
34
  target: "",
35
+ channel: "",
26
36
  };
27
37
  for (let index = 0; index < argv.length; index += 1) {
28
38
  const arg = argv[index];
@@ -38,6 +48,7 @@ function parseArgs(argv) {
38
48
  else if (arg === "--title") args.title = readValue();
39
49
  else if (arg === "--notes") args.notes = readValue();
40
50
  else if (arg === "--target") args.target = readValue();
51
+ else if (arg === "--channel") args.channel = readValue();
41
52
  else if (arg === "--api-url") args.apiUrl = readValue();
42
53
  else if (arg === "--help" || arg === "-h") {
43
54
  args.help = true;
@@ -50,11 +61,12 @@ function parseArgs(argv) {
50
61
 
51
62
  function usage() {
52
63
  return [
53
- "usage: node scripts/ensure-github-release.mjs --repository <owner/repo> --tag <tag>",
64
+ "usage: node scripts/ensure-github-release.mjs --repository <owner/repo> --tag <tag> [--channel alpha|release]",
54
65
  "",
55
66
  "Ensures Buildchain GitHub Release metadata is deterministic:",
56
- "- vX.Y.Z-alpha.N => prerelease=true, make_latest=false",
57
- "- vX.Y.Z => prerelease=false, make_latest=true",
67
+ "- explicit alpha => prerelease=true, make_latest=false",
68
+ "- explicit release/stable/major => prerelease=false, make_latest=true",
69
+ "- without --channel, semver prerelease syntax remains the fallback",
58
70
  ].join("\n");
59
71
  }
60
72
 
@@ -106,9 +118,10 @@ export async function ensureGitHubRelease({
106
118
  title = "",
107
119
  notes = "",
108
120
  target = "",
121
+ channel = "",
109
122
  } = {}) {
110
123
  const { owner, repo } = splitRepository(repository);
111
- const metadata = classifyReleaseTag(tag);
124
+ const metadata = classifyReleaseTag(tag, { channel });
112
125
  const encodedTag = encodeURIComponent(metadata.tag);
113
126
  const releasePath = `/repos/${owner}/${repo}/releases/tags/${encodedTag}`;
114
127
  const refPath = `/repos/${owner}/${repo}/git/ref/tags/${encodedTag}`;
@@ -163,6 +176,7 @@ async function main() {
163
176
  title: args.title,
164
177
  notes: args.notes,
165
178
  target: args.target,
179
+ channel: args.channel,
166
180
  });
167
181
  console.log(`github-release-${result.action}=${result.metadata.tag}`);
168
182
  console.log(`github-release-prerelease=${result.metadata.prerelease}`);
@@ -204,7 +204,7 @@ ${secrets.trimEnd()}
204
204
  ${publicOutputs(outputs)}
205
205
 
206
206
  permissions:
207
- actions: read
207
+ actions: write
208
208
  checks: write
209
209
  contents: write
210
210
  id-token: write
@@ -404,7 +404,7 @@ jobs:
404
404
  if: \${{ needs.resolve-promotion.outputs.channel == 'alpha' }}
405
405
  uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
406
406
  permissions:
407
- actions: read
407
+ actions: write
408
408
  checks: write
409
409
  contents: write
410
410
  id-token: write
@@ -419,7 +419,7 @@ ${alphaForwarded}
419
419
  if: \${{ needs.resolve-promotion.outputs.channel == 'stable' }}
420
420
  uses: kungfu-systems/buildchain/${stableRoute.workflowPath}@${stableRoute.callRef}
421
421
  permissions:
422
- actions: read
422
+ actions: write
423
423
  checks: write
424
424
  contents: write
425
425
  id-token: write
@@ -28,6 +28,7 @@ import {
28
28
  enumerateWorkflowInputs,
29
29
  } from "../packages/core/public-surface-audit.js";
30
30
  import { createSurfaceTimestampPolicy } from "../packages/core/surface-manifest.js";
31
+ import { projectHomepageIntro } from "./site-bundle-homepage.mjs";
31
32
 
32
33
  const SITE_BUNDLE_CONTRACT = "kungfu-buildchain-site-bundle";
33
34
  const PUBLICATION_RELEASE_REGISTRY_CONTRACT = "kungfu-buildchain-publication-release-registry";
@@ -702,6 +703,7 @@ function buildSiteBundle() {
702
703
  const packageJson = readJson("package.json");
703
704
  const inventory = readJson("tests/buildchain-inventory.json");
704
705
  const readme = parseReadme(readText(README_PATH));
706
+ const homepageIntro = projectHomepageIntro(readme.intro);
705
707
  const docs = BUILDCHAIN_AGENT_MANUALS.map((manual) => docEntry(manual.id, manual.title, manual.path, manual.plane));
706
708
  const pages = buildSitePages();
707
709
  const preservedTimestampPolicy = readExistingSiteTimestampPolicy(packageJson.version);
@@ -945,8 +947,10 @@ function buildSiteBundle() {
945
947
  },
946
948
  githubRelease: {
947
949
  exactTagRelease: true,
948
- prereleaseTags: "semver prerelease tags set prerelease=true and make_latest=false",
949
- stableTags: "stable semver tags set make_latest=true",
950
+ authoritativeIntent: "Buildchain publication channel controls prerelease/latest metadata when available",
951
+ alphaChannel: "alpha publication sets prerelease=true and make_latest=false",
952
+ stableChannels: "release, stable, and major publication set prerelease=false and make_latest=true",
953
+ tagFallback: "ordinary callers without publication intent use semver prerelease syntax",
950
954
  evidenceAssets: [
951
955
  "publish evidence JSON",
952
956
  "buildchain.release.json",
@@ -1218,8 +1222,8 @@ function buildSiteBundle() {
1218
1222
  },
1219
1223
  homepage: {
1220
1224
  title: readme.title,
1221
- lead: paragraphBlocks(readme.intro)[0] || "",
1222
- mechanismSummary: paragraphBlocks(readme.intro).slice(1),
1225
+ lead: homepageIntro.lead,
1226
+ mechanismSummary: homepageIntro.mechanismSummary,
1223
1227
  sections: homepageSections,
1224
1228
  displayPlan: {
1225
1229
  firstScreen: {
@@ -111,6 +111,8 @@ export function validateRuntimeOverrideTrust({
111
111
  requestedRef = "",
112
112
  eventName = "",
113
113
  actorPermission = "",
114
+ sameRepositoryPullRequest = false,
115
+ pullRequestHeadSha = "",
114
116
  } = {}) {
115
117
  if (!String(requestedRef || "").trim()) {
116
118
  return { ok: true, decision: "stable-default" };
@@ -118,6 +120,16 @@ export function validateRuntimeOverrideTrust({
118
120
  if (isOfficialBuildchainChannelRef(requestedRef)) {
119
121
  return { ok: true, decision: "official-channel" };
120
122
  }
123
+ const normalizedRequested = String(requestedRef || "").trim().toLowerCase();
124
+ const normalizedHeadSha = String(pullRequestHeadSha || "").trim().toLowerCase();
125
+ if (
126
+ eventName === "pull_request" &&
127
+ sameRepositoryPullRequest === true &&
128
+ EXACT_SHA_RE.test(normalizedRequested) &&
129
+ normalizedRequested === normalizedHeadSha
130
+ ) {
131
+ return { ok: true, decision: "same-repository-pr-head" };
132
+ }
121
133
  if (eventName !== "workflow_dispatch") {
122
134
  return {
123
135
  ok: false,
@@ -0,0 +1,39 @@
1
+ const BADGE_BLOCK_START = "<!-- buildchain:badges:start -->";
2
+ const BADGE_BLOCK_END = "<!-- buildchain:badges:end -->";
3
+
4
+ function normalizeMarkdown(value) {
5
+ return String(value || "").replace(/\r\n/g, "\n").trim();
6
+ }
7
+
8
+ function paragraphBlocks(markdown) {
9
+ return normalizeMarkdown(markdown)
10
+ .split(/\n{2,}/)
11
+ .map((block) => block.replace(/\n/g, " ").trim())
12
+ .filter(Boolean);
13
+ }
14
+
15
+ export function projectHomepageIntro(markdown) {
16
+ const intro = normalizeMarkdown(markdown);
17
+ const start = intro.indexOf(BADGE_BLOCK_START);
18
+ const end = intro.indexOf(BADGE_BLOCK_END);
19
+ if ((start >= 0) !== (end >= 0) || (start >= 0 && end < start)) {
20
+ throw new Error("README.md managed badge block is incomplete");
21
+ }
22
+ if (start >= 0) {
23
+ const endOffset = end + BADGE_BLOCK_END.length;
24
+ const lead = intro.slice(start, endOffset).trim();
25
+ const remaining = [intro.slice(0, start), intro.slice(endOffset)]
26
+ .map((part) => part.trim())
27
+ .filter(Boolean)
28
+ .join("\n\n");
29
+ return {
30
+ lead,
31
+ mechanismSummary: paragraphBlocks(remaining),
32
+ };
33
+ }
34
+ const blocks = paragraphBlocks(intro);
35
+ return {
36
+ lead: blocks[0] || "",
37
+ mechanismSummary: blocks.slice(1),
38
+ };
39
+ }
@@ -15,6 +15,9 @@ const FUNCTION_CODE = `function handler(event) {
15
15
  }
16
16
  `;
17
17
 
18
+ const DISTRIBUTION_UPDATE_MAX_ATTEMPTS = 3;
19
+ const DISTRIBUTION_UPDATE_RETRY_DELAY_MS = 250;
20
+
18
21
  function readArg(name, fallback = "") {
19
22
  const flag = `--${name}`;
20
23
  const index = process.argv.indexOf(flag);
@@ -31,7 +34,7 @@ function runAws(args, { allowFailure = false } = {}) {
31
34
  });
32
35
  if (result.error) {
33
36
  if (allowFailure) {
34
- return { ok: false, stdout: "", stderr: String(result.error.message || result.error) };
37
+ return { ok: false, status: null, stdout: "", stderr: String(result.error.message || result.error) };
35
38
  }
36
39
  throw result.error;
37
40
  }
@@ -39,11 +42,11 @@ function runAws(args, { allowFailure = false } = {}) {
39
42
  const stderr = String(result.stderr || "");
40
43
  if (result.status !== 0) {
41
44
  if (allowFailure) {
42
- return { ok: false, stdout, stderr };
45
+ return { ok: false, status: result.status, stdout, stderr };
43
46
  }
44
47
  throw new Error(`aws ${args.join(" ")} failed with exit code ${result.status}${stderr.trim() ? `: ${stderr.trim()}` : ""}`);
45
48
  }
46
- return { ok: true, stdout, stderr };
49
+ return { ok: true, status: result.status, stdout, stderr };
47
50
  }
48
51
 
49
52
  function runAwsJson(args, options = {}) {
@@ -58,6 +61,22 @@ function functionConfig(comment) {
58
61
  return `Comment=${comment.replace(/,/g, " ")},Runtime=cloudfront-js-2.0`;
59
62
  }
60
63
 
64
+ function awsFailureMessage(args, result) {
65
+ const status = result.status === null || result.status === undefined
66
+ ? ""
67
+ : ` with exit code ${result.status}`;
68
+ const stderr = String(result.stderr || "").trim();
69
+ return `aws ${args.join(" ")} failed${status}${stderr ? `: ${stderr}` : ""}`;
70
+ }
71
+
72
+ function isStaleDistributionEtag(result) {
73
+ return result.ok === false && /\bPreconditionFailed\b/.test(String(result.stderr || ""));
74
+ }
75
+
76
+ function sleepSync(milliseconds) {
77
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
78
+ }
79
+
61
80
  function writeFunctionCode(tmpDir) {
62
81
  const file = path.join(tmpDir, "directory-index-rewrite.js");
63
82
  fs.writeFileSync(file, FUNCTION_CODE);
@@ -130,57 +149,85 @@ function publishFunction({ name, etag }) {
130
149
  }
131
150
 
132
151
  function attachFunction({ distributionId, functionArn }) {
133
- const current = runAwsJson(["cloudfront", "get-distribution-config", "--id", distributionId]);
134
- const etag = current.data?.ETag || "";
135
- const config = current.data?.DistributionConfig;
136
- if (!etag || !config) {
137
- throw new Error(`cloudfront get-distribution-config did not return ETag and DistributionConfig for ${distributionId}`);
138
- }
139
- const defaultBehavior = config.DefaultCacheBehavior || {};
140
- const existing = defaultBehavior.FunctionAssociations || { Quantity: 0, Items: [] };
141
- const items = Array.isArray(existing.Items) ? [...existing.Items] : [];
142
- const currentViewer = items.find((item) => item.EventType === "viewer-request");
143
- if (currentViewer && currentViewer.FunctionARN && currentViewer.FunctionARN !== functionArn) {
144
- throw new Error(
145
- `CloudFront distribution ${distributionId} already has a different viewer-request function ${currentViewer.FunctionARN}; ` +
146
- "remove or migrate it before Buildchain can manage directory-index rewrites",
147
- );
148
- }
149
- const nextItems = [
150
- ...items.filter((item) => item.EventType !== "viewer-request"),
151
- {
152
- EventType: "viewer-request",
153
- FunctionARN: functionArn,
154
- },
155
- ];
156
- config.DefaultCacheBehavior = {
157
- ...defaultBehavior,
158
- FunctionAssociations: {
159
- Quantity: nextItems.length,
160
- Items: nextItems,
161
- },
162
- };
163
- const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-cloudfront-distribution-"));
164
- try {
165
- const configFile = path.join(tmpDir, "distribution-config.json");
166
- fs.writeFileSync(configFile, `${JSON.stringify(config, null, 2)}\n`);
167
- runAws([
168
- "cloudfront",
169
- "update-distribution",
170
- "--id",
171
- distributionId,
172
- "--if-match",
173
- etag,
174
- "--distribution-config",
175
- `file://${configFile}`,
176
- ]);
177
- } finally {
178
- fs.rmSync(tmpDir, { recursive: true, force: true });
152
+ let previousViewerRequestFunction;
153
+ for (let attempt = 1; attempt <= DISTRIBUTION_UPDATE_MAX_ATTEMPTS; attempt += 1) {
154
+ const current = runAwsJson(["cloudfront", "get-distribution-config", "--id", distributionId]);
155
+ const etag = current.data?.ETag || "";
156
+ const config = current.data?.DistributionConfig;
157
+ if (!etag || !config) {
158
+ throw new Error(`cloudfront get-distribution-config did not return ETag and DistributionConfig for ${distributionId}`);
159
+ }
160
+ const defaultBehavior = config.DefaultCacheBehavior || {};
161
+ const existing = defaultBehavior.FunctionAssociations || { Quantity: 0, Items: [] };
162
+ const items = Array.isArray(existing.Items) ? [...existing.Items] : [];
163
+ const currentViewer = items.find((item) => item.EventType === "viewer-request");
164
+ if (previousViewerRequestFunction === undefined) {
165
+ previousViewerRequestFunction = currentViewer?.FunctionARN || "";
166
+ }
167
+ if (currentViewer && currentViewer.FunctionARN && currentViewer.FunctionARN !== functionArn) {
168
+ throw new Error(
169
+ `CloudFront distribution ${distributionId} already has a different viewer-request function ${currentViewer.FunctionARN}; ` +
170
+ "remove or migrate it before Buildchain can manage directory-index rewrites",
171
+ );
172
+ }
173
+ if (currentViewer?.FunctionARN === functionArn) {
174
+ return {
175
+ previousViewerRequestFunction,
176
+ nextViewerRequestFunction: functionArn,
177
+ };
178
+ }
179
+
180
+ const nextItems = [
181
+ ...items.filter((item) => item.EventType !== "viewer-request"),
182
+ {
183
+ EventType: "viewer-request",
184
+ FunctionARN: functionArn,
185
+ },
186
+ ];
187
+ config.DefaultCacheBehavior = {
188
+ ...defaultBehavior,
189
+ FunctionAssociations: {
190
+ Quantity: nextItems.length,
191
+ Items: nextItems,
192
+ },
193
+ };
194
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "buildchain-cloudfront-distribution-"));
195
+ let args;
196
+ let updated;
197
+ try {
198
+ const configFile = path.join(tmpDir, "distribution-config.json");
199
+ fs.writeFileSync(configFile, `${JSON.stringify(config, null, 2)}\n`);
200
+ args = [
201
+ "cloudfront",
202
+ "update-distribution",
203
+ "--id",
204
+ distributionId,
205
+ "--if-match",
206
+ etag,
207
+ "--distribution-config",
208
+ `file://${configFile}`,
209
+ ];
210
+ updated = runAws(args, { allowFailure: true });
211
+ } finally {
212
+ fs.rmSync(tmpDir, { recursive: true, force: true });
213
+ }
214
+ if (updated.ok) {
215
+ return {
216
+ previousViewerRequestFunction,
217
+ nextViewerRequestFunction: functionArn,
218
+ };
219
+ }
220
+ if (!isStaleDistributionEtag(updated)) {
221
+ throw new Error(awsFailureMessage(args, updated));
222
+ }
223
+ if (attempt === DISTRIBUTION_UPDATE_MAX_ATTEMPTS) {
224
+ throw new Error(
225
+ `CloudFront update-distribution still failed after ${attempt} attempts: ${awsFailureMessage(args, updated)}`,
226
+ );
227
+ }
228
+ sleepSync(DISTRIBUTION_UPDATE_RETRY_DELAY_MS * attempt);
179
229
  }
180
- return {
181
- previousViewerRequestFunction: currentViewer?.FunctionARN || "",
182
- nextViewerRequestFunction: functionArn,
183
- };
230
+ throw new Error(`CloudFront update-distribution retry loop ended unexpectedly for ${distributionId}`);
184
231
  }
185
232
 
186
233
  export function ensureCloudFrontDirectoryIndexRewrite({