@kungfu-tech/buildchain 3.0.5 → 3.0.6-alpha.0
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/README.md +31 -0
- package/actions/macos-credential-island/README.md +8 -0
- package/bin/buildchain.mjs +1 -1
- package/contracts/auditable-demo-media-profiles-v1.json +61 -0
- package/contracts/auditable-demo-scenario-v1.schema.json +164 -0
- package/contracts/evidence/auditable-demo-responsive-web-delivery-v1.json +3 -3
- package/contracts/evidence/auditable-demo-web-delivery-v1.json +3 -3
- package/dist/site/buildchain-contract.json +107 -26
- package/dist/site/buildchain-site.json +121 -49
- package/dist/site/capability-registry.json +3 -3
- package/dist/site/cli-registry.json +9 -3
- package/dist/site/controller-registry.json +27 -3
- package/dist/site/kfd-claims.json +101 -15
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +10 -9
- package/dist/site/node-api-registry.json +540 -31
- package/dist/site/page-registry.json +105 -29
- package/dist/site/public-surface-audit.json +184 -11
- package/dist/site/publication-authority-registry.json +62 -1
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +13 -13
- package/dist/site/workflow-registry.json +118 -4
- package/docs/MAP.md +4 -1
- package/docs/auditable-demo.md +90 -7
- package/docs/aws-us-elastic-runner-burst-plane.md +151 -16
- package/docs/cli-reference.md +35 -2
- package/docs/dev-alpha-candidate-patrol.md +14 -5
- package/docs/dev-qualification-patrol.md +108 -0
- package/docs/node-api-reference.md +123 -72
- package/docs/publish-transaction.md +7 -5
- package/docs/release-flow.md +4 -0
- package/docs/release-governance.md +39 -0
- package/docs/release-propagation.md +93 -0
- package/docs/reusable-build-surface.md +53 -12
- package/docs/stable-candidate-patrol.md +5 -2
- package/package.json +1 -1
- package/packages/core/buildchain-contract.js +36 -1
- package/packages/core/buildchain-publication-authority.js +3 -0
- package/packages/core/public-surface-audit.js +4 -35
- package/packages/core/release-propagation-agent-entry.js +185 -0
- package/packages/core/release-propagation-pickup.js +387 -0
- package/packages/core/release-propagation-push.js +293 -0
- package/packages/core/release-propagation-release.js +61 -18
- package/packages/core/release-propagation-stage-evidence.js +36 -0
- package/packages/core/release-propagation-work.js +8 -4
- package/packages/core/release-propagation.js +27 -2
- package/packages/core/workflow-call-contract.js +308 -0
- package/packages/core/workflow-yaml-contract.js +272 -0
- package/scripts/artifact-signing-controller-core.mjs +525 -0
- package/scripts/artifact-signing-controller.mjs +302 -0
- package/scripts/artifact-signing-delegation.mjs +51 -3
- package/scripts/auditable-demo-platform.mjs +564 -0
- package/scripts/auditable-demo-renditions.mjs +7 -2
- package/scripts/auditable-demo-transport-smoke.mjs +176 -0
- package/scripts/auditable-demo.mjs +27 -21
- package/scripts/aws-macos-jit-controller-core.mjs +389 -0
- package/scripts/aws-macos-jit-controller-runtime.mjs +82 -0
- package/scripts/aws-macos-jit-controller.mjs +558 -0
- package/scripts/aws-macos-jit-job-controller.mjs +401 -0
- package/scripts/aws-windows-jit-campaign-core.mjs +296 -0
- package/scripts/aws-windows-jit-campaign.mjs +202 -0
- package/scripts/aws-windows-jit-controller-core.mjs +20 -2
- package/scripts/aws-windows-jit-controller.mjs +184 -109
- package/scripts/aws-windows-jit-core.mjs +21 -3
- package/scripts/aws-windows-jit.mjs +2 -0
- package/scripts/build-standalone-binary.mjs +6 -0
- package/scripts/buildchain-cli-help.mjs +2 -1
- package/scripts/capture-package-release-propagation.mjs +24 -1
- package/scripts/check-inventory.mjs +6 -0
- package/scripts/compiler-cache-evidence.mjs +90 -7
- package/scripts/dev-alpha-candidate-patrol.mjs +80 -5
- package/scripts/dev-pr-auto-merge.mjs +4 -4
- package/scripts/dev-qualification-patrol.mjs +594 -0
- package/scripts/dispatch-artifact-signing-authority.mjs +312 -61
- package/scripts/finalize-native-artifact-signing-result.mjs +37 -15
- package/scripts/generate-site-bundle.mjs +5 -0
- package/scripts/inspect-artifact-signing-requests.mjs +12 -0
- package/scripts/locked-source-checkout.mjs +17 -3
- package/scripts/release-propagation.mjs +150 -2
- package/scripts/run-lifecycle-core.mjs +25 -0
- package/scripts/site-capability-metadata.mjs +1 -1
- package/scripts/stable-candidate-patrol.mjs +30 -4
- package/scripts/workflow-call-contract.mjs +133 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
4
5
|
import { pathToFileURL } from "node:url";
|
|
5
6
|
import {
|
|
6
7
|
claimReleasePropagationWork,
|
|
@@ -15,6 +16,15 @@ import {
|
|
|
15
16
|
resumeReleasePropagationWork,
|
|
16
17
|
verifyReleasePropagationWork,
|
|
17
18
|
writeReleasePropagationLock,
|
|
19
|
+
createManualUpstreamPickupCapture,
|
|
20
|
+
createManualUpstreamPickupPlan,
|
|
21
|
+
createReleasePropagationPushPlan,
|
|
22
|
+
executeReleasePropagationPush,
|
|
23
|
+
inspectReleasePropagationPushState,
|
|
24
|
+
normalizeManualUpstreamPickupConfig,
|
|
25
|
+
planSiteUpstreamAgentEntry,
|
|
26
|
+
RELEASE_PROPAGATION_FAILURE_MATRIX,
|
|
27
|
+
resolveNpmRegistryRelease,
|
|
18
28
|
} from "../packages/core/release-propagation.js";
|
|
19
29
|
|
|
20
30
|
function usage() {
|
|
@@ -61,6 +71,28 @@ function usage() {
|
|
|
61
71
|
--completion-decision <json-or-path>
|
|
62
72
|
--expected-work-root <sha256:...>
|
|
63
73
|
[--output <file>] [--json]
|
|
74
|
+
buildchain release-propagation work push-plan --work <json-or-path>
|
|
75
|
+
--expected-work-root <sha256:...>
|
|
76
|
+
[--cwd <dir>] [--remote <name>]
|
|
77
|
+
[--output <file>] [--json]
|
|
78
|
+
buildchain release-propagation work push-branch --work <json-or-path>
|
|
79
|
+
--expected-work-root <sha256:...>
|
|
80
|
+
[--cwd <dir>] [--remote <name>]
|
|
81
|
+
--execute [--output <file>] [--json]
|
|
82
|
+
buildchain release-propagation entry plan --source-id <paper|kfd|buildchain|kungfu-core>
|
|
83
|
+
[--channel <alpha|release>]
|
|
84
|
+
[--handoff-work <json-or-path>]
|
|
85
|
+
[--output <file>] [--json]
|
|
86
|
+
buildchain release-propagation entry fault-matrix [--json]
|
|
87
|
+
buildchain release-propagation pickup plan --config <json-or-path>
|
|
88
|
+
--source-id <id> --channel <alpha|release>
|
|
89
|
+
--current-version <version>
|
|
90
|
+
[--output <file>] [--json]
|
|
91
|
+
buildchain release-propagation pickup create --config <json-or-path>
|
|
92
|
+
--source-id <id> --channel <alpha|release>
|
|
93
|
+
--current-version <version>
|
|
94
|
+
--expected-downstream-base-sha <git-sha>
|
|
95
|
+
[--output <file>] [--json]
|
|
64
96
|
`;
|
|
65
97
|
}
|
|
66
98
|
|
|
@@ -168,10 +200,115 @@ function runWorkCli(args) {
|
|
|
168
200
|
emitWorkResult(workArgs, result, `release propagation work complete: ${result.contentRoot}`);
|
|
169
201
|
return;
|
|
170
202
|
}
|
|
203
|
+
if (mode === "push-plan" || mode === "push-branch") {
|
|
204
|
+
const work = readJsonFlag(workArgs, "work");
|
|
205
|
+
const cwd = readFlag(workArgs, "cwd", process.cwd());
|
|
206
|
+
const remote = readFlag(workArgs, "remote", "origin");
|
|
207
|
+
let result;
|
|
208
|
+
if (mode === "push-plan") {
|
|
209
|
+
const repositoryState = inspectReleasePropagationPushState({ work, cwd, remote });
|
|
210
|
+
result = createReleasePropagationPushPlan({
|
|
211
|
+
work,
|
|
212
|
+
expectedWorkRoot: readFlag(workArgs, "expected-work-root"),
|
|
213
|
+
repositoryState,
|
|
214
|
+
});
|
|
215
|
+
} else {
|
|
216
|
+
if (!hasFlag(workArgs, "execute")) throw new Error("work push-branch requires --execute");
|
|
217
|
+
result = executeReleasePropagationPush({
|
|
218
|
+
work,
|
|
219
|
+
expectedWorkRoot: readFlag(workArgs, "expected-work-root"),
|
|
220
|
+
cwd,
|
|
221
|
+
remote,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
emitWorkResult(workArgs, result, mode === "push-plan"
|
|
225
|
+
? `release propagation push plan: ${result.planRoot}`
|
|
226
|
+
: `release propagation push result: ${result.resultRoot}`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
171
229
|
throw new Error(`unsupported release-propagation work command: ${mode}`);
|
|
172
230
|
}
|
|
173
231
|
|
|
174
|
-
|
|
232
|
+
function runEntryCli(args) {
|
|
233
|
+
const [mode = "", ...entryArgs] = args;
|
|
234
|
+
if (!mode || mode === "--help" || mode === "-h") {
|
|
235
|
+
process.stdout.write(usage());
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
let result;
|
|
239
|
+
if (mode === "fault-matrix") {
|
|
240
|
+
result = RELEASE_PROPAGATION_FAILURE_MATRIX;
|
|
241
|
+
} else if (mode === "plan") {
|
|
242
|
+
result = planSiteUpstreamAgentEntry({
|
|
243
|
+
sourceId: readFlag(entryArgs, "source-id"),
|
|
244
|
+
channel: readFlag(entryArgs, "channel"),
|
|
245
|
+
handoffWork: entryArgs.includes("--handoff-work")
|
|
246
|
+
? readJsonFlag(entryArgs, "handoff-work")
|
|
247
|
+
: null,
|
|
248
|
+
});
|
|
249
|
+
} else {
|
|
250
|
+
throw new Error(`unsupported release-propagation entry command: ${mode}`);
|
|
251
|
+
}
|
|
252
|
+
writeOutput(readFlag(entryArgs, "output", ""), result);
|
|
253
|
+
if (hasFlag(entryArgs, "json")) printJson(result);
|
|
254
|
+
else process.stdout.write(`${result.contract}\n`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function resolvePickupPlan(args) {
|
|
258
|
+
const config = normalizeManualUpstreamPickupConfig(readJsonFlag(args, "config"));
|
|
259
|
+
const sourceId = readFlag(args, "source-id");
|
|
260
|
+
const channel = readFlag(args, "channel");
|
|
261
|
+
const source = config.sources.find((entry) => entry.id === sourceId);
|
|
262
|
+
if (!source) throw new Error(`manual pickup source is not configured: ${sourceId}`);
|
|
263
|
+
const distTag = source.distTags[channel];
|
|
264
|
+
if (!distTag) throw new Error("manual pickup channel must be alpha or release");
|
|
265
|
+
const packageMetadata = args.includes("--package-metadata")
|
|
266
|
+
? readJsonFlag(args, "package-metadata")
|
|
267
|
+
: JSON.parse(execFileSync("npm", ["view", `${source.package}@${distTag}`, "--json", "--registry=https://registry.npmjs.org/"], {
|
|
268
|
+
encoding: "utf8",
|
|
269
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
270
|
+
}));
|
|
271
|
+
const attestationUrl = packageMetadata.dist?.attestations?.url;
|
|
272
|
+
if (!attestationUrl) throw new Error("published npm package does not expose an attestation URL");
|
|
273
|
+
let attestations;
|
|
274
|
+
if (args.includes("--attestations")) {
|
|
275
|
+
attestations = readJsonFlag(args, "attestations");
|
|
276
|
+
} else {
|
|
277
|
+
const response = await fetch(attestationUrl, { headers: { accept: "application/json" } });
|
|
278
|
+
if (!response.ok) throw new Error(`npm attestation lookup failed: HTTP ${response.status}`);
|
|
279
|
+
attestations = await response.json();
|
|
280
|
+
}
|
|
281
|
+
const upstreamRelease = resolveNpmRegistryRelease({ source, channel, packageMetadata, attestations });
|
|
282
|
+
return createManualUpstreamPickupPlan({
|
|
283
|
+
config,
|
|
284
|
+
sourceId,
|
|
285
|
+
channel,
|
|
286
|
+
currentVersion: readFlag(args, "current-version"),
|
|
287
|
+
upstreamRelease,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function runPickupCli(args) {
|
|
292
|
+
const [mode = "", ...pickupArgs] = args;
|
|
293
|
+
if (!mode || mode === "--help" || mode === "-h") {
|
|
294
|
+
process.stdout.write(usage());
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (mode !== "plan" && mode !== "create") throw new Error(`unsupported release-propagation pickup command: ${mode}`);
|
|
298
|
+
const plan = await resolvePickupPlan(pickupArgs);
|
|
299
|
+
const result = mode === "create"
|
|
300
|
+
? createManualUpstreamPickupCapture({
|
|
301
|
+
plan,
|
|
302
|
+
expectedDownstreamBaseSha: readFlag(pickupArgs, "expected-downstream-base-sha"),
|
|
303
|
+
})
|
|
304
|
+
: plan;
|
|
305
|
+
writeOutput(readFlag(pickupArgs, "output", ""), result);
|
|
306
|
+
if (hasFlag(pickupArgs, "json")) printJson(result);
|
|
307
|
+
else if (mode === "plan") process.stdout.write(`manual upstream pickup: ${plan.source.id} ${plan.currentVersion} -> ${plan.resolvedVersion} (${plan.status})\n`);
|
|
308
|
+
else process.stdout.write(`manual upstream pickup next action: ${result.nextAction}\n`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function runReleasePropagationCli(argv = process.argv.slice(2)) {
|
|
175
312
|
const [mode = "", ...args] = argv;
|
|
176
313
|
if (!mode || mode === "--help" || mode === "-h") {
|
|
177
314
|
process.stdout.write(usage());
|
|
@@ -206,6 +343,14 @@ export function runReleasePropagationCli(argv = process.argv.slice(2)) {
|
|
|
206
343
|
runWorkCli(args);
|
|
207
344
|
return;
|
|
208
345
|
}
|
|
346
|
+
if (mode === "entry") {
|
|
347
|
+
runEntryCli(args);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (mode === "pickup") {
|
|
351
|
+
await runPickupCli(args);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
209
354
|
if (mode === "write-lock") {
|
|
210
355
|
const plan = readReleasePropagationJson(readFlag(args, "plan"), {
|
|
211
356
|
label: "--plan",
|
|
@@ -259,5 +404,8 @@ export function runReleasePropagationCli(argv = process.argv.slice(2)) {
|
|
|
259
404
|
}
|
|
260
405
|
|
|
261
406
|
if (!process.env.BUILDCHAIN_EMBEDDED_ENTRYPOINT && process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
262
|
-
runReleasePropagationCli()
|
|
407
|
+
runReleasePropagationCli().catch((error) => {
|
|
408
|
+
console.error(`buildchain release-propagation: ${error.message}`);
|
|
409
|
+
process.exitCode = 1;
|
|
410
|
+
});
|
|
263
411
|
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
parseExpectedArtifactsJson,
|
|
29
29
|
validateExpectedArtifacts,
|
|
30
30
|
} from "./build-contract-core.mjs";
|
|
31
|
+
import { verifyCompilerCacheActivity } from "./compiler-cache-evidence.mjs";
|
|
31
32
|
|
|
32
33
|
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
33
34
|
const buildchainCliCandidates = [
|
|
@@ -391,6 +392,22 @@ function writeDiagnosticsSidecarManifest(filePath, {
|
|
|
391
392
|
return manifest;
|
|
392
393
|
}
|
|
393
394
|
|
|
395
|
+
export function verifyBuildLifecycleCompilerCacheActivity({
|
|
396
|
+
stageName = "",
|
|
397
|
+
executed = false,
|
|
398
|
+
cwd = process.cwd(),
|
|
399
|
+
env = process.env,
|
|
400
|
+
verifier = verifyCompilerCacheActivity,
|
|
401
|
+
frameworkLog,
|
|
402
|
+
} = {}) {
|
|
403
|
+
if (stageName !== "build" || !executed) return undefined;
|
|
404
|
+
const activity = verifier({ cwd, env });
|
|
405
|
+
if (activity) {
|
|
406
|
+
frameworkLog?.info("compiler-cache.activity", { attributes: activity });
|
|
407
|
+
}
|
|
408
|
+
return activity;
|
|
409
|
+
}
|
|
410
|
+
|
|
394
411
|
export function runLifecycle({
|
|
395
412
|
cwd = process.cwd(),
|
|
396
413
|
stageName = "",
|
|
@@ -646,6 +663,13 @@ export function runLifecycle({
|
|
|
646
663
|
throw new Error(`required lifecycle stage did not run: ${stageName || "command"}`);
|
|
647
664
|
}
|
|
648
665
|
|
|
666
|
+
const compilerCacheActivity = verifyBuildLifecycleCompilerCacheActivity({
|
|
667
|
+
stageName,
|
|
668
|
+
executed,
|
|
669
|
+
cwd: resolvedCwd,
|
|
670
|
+
frameworkLog,
|
|
671
|
+
});
|
|
672
|
+
|
|
649
673
|
const shouldReadProcessSummary = Boolean(
|
|
650
674
|
resolvedProcessSummaryPath
|
|
651
675
|
&& (fs.existsSync(resolvedProcessSummaryPath) || processSummaryRequired),
|
|
@@ -739,6 +763,7 @@ export function runLifecycle({
|
|
|
739
763
|
fileCount: summary.fileCount,
|
|
740
764
|
});
|
|
741
765
|
observability.lifecycle = lifecycleObservability;
|
|
766
|
+
Object.assign(observability, { compilerCacheActivity });
|
|
742
767
|
observability.diagnostics = {
|
|
743
768
|
contract: BUILDCHAIN_DIAGNOSTICS_CONTRACT,
|
|
744
769
|
path: relativeDiagnosticsPath,
|
|
@@ -120,7 +120,7 @@ export function cliCommandMeta(id) {
|
|
|
120
120
|
"release-dry-run": { group: "governance-versioning", purpose: "Explain what a channel merge would publish before the PR is merged." },
|
|
121
121
|
"release-line-open": { group: "governance-versioning", purpose: "Plan or write the initial version-state commit for a new minor release line." },
|
|
122
122
|
"release-governance": { group: "governance-versioning", purpose: "Reconcile a managed branch's Buildchain aggregate check to an already-tested pull request SHA." },
|
|
123
|
-
"release-propagation": { group: "site-and-propagation", purpose: "
|
|
123
|
+
"release-propagation": { group: "site-and-propagation", purpose: "Route exact Paper, KFD, Buildchain, or Core Site updates through one resumable propagation Work." },
|
|
124
124
|
"release-transaction": { group: "release-passport-trust", purpose: "Inspect, recover, finalize, or abort durable release transactions." },
|
|
125
125
|
sample: { group: "observability-diagnostics", purpose: "Inspect sampler command families." },
|
|
126
126
|
"sample-process-tree": { group: "observability-diagnostics", purpose: "Sample process-tree diagnostics for a wrapped command." },
|
|
@@ -23,6 +23,20 @@ function bool(value, fallback = false) {
|
|
|
23
23
|
return ["1", "true", "yes", "on"].includes(String(value).trim().toLowerCase());
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function autoMergeMethod(value, fallback = "merge") {
|
|
27
|
+
const normalized = text(value || fallback).toLowerCase();
|
|
28
|
+
if (!["merge", "squash", "rebase"].includes(normalized)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`mergeMethod must be merge, squash, or rebase, got ${value || "<empty>"}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shouldEnableAutoMerge(options, pullRequest) {
|
|
37
|
+
return options.autoMerge && !pullRequest.auto_merge;
|
|
38
|
+
}
|
|
39
|
+
|
|
26
40
|
function list(value) {
|
|
27
41
|
return [...new Set(String(value || "").split(/[\n,]+/).map((entry) => entry.trim()).filter(Boolean))];
|
|
28
42
|
}
|
|
@@ -67,6 +81,7 @@ export function normalizeStableCandidatePatrolOptions(options = {}) {
|
|
|
67
81
|
releaseNow: text(options.releaseNow ?? process.env.BUILDCHAIN_STABLE_PATROL_RELEASE_NOW).replace(/^v/, ""),
|
|
68
82
|
autoPromote: bool(options.autoPromote ?? process.env.BUILDCHAIN_STABLE_PATROL_AUTO_PROMOTE, false),
|
|
69
83
|
autoMerge: bool(options.autoMerge ?? process.env.BUILDCHAIN_STABLE_PATROL_AUTO_MERGE, false),
|
|
84
|
+
mergeMethod: autoMergeMethod(options.mergeMethod ?? process.env.BUILDCHAIN_STABLE_PATROL_MERGE_METHOD),
|
|
70
85
|
dryRun: bool(options.dryRun ?? process.env.BUILDCHAIN_STABLE_PATROL_DRY_RUN, true),
|
|
71
86
|
now: text(options.now ?? process.env.BUILDCHAIN_STABLE_PATROL_NOW) || new Date().toISOString(),
|
|
72
87
|
outputPath: text(options.outputPath ?? process.env.BUILDCHAIN_STABLE_PATROL_OUTPUT_PATH) || ".buildchain/patrol/stable-candidate.json",
|
|
@@ -239,7 +254,9 @@ export async function runStableCandidatePatrol(optionsInput = {}, clientInput) {
|
|
|
239
254
|
"The source-lock branch freezes the exact candidate; newer alpha publications do not alter this PR.",
|
|
240
255
|
].join("\n"),
|
|
241
256
|
});
|
|
242
|
-
if (options
|
|
257
|
+
if (shouldEnableAutoMerge(options, pullRequest)) {
|
|
258
|
+
await client.enableAutoMerge(pullRequest, options.mergeMethod);
|
|
259
|
+
}
|
|
243
260
|
promotion.pullRequest = pullRequest;
|
|
244
261
|
const storedCandidate = ledger.candidates.find((entry) => entry.version === selection.candidate.version);
|
|
245
262
|
storedCandidate.promotionRequest = {
|
|
@@ -434,9 +451,18 @@ export function createGitHubStableCandidateClient({ repository: repositoryInput,
|
|
|
434
451
|
const open = await api(`/repos/${owner}/${repo}/pulls?state=open&head=${encodeURIComponent(`${owner}:${head}`)}&base=${encodeURIComponent(base)}&per_page=20`);
|
|
435
452
|
return open[0] || api(`/repos/${owner}/${repo}/pulls`, { method: "POST", body: { head, base, title, body } });
|
|
436
453
|
},
|
|
437
|
-
async enableAutoMerge(pullRequest) {
|
|
438
|
-
const query = `mutation($id:ID!){enablePullRequestAutoMerge(input:{pullRequestId:$id,mergeMethod
|
|
439
|
-
return api("/graphql", {
|
|
454
|
+
async enableAutoMerge(pullRequest, mergeMethod) {
|
|
455
|
+
const query = `mutation($id:ID!,$mergeMethod:PullRequestMergeMethod!){enablePullRequestAutoMerge(input:{pullRequestId:$id,mergeMethod:$mergeMethod}){pullRequest{url}}}`;
|
|
456
|
+
return api("/graphql", {
|
|
457
|
+
method: "POST",
|
|
458
|
+
body: {
|
|
459
|
+
query,
|
|
460
|
+
variables: {
|
|
461
|
+
id: pullRequest.node_id,
|
|
462
|
+
mergeMethod: autoMergeMethod(mergeMethod).toUpperCase(),
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
});
|
|
440
466
|
},
|
|
441
467
|
async setVariable(name, value) {
|
|
442
468
|
const current = await api(`/repos/${owner}/${repo}/actions/variables/${encodeURIComponent(name)}`, { allow404: true });
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { evaluateWorkflowCallContract } from "../packages/core/workflow-call-contract.js";
|
|
7
|
+
|
|
8
|
+
function usage() {
|
|
9
|
+
return `usage: node scripts/workflow-call-contract.mjs check \\
|
|
10
|
+
--caller-workflow <path> --job <id> --caller-repository <owner/repo> \\
|
|
11
|
+
--callee-root <checkout> --callee-workflow <path> --callee-repository <owner/repo> \\
|
|
12
|
+
--trusted-event <event[:type]> [--trusted-event ...] \\
|
|
13
|
+
[--expected-contract-root sha256:...] [--allow-dirty] [--output <path>]`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseArgs(argv) {
|
|
17
|
+
const options = { trustedEvents: [] };
|
|
18
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
19
|
+
const arg = argv[index];
|
|
20
|
+
if (arg === "--allow-dirty") {
|
|
21
|
+
options.allowDirty = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (!arg.startsWith("--") || !argv[index + 1])
|
|
25
|
+
throw new Error(`invalid argument: ${arg}`);
|
|
26
|
+
const value = argv[index + 1];
|
|
27
|
+
index += 1;
|
|
28
|
+
if (arg === "--trusted-event") options.trustedEvents.push(value);
|
|
29
|
+
else
|
|
30
|
+
options[
|
|
31
|
+
arg.slice(2).replace(/-([a-z])/g, (_, char) => char.toUpperCase())
|
|
32
|
+
] = value;
|
|
33
|
+
}
|
|
34
|
+
return options;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function git(root, ...args) {
|
|
38
|
+
return execFileSync("git", ["-C", root, ...args], {
|
|
39
|
+
encoding: "utf8",
|
|
40
|
+
}).trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function required(options, names) {
|
|
44
|
+
const missing = names.filter((name) => !options[name]);
|
|
45
|
+
if (missing.length)
|
|
46
|
+
throw new Error(`missing required options: ${missing.join(", ")}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function checkWorkflowCall(options) {
|
|
50
|
+
const callerRoot = path.resolve(options.callerRoot || process.cwd());
|
|
51
|
+
const calleeRoot = path.resolve(options.calleeRoot);
|
|
52
|
+
const callerStatus = git(
|
|
53
|
+
callerRoot,
|
|
54
|
+
"status",
|
|
55
|
+
"--porcelain",
|
|
56
|
+
"--untracked-files=no",
|
|
57
|
+
);
|
|
58
|
+
if (callerStatus && !options.allowDirty) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
"caller checkout is dirty; use --allow-dirty only for local diagnostic validation",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const calleeStatus = git(
|
|
64
|
+
calleeRoot,
|
|
65
|
+
"status",
|
|
66
|
+
"--porcelain",
|
|
67
|
+
"--untracked-files=no",
|
|
68
|
+
);
|
|
69
|
+
if (calleeStatus) {
|
|
70
|
+
throw new Error("callee checkout is dirty; exact pinned-ref bytes are required");
|
|
71
|
+
}
|
|
72
|
+
const callerSha = git(callerRoot, "rev-parse", "HEAD");
|
|
73
|
+
const callerTree = git(callerRoot, "rev-parse", "HEAD^{tree}");
|
|
74
|
+
const calleeSha = git(calleeRoot, "rev-parse", "HEAD");
|
|
75
|
+
const report = evaluateWorkflowCallContract({
|
|
76
|
+
callerText: fs.readFileSync(
|
|
77
|
+
path.join(callerRoot, options.callerWorkflow),
|
|
78
|
+
"utf8",
|
|
79
|
+
),
|
|
80
|
+
calleeText: fs.readFileSync(
|
|
81
|
+
path.join(calleeRoot, options.calleeWorkflow),
|
|
82
|
+
"utf8",
|
|
83
|
+
),
|
|
84
|
+
callerRepository: options.callerRepository,
|
|
85
|
+
callerWorkflowPath: options.callerWorkflow,
|
|
86
|
+
callerSha,
|
|
87
|
+
callerTree,
|
|
88
|
+
callerSourceState: callerStatus ? "diagnostic-dirty" : "clean",
|
|
89
|
+
calleeRepository: options.calleeRepository,
|
|
90
|
+
calleeWorkflowPath: options.calleeWorkflow,
|
|
91
|
+
calleeSha,
|
|
92
|
+
jobId: options.job,
|
|
93
|
+
trustedEventClasses: options.trustedEvents,
|
|
94
|
+
expectedContractRoot: options.expectedContractRoot || "",
|
|
95
|
+
});
|
|
96
|
+
if (options.output) {
|
|
97
|
+
const output = path.resolve(callerRoot, options.output);
|
|
98
|
+
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
99
|
+
fs.writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`);
|
|
100
|
+
}
|
|
101
|
+
return report;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function main(argv = process.argv.slice(2)) {
|
|
105
|
+
const command = argv.shift();
|
|
106
|
+
if (command !== "check") throw new Error(usage());
|
|
107
|
+
const options = parseArgs(argv);
|
|
108
|
+
required(options, [
|
|
109
|
+
"callerWorkflow",
|
|
110
|
+
"job",
|
|
111
|
+
"callerRepository",
|
|
112
|
+
"calleeRoot",
|
|
113
|
+
"calleeWorkflow",
|
|
114
|
+
"calleeRepository",
|
|
115
|
+
]);
|
|
116
|
+
if (!options.trustedEvents.length)
|
|
117
|
+
throw new Error("at least one --trusted-event is required");
|
|
118
|
+
const report = checkWorkflowCall(options);
|
|
119
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
120
|
+
if (!report.ok) process.exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (
|
|
124
|
+
process.argv[1] &&
|
|
125
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
126
|
+
) {
|
|
127
|
+
try {
|
|
128
|
+
main();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error(`workflow call contract: ${error.message}`);
|
|
131
|
+
process.exitCode = 1;
|
|
132
|
+
}
|
|
133
|
+
}
|