@kungfu-tech/buildchain 2.14.17-alpha.5 → 2.14.17-alpha.6

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.
@@ -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",
@@ -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 = {
@@ -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
+ }