@kungfu-tech/buildchain 2.14.17 → 2.14.18-alpha.1

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.
@@ -87,6 +87,8 @@ const WORKFLOW_AND_ACTION_FILES = Object.freeze([
87
87
  ".github/workflows/release-candidate-promote.yml",
88
88
  ".github/workflows/buildchain-ref-promotion.yml",
89
89
  ".github/workflows/release-propagation.yml",
90
+ "actions/macos-credential-island/action.yml",
91
+ "actions/macos-credential-island/index.js",
90
92
  "actions/promote-buildchain-ref/action.yml",
91
93
  "actions/promote-buildchain-ref/index.js",
92
94
  ]);
@@ -22,16 +22,23 @@ const CONTROLLER_SPECS = [
22
22
  id: "build-lifecycle",
23
23
  workflowId: ".build",
24
24
  version: 1,
25
- capabilities: ["source-lock", "lifecycle-build", "lifecycle-verify", "artifact-admission"],
25
+ capabilities: [
26
+ "source-lock",
27
+ "lifecycle-build",
28
+ "credential-island",
29
+ "lifecycle-verify",
30
+ "artifact-admission",
31
+ ],
26
32
  stages: [
27
33
  "resolve-runtime",
28
34
  "resolve-source",
29
35
  "anchored-release-preflight",
30
36
  "build",
37
+ "credential-island",
31
38
  "verify",
32
39
  "aggregate",
33
40
  ],
34
- optionalStages: ["anchored-release-preflight"],
41
+ optionalStages: ["anchored-release-preflight", "credential-island"],
35
42
  evidence: [
36
43
  "platform-manifests",
37
44
  "build-summary",
@@ -124,6 +124,50 @@ function normalizeRegistrySurface(entry) {
124
124
  };
125
125
  }
126
126
 
127
+ function globPatternRegExp(pattern) {
128
+ const normalized = String(pattern || "").replaceAll("\\", "/");
129
+ let source = "";
130
+ for (let index = 0; index < normalized.length; index += 1) {
131
+ const character = normalized[index];
132
+ if (character === "*" && normalized[index + 1] === "*") {
133
+ source += ".*";
134
+ index += 1;
135
+ } else if (character === "*") {
136
+ source += "[^/]*";
137
+ } else if (character === "?") {
138
+ source += "[^/]";
139
+ } else {
140
+ source += character.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
141
+ }
142
+ }
143
+ return new RegExp(`^${source}$`);
144
+ }
145
+
146
+ function distributionBindings(registry, detectedSurfaces) {
147
+ return detectedSurfaces.flatMap((detected) =>
148
+ registry.surfaces.flatMap((declared) =>
149
+ (declared.distribution?.artifacts || [])
150
+ .filter(
151
+ (artifact) =>
152
+ normalizeKind(artifact.kind) === detected.kind &&
153
+ globPatternRegExp(artifact.pathGlob).test(
154
+ String(detected.artifactPath || "").replaceAll("\\", "/"),
155
+ ),
156
+ )
157
+ .map((artifact) => ({
158
+ detectedSurfaceId: detected.id,
159
+ declaredSurfaceId: declared.id,
160
+ artifact: {
161
+ kind: artifact.kind,
162
+ platform: artifact.platform,
163
+ pathGlob: artifact.pathGlob,
164
+ ...(artifact.sha256 ? { sha256: artifact.sha256 } : {}),
165
+ },
166
+ })),
167
+ ),
168
+ );
169
+ }
170
+
127
171
  function stableId(value) {
128
172
  return String(value || "")
129
173
  .toLowerCase()
@@ -507,7 +551,11 @@ export function auditKfd3Surfaces({
507
551
  const registry = readKfd3SurfaceRegistry({ cwd, registryPath });
508
552
  const detectedIds = new Set(detection.surfaces.map((entry) => entry.id));
509
553
  const declaredIds = new Set(registry.surfaces.map((entry) => entry.id));
510
- const detectedButUnregistered = detection.surfaces.filter((entry) => !declaredIds.has(entry.id));
554
+ const bindings = distributionBindings(registry, detection.surfaces);
555
+ const distributedIds = new Set(bindings.map((entry) => entry.detectedSurfaceId));
556
+ const detectedButUnregistered = detection.surfaces.filter(
557
+ (entry) => !declaredIds.has(entry.id) && !distributedIds.has(entry.id),
558
+ );
511
559
  const declaredButMissing = registry.surfaces.filter((entry) => !detectedIds.has(entry.id));
512
560
  const enforced = registry.surfaces.filter((entry) => entry.state === "enforced" || entry.enforcement === "enforced");
513
561
  const issues = [
@@ -543,10 +591,12 @@ export function auditKfd3Surfaces({
543
591
  detected: detection.surfaces,
544
592
  declared: registry.surfaces,
545
593
  enforced,
594
+ distributionBindings: bindings,
546
595
  },
547
596
  comparison: {
548
597
  detectedButUnregistered,
549
598
  declaredButMissing,
599
+ distributionBindings: bindings,
550
600
  },
551
601
  issues,
552
602
  };
@@ -13,13 +13,18 @@ export function aggregateBuildSummaryCli() {
13
13
  const outputPath = path.resolve(readEnv("BUILDCHAIN_SUMMARY_OUTPUT", ".buildchain/artifacts/build-summary.json"));
14
14
  const artifactName = readEnv("BUILDCHAIN_ARTIFACT_NAME", "buildchain-artifact");
15
15
  const expectedPlatformCount = Number(readEnv("BUILDCHAIN_PLATFORM_COUNT", "0"));
16
+ const additionalPlatformCount = Number(readEnv("BUILDCHAIN_ADDITIONAL_PLATFORM_COUNT", "0"));
17
+ if (!Number.isInteger(additionalPlatformCount) || additionalPlatformCount < 0) {
18
+ throw new Error("BUILDCHAIN_ADDITIONAL_PLATFORM_COUNT must be a non-negative integer");
19
+ }
20
+ const expectedManifestCount = expectedPlatformCount + additionalPlatformCount;
16
21
  const manifestFiles = findJsonFiles(inputRoot)
17
22
  .filter((file) => path.basename(file) === "manifest.json")
18
23
  .sort();
19
24
  const manifests = manifestFiles.map((file) => JSON.parse(fs.readFileSync(file, "utf8")));
20
- if (expectedPlatformCount > 0 && manifests.length !== expectedPlatformCount) {
25
+ if (expectedManifestCount > 0 && manifests.length !== expectedManifestCount) {
21
26
  throw new Error(
22
- `expected ${expectedPlatformCount} platform manifests, found ${manifests.length} under ${inputRoot}`,
27
+ `expected ${expectedManifestCount} platform manifests, found ${manifests.length} under ${inputRoot}`,
23
28
  );
24
29
  }
25
30
  const summary = {
@@ -291,7 +291,7 @@ function writeManifest(manifestPath, manifest) {
291
291
  }
292
292
 
293
293
  function resolveUploadGroups() {
294
- return [
294
+ const groups = [
295
295
  {
296
296
  role: "payload",
297
297
  artifactName: env("BUILDCHAIN_ARTIFACT_RELAY_PAYLOAD_ARTIFACT_NAME"),
@@ -311,6 +311,16 @@ function resolveUploadGroups() {
311
311
  required: true,
312
312
  },
313
313
  ];
314
+ const credentialInputPaths = splitLines(env("BUILDCHAIN_ARTIFACT_RELAY_CREDENTIAL_INPUT_PATHS"));
315
+ if (credentialInputPaths.length > 0) {
316
+ groups.push({
317
+ role: "credential-input",
318
+ artifactName: env("BUILDCHAIN_ARTIFACT_RELAY_CREDENTIAL_INPUT_ARTIFACT_NAME"),
319
+ paths: credentialInputPaths,
320
+ required: true,
321
+ });
322
+ }
323
+ return groups;
314
324
  }
315
325
 
316
326
  export async function uploadRelayArtifacts({
@@ -59,6 +59,7 @@ const requiredPaths = [
59
59
  "scripts/generate-site-bundle.mjs",
60
60
  "scripts/generate-buildchain-kfd-witnesses.mjs",
61
61
  "scripts/generate-release-candidate-passport.mjs",
62
+ "scripts/seal-macos-credential-input.mjs",
62
63
  "scripts/shifu-gate-profile.mjs",
63
64
  "scripts/artifact-relay-s3.mjs",
64
65
  "scripts/anchored-version-material.mjs",
@@ -54,7 +54,15 @@ function routerOutputs(outputBlock) {
54
54
  .replace("value: ${{ jobs.build.outputs.controller-receipt-artifact }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-artifact }}")
55
55
  .replace("value: ${{ jobs.build.outputs.controller-receipt-json }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-json }}")
56
56
  .replace("value: ${{ jobs.build.outputs.controller-receipt-digest }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-digest }}")
57
- .replace("value: ${{ jobs.build.outputs.controller-receipt-status }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-status }}");
57
+ .replace("value: ${{ jobs.build.outputs.controller-receipt-status }}", "value: ${{ jobs.controller-receipt.outputs.controller-receipt-status }}")
58
+ .replace(
59
+ /( credential-island-macos-artifact:\n(?: .*\n)*? value:) \$\{\{ jobs\.build\.outputs\.artifact-name \}\}/,
60
+ "$1 ${{ jobs.build.outputs.credential-island-macos-artifact }}",
61
+ )
62
+ .replace(
63
+ /( credential-island-macos-manifest-artifact:\n(?: .*\n)*? value:) \$\{\{ jobs\.build\.outputs\.manifest-artifact-name \}\}/,
64
+ "$1 ${{ jobs.build.outputs.credential-island-macos-manifest-artifact }}",
65
+ );
58
66
  return [
59
67
  " buildchain-channel:",
60
68
  ' description: "Resolved Buildchain channel: alpha, stable, or override"',
@@ -287,6 +295,10 @@ function bindRouterCheckoutToWorkflowSha(workflow) {
287
295
 
288
296
  export function generateChannelBuildWorkflow(source) {
289
297
  const generated = bindRouterCheckoutToWorkflowSha(generateChannelBuildWorkflowBase(source))
298
+ .replace(
299
+ "\npermissions:\n contents: read\n",
300
+ "\npermissions:\n actions: read\n contents: read\n",
301
+ )
290
302
  .replace(
291
303
  " contract-lock-path: ${{ steps.lock.outputs.path }}\n",
292
304
  [
@@ -301,6 +313,10 @@ export function generateChannelBuildWorkflow(source) {
301
313
  .replace(
302
314
  " needs: resolve-channel\n uses: ./.github/workflows/.build.yml",
303
315
  " needs:\n - resolve-channel\n - controller-plan\n uses: ./.github/workflows/.build.yml",
316
+ )
317
+ .replace(
318
+ " uses: ./.github/workflows/.build.yml\n permissions:\n contents: read\n",
319
+ " uses: ./.github/workflows/.build.yml\n permissions:\n actions: read\n contents: read\n",
304
320
  );
305
321
  return `${generated.trimEnd()}\n\n${routerControllerReceiptJob()}\n\n${routerAggregateJob()}\n`;
306
322
  }
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+
7
+ import {
8
+ INPUT_CONTRACT,
9
+ assertContainedSymlinks,
10
+ assertRealPathInside,
11
+ requireRepository,
12
+ requireSha,
13
+ resolveInside,
14
+ sha256File,
15
+ } from "../actions/macos-credential-island/lib.js";
16
+
17
+ function env(name) {
18
+ const value = String(process.env[name] || "").trim();
19
+ if (!value) throw new Error(`${name} is required`);
20
+ return value;
21
+ }
22
+
23
+ function run(command, args) {
24
+ const result = spawnSync(command, args, {
25
+ encoding: "utf8",
26
+ stdio: ["ignore", "pipe", "pipe"],
27
+ });
28
+ if (result.error || result.status !== 0) {
29
+ throw (
30
+ result.error ||
31
+ new Error(
32
+ `${path.basename(command)} failed with status ${result.status}: ${(result.stderr || result.stdout || "").trim().slice(0, 1200)}`,
33
+ )
34
+ );
35
+ }
36
+ return `${result.stdout || ""}${result.stderr || ""}`.trim();
37
+ }
38
+
39
+ function plistValue(appPath, key) {
40
+ return run("/usr/bin/plutil", [
41
+ "-extract",
42
+ key,
43
+ "raw",
44
+ "-o",
45
+ "-",
46
+ path.join(appPath, "Contents", "Info.plist"),
47
+ ]);
48
+ }
49
+
50
+ export function sealMacosCredentialInput() {
51
+ if (process.platform !== "darwin")
52
+ throw new Error("credential input sealing requires macOS");
53
+ const workspace = path.resolve(process.cwd());
54
+ const appPath = resolveInside(
55
+ workspace,
56
+ env("BUILDCHAIN_CREDENTIAL_ISLAND_APP_PATH"),
57
+ "credential island app path",
58
+ );
59
+ if (!fs.statSync(appPath).isDirectory() || !appPath.endsWith(".app")) {
60
+ throw new Error("credential island app path must name one .app directory");
61
+ }
62
+ assertRealPathInside(workspace, appPath, "credential island app path");
63
+ assertContainedSymlinks(appPath);
64
+ const repository = requireRepository(env("BUILDCHAIN_SOURCE_REPOSITORY"));
65
+ const sourceSha = requireSha(env("BUILDCHAIN_SOURCE_SHA"), "source SHA");
66
+ const treeSha = requireSha(
67
+ env("BUILDCHAIN_SOURCE_TREE_SHA"),
68
+ "source tree SHA",
69
+ );
70
+ const platformId = env("BUILDCHAIN_PLATFORM_ID");
71
+ const arch = process.arch;
72
+ if (!["arm64", "x64"].includes(arch))
73
+ throw new Error(`unsupported macOS architecture: ${arch}`);
74
+ const outputRoot = path.resolve(env("BUILDCHAIN_CREDENTIAL_ISLAND_OUTPUT"));
75
+ const relativeOutput = path.relative(workspace, outputRoot);
76
+ if (
77
+ !relativeOutput ||
78
+ relativeOutput.startsWith("..") ||
79
+ path.isAbsolute(relativeOutput)
80
+ ) {
81
+ throw new Error(
82
+ "credential island output must stay inside the build workspace",
83
+ );
84
+ }
85
+ fs.mkdirSync(outputRoot, { recursive: true });
86
+ const archivePath = path.join(outputRoot, "unsigned-app.zip");
87
+ run("/usr/bin/ditto", [
88
+ "-c",
89
+ "-k",
90
+ "--sequesterRsrc",
91
+ "--keepParent",
92
+ appPath,
93
+ archivePath,
94
+ ]);
95
+ const archive = {
96
+ file: path.basename(archivePath),
97
+ format: "ditto-zip",
98
+ bytes: fs.statSync(archivePath).size,
99
+ sha256: sha256File(archivePath),
100
+ };
101
+ const manifest = {
102
+ schema: INPUT_CONTRACT,
103
+ sealedAt: new Date().toISOString(),
104
+ source: { repository, sha: sourceSha, treeSha },
105
+ platform: { id: platformId, os: "macos", arch },
106
+ app: {
107
+ archivePath: path.basename(appPath),
108
+ bundleId: plistValue(appPath, "CFBundleIdentifier"),
109
+ productName: plistValue(appPath, "CFBundleName"),
110
+ version: plistValue(appPath, "CFBundleShortVersionString"),
111
+ buildVersion: plistValue(appPath, "CFBundleVersion"),
112
+ },
113
+ archive,
114
+ };
115
+ const manifestPath = path.join(outputRoot, "credential-input.json");
116
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
117
+ process.stdout.write(
118
+ `${JSON.stringify({ manifestPath, archivePath, archive })}\n`,
119
+ );
120
+ return manifest;
121
+ }
122
+
123
+ if (
124
+ process.argv[1] &&
125
+ import.meta.url === pathToFileURL(process.argv[1]).href
126
+ ) {
127
+ try {
128
+ sealMacosCredentialInput();
129
+ } catch (error) {
130
+ console.error(
131
+ `::error::${String(error?.message || error).replace(/\r?\n/gu, "%0A")}`,
132
+ );
133
+ process.exitCode = 1;
134
+ }
135
+ }