@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.
- package/actions/macos-credential-island/README.md +18 -0
- package/actions/promote-buildchain-ref/README.md +8 -0
- package/dist/site/buildchain-contract.json +182 -27
- package/dist/site/buildchain-site.json +43 -16
- package/dist/site/capability-registry.json +2 -2
- package/dist/site/controller-registry.json +85 -4
- package/dist/site/kfd-claims.json +105 -13
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +3 -3
- package/dist/site/node-api-registry.json +4 -4
- package/dist/site/page-registry.json +36 -9
- package/dist/site/public-surface-audit.json +71 -15
- package/dist/site/publication-authority-registry.json +2 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +7 -7
- package/dist/site/workflow-registry.json +65 -11
- package/docs/kfd-support.md +6 -0
- package/docs/reusable-build-surface.md +48 -0
- package/docs/versioning.md +1 -0
- package/package.json +1 -1
- package/packages/core/buildchain-contract.js +58 -0
- package/packages/core/buildchain-kfd-claims.js +2 -0
- package/packages/core/controller-evidence.js +9 -2
- package/packages/core/kfd3-surface-register.js +51 -1
- package/scripts/aggregate-build-summary.mjs +7 -2
- package/scripts/artifact-relay-s3.mjs +11 -1
- package/scripts/check-inventory.mjs +1 -0
- package/scripts/generate-channel-build-workflow.mjs +17 -1
- package/scripts/seal-macos-credential-input.mjs +135 -0
|
@@ -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: [
|
|
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
|
|
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 (
|
|
25
|
+
if (expectedManifestCount > 0 && manifests.length !== expectedManifestCount) {
|
|
21
26
|
throw new Error(
|
|
22
|
-
`expected ${
|
|
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
|
-
|
|
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
|
+
}
|