@kungfu-tech/buildchain 4.0.2-alpha.0 → 4.0.2-alpha.10
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/architecture/agent-change-map.md +12 -5
- package/architecture/ci-lane-change-budget.json +76 -8
- package/architecture/internal-capabilities.json +5 -1
- package/architecture/maintainability-debt.json +62 -132
- package/architecture/maintainability-policy.json +17 -12
- package/architecture/release-tail-contract-inventory.json +4 -4
- package/architecture/v3-core-mechanism-inventory.json +1 -0
- package/architecture/v3-v4-live-capability-inventory.json +17 -17
- package/architecture/v4-delivery-warrant-shadow-fixtures.json +5 -5
- package/architecture/v4-release-invocation-fixtures.json +139 -0
- package/architecture/v4-release-topology.json +316 -0
- package/architecture/v4-runtime-semantic-closure.json +4 -1
- package/contracts/buildchain-v2-residuals-v1.json +0 -9
- package/contracts/fixtures/v4-tail-reseal-v1/valid.json +1 -1
- package/contracts/v4-release-invocation-v1.schema.json +134 -0
- package/dist/site/buildchain-contract.json +9 -9
- package/dist/site/buildchain-site.json +7 -7
- package/dist/site/kfd-claims.json +26 -5
- package/dist/site/kfd-upstream-aggregate.json +1 -1
- package/dist/site/manual-registry.json +1 -1
- package/dist/site/node-api-registry.json +92 -17
- package/dist/site/page-registry.json +2 -2
- package/dist/site/public-surface-audit.json +36 -9
- package/dist/site/publication-authority-registry.json +2 -3
- package/dist/site/publication-registry.json +4 -4
- package/dist/site/site-manifest.json +5 -5
- package/dist/site/workflow-registry.json +38 -11
- package/docs/node-api-reference.md +19 -16
- package/package.json +2 -2
- package/packages/core/dev-delivery-candidate-identity.js +45 -17
- package/packages/core/dev-delivery-execution-transfer.js +6 -7
- package/packages/core/dev-delivery-provider-heartbeat.js +22 -6
- package/packages/core/dev-delivery-warrant-legacy-recovery.js +4 -2
- package/packages/core/dev-delivery-warrant-native-compatibility.js +33 -0
- package/packages/core/dev-delivery-warrant-state.js +54 -54
- package/packages/core/dev-delivery-warrant.js +8 -0
- package/packages/core/dev-delivery-writer-protocol-transition.js +72 -0
- package/packages/core/release-tail-product-capabilities.js +29 -0
- package/packages/core/release-tail-provider-plane.js +2 -2
- package/packages/core/v4-canonical-contracts.js +14 -0
- package/packages/core/v4-floating-consumer-policy.js +4 -1
- package/packages/core/v4-product-publication.js +387 -0
- package/packages/core/v4-protected-publication-source.js +93 -0
- package/packages/core/v4-publication-qualification.js +1 -0
- package/packages/core/v4-release-invocation.js +425 -0
- package/scripts/audit-publication-control-plane.mjs +0 -1
- package/scripts/check-inventory.mjs +27 -59
- package/scripts/check-maintainability.mjs +13 -5
- package/scripts/check-v3-v4-capability-inventory.mjs +3 -61
- package/scripts/check-v4-floating-consumer-policy-contract.mjs +10 -20
- package/scripts/check-v4-release-topology.mjs +490 -0
- package/scripts/dev-delivery-warrant.mjs +26 -5
- package/scripts/generate-channel-promotion-workflow.mjs +14 -55
- package/scripts/release-candidate-resolver.mjs +23 -24
- package/scripts/resume-from-candidate-run.mjs +16 -17
- package/scripts/v3-v4-capability-catalog.mjs +107 -0
- package/scripts/v4-declarative-promotion-admission.mjs +4 -1
- package/scripts/v4-product-publication-intent.mjs +114 -0
- package/scripts/v4-release-candidate-adapter.mjs +41 -0
- package/scripts/capture-package-release-propagation.mjs +0 -263
- package/scripts/publication-commit-evidence.mjs +0 -444
- package/scripts/publish-github-artifact-attestation-evidence.mjs +0 -201
- package/scripts/stage-github-artifact-attestation-inputs.mjs +0 -65
|
@@ -74,7 +74,7 @@ function publicInputs(inputBlock) {
|
|
|
74
74
|
function publicOutputs(outputBlock) {
|
|
75
75
|
const forwarded = entries(outputBlock).map((entry) => entry.source.replace(
|
|
76
76
|
/^ value:.*$/m,
|
|
77
|
-
` value: \${{ jobs.
|
|
77
|
+
` value: \${{ jobs.invoke.outputs.${entry.name} }}`,
|
|
78
78
|
));
|
|
79
79
|
const routed = [
|
|
80
80
|
["buildchain-channel", "Selected promotion workflow-shell channel", "channel"],
|
|
@@ -214,8 +214,9 @@ function consumerAdmissionJob() {
|
|
|
214
214
|
id: policy
|
|
215
215
|
env:
|
|
216
216
|
BUILDCHAIN_CONSUMER_ROOT: .buildchain/consumer
|
|
217
|
+
BUILDCHAIN_INVOCATION_SOURCE_PATH: \${{ inputs.publication-publisher-workflow-path == '.github/workflows/buildchain-ref-promotion-recovery.yml' && '.github/workflows/release-candidate-promote.yml' || inputs.publication-publisher-workflow-path }}
|
|
217
218
|
BUILDCHAIN_EXPECTED_INVOCATION_CHANNEL: \${{ needs.resolve-promotion.outputs.channel }}
|
|
218
|
-
BUILDCHAIN_INVOKED_WORKFLOW: .github/workflows/release-candidate-promote.yml
|
|
219
|
+
BUILDCHAIN_INVOKED_WORKFLOW: \${{ inputs.publication-publisher-workflow-path == '.github/workflows/buildchain-ref-promotion-recovery.yml' && '.github/workflows/.release-candidate-promote.yml' || '.github/workflows/release-candidate-promote.yml' }}
|
|
219
220
|
BUILDCHAIN_WORKFLOW_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
|
|
220
221
|
BUILDCHAIN_RUNTIME_SHA: \${{ needs.resolve-promotion.outputs.router-sha }}
|
|
221
222
|
BUILDCHAIN_STABLE_CONTRACT_LOCK_PATH: \${{ inputs.buildchain-stable-contract-lock-path }}
|
|
@@ -292,7 +293,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
|
|
|
292
293
|
},
|
|
293
294
|
};
|
|
294
295
|
const alphaRoute = validateWorkflowRoute("alpha", routes.alpha, `v${major}-alpha`);
|
|
295
|
-
|
|
296
|
+
validateWorkflowRoute("stable", routes.stable, `v${major}`);
|
|
296
297
|
const inputs = blockBetween(source, " inputs:\n", " secrets:\n");
|
|
297
298
|
const secrets = blockBetween(source, " secrets:\n", " outputs:\n");
|
|
298
299
|
const outputs = blockBetween(source, " outputs:\n", "\nconcurrency:\n");
|
|
@@ -300,8 +301,7 @@ export function generateChannelPromotionWorkflow(source, { major = 2, shellRouti
|
|
|
300
301
|
for (const required of ["buildchain-ref", "buildchain-contract-lock-path", "channel", "target-ref", ...internalInputs]) {
|
|
301
302
|
if (!inputNames.includes(required)) throw new Error(`advanced promotion workflow missing input: ${required}`);
|
|
302
303
|
}
|
|
303
|
-
const
|
|
304
|
-
const stableForwarded = routedInputs(inputNames, stableRoute, major);
|
|
304
|
+
const invokeForwarded = routedInputs(inputNames, alphaRoute, major);
|
|
305
305
|
return `# Generated by scripts/generate-channel-promotion-workflow.mjs. Do not edit directly.
|
|
306
306
|
name: Release Candidate Promote
|
|
307
307
|
|
|
@@ -336,7 +336,7 @@ jobs:
|
|
|
336
336
|
target-ref: \${{ steps.route.outputs.target-ref }}
|
|
337
337
|
router-ref: \${{ steps.router.outputs.ref }}
|
|
338
338
|
router-sha: \${{ steps.router.outputs.sha }}
|
|
339
|
-
shell-ref: \${{ steps.
|
|
339
|
+
shell-ref: \${{ steps.identities.outputs.shell-ref }}
|
|
340
340
|
shell-call-ref: \${{ steps.identities.outputs.shell-call-ref }}
|
|
341
341
|
shell-sha: \${{ steps.identities.outputs.shell-sha }}
|
|
342
342
|
runtime-ref: \${{ steps.route.outputs.runtime-ref }}
|
|
@@ -425,32 +425,17 @@ jobs:
|
|
|
425
425
|
shell: bash
|
|
426
426
|
env:
|
|
427
427
|
GITHUB_TOKEN: \${{ github.token }}
|
|
428
|
-
|
|
429
|
-
SELECTED_SHELL_REF: \${{ steps.route.outputs.shell-ref }}
|
|
430
|
-
ALPHA_SHELL_REF: ${alphaRoute.logicalRef}
|
|
431
|
-
ALPHA_SHELL_CALL_REF: ${alphaRoute.callRef}
|
|
432
|
-
STABLE_SHELL_REF: ${stableRoute.logicalRef}
|
|
433
|
-
STABLE_SHELL_CALL_REF: ${stableRoute.callRef}
|
|
428
|
+
SELECTED_SHELL_REF: ${alphaRoute.logicalRef}
|
|
434
429
|
run: |
|
|
435
430
|
set -euo pipefail
|
|
436
|
-
if [[ "\${CHANNEL}" = "alpha" ]]; then
|
|
437
|
-
expected_ref="\${ALPHA_SHELL_REF}"
|
|
438
|
-
call_ref="\${ALPHA_SHELL_CALL_REF}"
|
|
439
|
-
else
|
|
440
|
-
expected_ref="\${STABLE_SHELL_REF}"
|
|
441
|
-
call_ref="\${STABLE_SHELL_CALL_REF}"
|
|
442
|
-
fi
|
|
443
|
-
if [[ "\${SELECTED_SHELL_REF}" != "\${expected_ref}" ]]; then
|
|
444
|
-
echo "::error::Selected promotion shell ref \${SELECTED_SHELL_REF} does not match configured \${expected_ref}"
|
|
445
|
-
exit 1
|
|
446
|
-
fi
|
|
447
431
|
node .buildchain/router/scripts/promotion-identity-resolver.mjs \\
|
|
448
432
|
--repository "\${{ steps.router.outputs.repository }}" \\
|
|
449
433
|
--router-ref "\${{ steps.router.outputs.ref }}" \\
|
|
450
434
|
--router-sha "\${{ steps.router.outputs.sha }}" \\
|
|
451
435
|
--shell-ref "\${SELECTED_SHELL_REF}" \\
|
|
452
|
-
--shell-call-ref "\${
|
|
436
|
+
--shell-call-ref "\${SELECTED_SHELL_REF}" \\
|
|
453
437
|
--runtime-ref "\${{ steps.route.outputs.runtime-ref }}"
|
|
438
|
+
echo "shell-ref=\${SELECTED_SHELL_REF}" >> "$GITHUB_OUTPUT"
|
|
454
439
|
|
|
455
440
|
- name: Checkout selected promotion workflow shell
|
|
456
441
|
uses: actions/checkout@v7.0.0
|
|
@@ -500,29 +485,21 @@ jobs:
|
|
|
500
485
|
- name: Verify immutable promotion checkouts
|
|
501
486
|
shell: bash
|
|
502
487
|
env:
|
|
503
|
-
|
|
504
|
-
ALPHA_SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
|
|
505
|
-
STABLE_SHELL_WORKFLOW_PATH: ${stableRoute.workflowPath}
|
|
488
|
+
SHELL_WORKFLOW_PATH: ${alphaRoute.workflowPath}
|
|
506
489
|
ROUTER_SHA: \${{ steps.router.outputs.sha }}
|
|
507
490
|
SHELL_SHA: \${{ steps.identities.outputs.shell-sha }}
|
|
508
491
|
RUNTIME_SHA: \${{ steps.identities.outputs.runtime-sha }}
|
|
509
492
|
run: |
|
|
510
493
|
set -euo pipefail
|
|
511
|
-
|
|
512
|
-
workflow_path="\${ALPHA_SHELL_WORKFLOW_PATH}"
|
|
513
|
-
else
|
|
514
|
-
workflow_path="\${STABLE_SHELL_WORKFLOW_PATH}"
|
|
515
|
-
fi
|
|
516
|
-
test -f ".buildchain/shell/\${workflow_path}"
|
|
494
|
+
test -f ".buildchain/shell/\${SHELL_WORKFLOW_PATH}"
|
|
517
495
|
[[ "$(git -C .buildchain/router rev-parse HEAD)" = "\${ROUTER_SHA}" ]] || { echo "::error::Promotion router checkout moved"; exit 1; }
|
|
518
496
|
[[ "$(git -C .buildchain/shell rev-parse HEAD)" = "\${SHELL_SHA}" ]] || { echo "::error::Promotion shell checkout moved"; exit 1; }
|
|
519
497
|
[[ "$(git -C .buildchain/runtime rev-parse HEAD)" = "\${RUNTIME_SHA}" ]] || { echo "::error::Promotion runtime checkout moved"; exit 1; }
|
|
520
498
|
|
|
521
499
|
${consumerAdmissionJob()}
|
|
522
|
-
|
|
523
|
-
name:
|
|
500
|
+
invoke:
|
|
501
|
+
name: Invoke the single v4 publisher adapter
|
|
524
502
|
needs: [resolve-promotion, consumer-admission]
|
|
525
|
-
if: \${{ needs.resolve-promotion.outputs.channel == 'alpha' }}
|
|
526
503
|
uses: kungfu-systems/buildchain/${alphaRoute.workflowPath}@${alphaRoute.callRef}
|
|
527
504
|
permissions:
|
|
528
505
|
actions: write
|
|
@@ -534,25 +511,7 @@ ${consumerAdmissionJob()}
|
|
|
534
511
|
issues: write
|
|
535
512
|
pull-requests: write
|
|
536
513
|
with:
|
|
537
|
-
${
|
|
538
|
-
secrets: inherit
|
|
539
|
-
|
|
540
|
-
stable:
|
|
541
|
-
name: Promote with stable workflow shell
|
|
542
|
-
needs: [resolve-promotion, consumer-admission]
|
|
543
|
-
if: \${{ needs.resolve-promotion.outputs.channel == 'stable' }}
|
|
544
|
-
uses: kungfu-systems/buildchain/${stableRoute.workflowPath}@${stableRoute.callRef}
|
|
545
|
-
permissions:
|
|
546
|
-
actions: write
|
|
547
|
-
artifact-metadata: write
|
|
548
|
-
attestations: write
|
|
549
|
-
checks: write
|
|
550
|
-
contents: write
|
|
551
|
-
id-token: write
|
|
552
|
-
issues: write
|
|
553
|
-
pull-requests: write
|
|
554
|
-
with:
|
|
555
|
-
${stableForwarded}
|
|
514
|
+
${invokeForwarded}
|
|
556
515
|
secrets: inherit
|
|
557
516
|
`;
|
|
558
517
|
}
|
|
@@ -13,7 +13,6 @@ import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
|
|
|
13
13
|
import { v4PublicationQualificationRoot, validateV4PublicationQualificationReceipt } from "../packages/core/v4-publication-qualification.js";
|
|
14
14
|
|
|
15
15
|
const DEFAULT_WORKFLOW_FILE = "build-surface-fixture.yml";
|
|
16
|
-
|
|
17
16
|
function env(name, fallback = "") {
|
|
18
17
|
return process.env[name] || fallback;
|
|
19
18
|
}
|
|
@@ -21,7 +20,6 @@ function env(name, fallback = "") {
|
|
|
21
20
|
export function releaseCandidateDownloadEnabled(value = "true") {
|
|
22
21
|
return String(value || "true").trim().toLowerCase() !== "false";
|
|
23
22
|
}
|
|
24
|
-
|
|
25
23
|
function splitRepository(repository) {
|
|
26
24
|
const match = String(repository || "").trim().match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
27
25
|
if (!match) {
|
|
@@ -44,7 +42,9 @@ function assertSha(value, label = "sha") {
|
|
|
44
42
|
|
|
45
43
|
export const releaseCandidateRuntimeSha = (passport) =>
|
|
46
44
|
assertSha(passport?.buildchain?.sha, "release candidate Passport Buildchain runtime SHA").toLowerCase();
|
|
47
|
-
|
|
45
|
+
export const resolveFreshPublicationVersion = ({ sealedBundle, candidateVersion = "" } = {}) =>
|
|
46
|
+
String(sealedBundle?.manifest?.npm?.version || candidateVersion || "").trim();
|
|
47
|
+
const optionalText = (value) => String(value || "");
|
|
48
48
|
function githubHeaders(token) {
|
|
49
49
|
const headers = {
|
|
50
50
|
accept: "application/vnd.github+json",
|
|
@@ -110,13 +110,13 @@ function digestFileSync(filePath, algorithm, encoding) {
|
|
|
110
110
|
return hash.digest(encoding);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
export function selectMergedChannelPullRequest({ pullRequests = [], targetRef, repository }) {
|
|
113
|
+
export function selectMergedChannelPullRequest({ pullRequests = [], targetRef, targetSha = "", repository }) {
|
|
114
114
|
const normalizedTarget = normalizeBranch(targetRef);
|
|
115
115
|
const candidates = pullRequests.filter((pr) => {
|
|
116
|
-
const
|
|
117
|
-
const merged = Boolean(pr.merged_at || pr.mergedAt || pr.
|
|
118
|
-
const
|
|
119
|
-
return merged &&
|
|
116
|
+
const baseRepo = pr.base?.repo?.full_name || pr.baseRepository?.nameWithOwner;
|
|
117
|
+
const merged = Boolean(pr.merged_at || pr.mergedAt || pr.merged === true);
|
|
118
|
+
const rooted = !repository || (baseRepo || pr.head?.repo?.full_name) === repository;
|
|
119
|
+
return merged && rooted && (!targetSha || (pr.merge_commit_sha || pr.mergeCommit?.oid) === targetSha) && normalizeBranch(pr.base?.ref || pr.baseRefName || "") === normalizedTarget;
|
|
120
120
|
});
|
|
121
121
|
candidates.sort((left, right) => {
|
|
122
122
|
const leftTime = Date.parse(left.merged_at || left.updated_at || left.closed_at || "");
|
|
@@ -513,11 +513,11 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
513
513
|
});
|
|
514
514
|
const channelPullRequest = selectMergedChannelPullRequest({
|
|
515
515
|
pullRequests: Array.isArray(pulls) ? pulls : [],
|
|
516
|
-
targetRef: normalizedTarget,
|
|
516
|
+
targetRef: normalizedTarget, targetSha: sha,
|
|
517
517
|
repository: repoInfo.fullName,
|
|
518
518
|
});
|
|
519
519
|
if (!channelPullRequest) {
|
|
520
|
-
throw new Error(`no
|
|
520
|
+
throw new Error(`no exact merged channel PR rooted in ${repoInfo.fullName} found for ${sha} into ${normalizedTarget}`);
|
|
521
521
|
}
|
|
522
522
|
let pullRequest = channelPullRequest;
|
|
523
523
|
if (majorGateTarget) {
|
|
@@ -725,16 +725,6 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
725
725
|
const noun = publishArtifactKind === "npm" ? "npm package tarballs" : "platform manifests";
|
|
726
726
|
throw new Error(`expected at least ${minimumPayloadCount} downloaded ${noun}, found ${downloadedRequiredArtifactCount}`);
|
|
727
727
|
}
|
|
728
|
-
const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8")));
|
|
729
|
-
const generatedRequiredArtifacts = generatePublishRequiredArtifacts({
|
|
730
|
-
manifests,
|
|
731
|
-
version: passport.target?.version || "",
|
|
732
|
-
kind: publishArtifactKind,
|
|
733
|
-
tarballPaths: npmTarballPaths,
|
|
734
|
-
mainPackage: publishPackageMain,
|
|
735
|
-
});
|
|
736
|
-
const requiredArtifactsPath = path.join(resolvedOutput, "publish-required-artifacts.json");
|
|
737
|
-
fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(generatedRequiredArtifacts, null, 2)}\n`);
|
|
738
728
|
const sealedBundle = publishArtifactKind === "npm"
|
|
739
729
|
? createResolvedPublicationSealedBundle({
|
|
740
730
|
bundleRoot: payloadDir,
|
|
@@ -750,9 +740,17 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
750
740
|
releaseAssetPaths,
|
|
751
741
|
})
|
|
752
742
|
: undefined;
|
|
753
|
-
const
|
|
754
|
-
|
|
755
|
-
|
|
743
|
+
const manifests = platformManifestPaths.map((manifestPath) => JSON.parse(fs.readFileSync(manifestPath, "utf8"))), publicationVersion = resolveFreshPublicationVersion({ sealedBundle, candidateVersion: passport.target?.version });
|
|
744
|
+
const generatedRequiredArtifacts = generatePublishRequiredArtifacts({
|
|
745
|
+
manifests,
|
|
746
|
+
version: publicationVersion,
|
|
747
|
+
kind: publishArtifactKind,
|
|
748
|
+
tarballPaths: npmTarballPaths,
|
|
749
|
+
mainPackage: publishPackageMain,
|
|
750
|
+
});
|
|
751
|
+
const requiredArtifactsPath = path.join(resolvedOutput, "publish-required-artifacts.json");
|
|
752
|
+
fs.writeFileSync(requiredArtifactsPath, `${JSON.stringify(generatedRequiredArtifacts, null, 2)}\n`);
|
|
753
|
+
const sealedBundleManifestPath = sealedBundle ? path.join(resolvedOutput, "sealed-bundle.json") : "";
|
|
756
754
|
if (sealedBundleManifestPath) {
|
|
757
755
|
fs.writeFileSync(sealedBundleManifestPath, `${JSON.stringify(sealedBundle.manifest, null, 2)}\n`);
|
|
758
756
|
}
|
|
@@ -772,6 +770,7 @@ export async function resolveReleaseCandidateArtifacts({
|
|
|
772
770
|
sealedBundleManifest: sealedBundleManifestPath ? outputPath(sealedBundleManifestPath) : "",
|
|
773
771
|
},
|
|
774
772
|
version: passport.target?.version || "",
|
|
773
|
+
publicationVersion,
|
|
775
774
|
candidateHash: passport.candidateHash || "",
|
|
776
775
|
payloadCount: payloadArtifacts.length,
|
|
777
776
|
platformManifestCount: platformManifestPaths.length,
|
|
@@ -811,6 +810,7 @@ export async function resolveReleaseCandidateArtifactsCli() {
|
|
|
811
810
|
"release-candidate-publication-qualification-path": result.paths?.publicationQualification || "",
|
|
812
811
|
"release-candidate-publication-qualification-root": result.publicationQualificationRoot || "",
|
|
813
812
|
"release-candidate-version": result.version || "",
|
|
813
|
+
"release-candidate-publication-version": optionalText(result.publicationVersion),
|
|
814
814
|
"release-candidate-source-sha": result.artifacts?.sourceSha || "",
|
|
815
815
|
"release-candidate-artifact": result.artifacts?.passport || "",
|
|
816
816
|
"release-candidate-build-summary-artifact": result.artifacts?.summary || "",
|
|
@@ -843,7 +843,6 @@ export async function resolveReleaseCandidateArtifactsCli() {
|
|
|
843
843
|
console.log(JSON.stringify(result, null, 2));
|
|
844
844
|
return result;
|
|
845
845
|
}
|
|
846
|
-
|
|
847
846
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
848
847
|
try {
|
|
849
848
|
await resolveReleaseCandidateArtifactsCli();
|
|
@@ -785,14 +785,13 @@ async function recoverCandidateEvidence({
|
|
|
785
785
|
for (const artifact of [selected.passport, selected.summary]) initialDownloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
|
|
786
786
|
const passport = readOnlyJson(initialDownloads[0].files.filter((file) => path.basename(file.path) === "release-candidate-passport.json"), "release-candidate-passport.json");
|
|
787
787
|
const buildSummary = readOnlyJson(initialDownloads[1].files.filter((file) => path.basename(file.path) === "build-summary.json"), "build-summary.json");
|
|
788
|
-
const
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
);
|
|
792
|
-
const stageCapsuleSidecar =
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
: undefined;
|
|
788
|
+
const [stageCapsuleFile, publicationQualificationFile] = [
|
|
789
|
+
"release-candidate-stage-capsules.json",
|
|
790
|
+
"release-candidate-publication-qualification.json",
|
|
791
|
+
].map((name) => initialDownloads[0].files.find((file) => path.basename(file.path) === name));
|
|
792
|
+
const stageCapsuleSidecar = stageCapsuleFile
|
|
793
|
+
? readOnlyJson([stageCapsuleFile], "release-candidate-stage-capsules.json")
|
|
794
|
+
: undefined;
|
|
796
795
|
const { names: requiredNames, publicationNames } = candidateArtifactNames({ passport, selected, artifacts, artifactPatterns });
|
|
797
796
|
const chosen = artifacts.filter((artifact) => requiredNames.has(artifact.name));
|
|
798
797
|
if (chosen.length !== requiredNames.size) {
|
|
@@ -804,7 +803,7 @@ async function recoverCandidateEvidence({
|
|
|
804
803
|
for (const artifact of chosen.filter((entry) => ![selected.passport.id, selected.summary.id].includes(entry.id))) downloads.push(await downloadArtifact({ artifact, repoInfo, apiUrl, token, archiveDir, bundleRoot, fetchImpl }));
|
|
805
804
|
return {
|
|
806
805
|
run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
|
|
807
|
-
passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
|
|
806
|
+
passport, buildSummary, stageCapsuleSidecar, stageCapsuleFile, publicationQualificationFile, chosen, downloads, publicationNames,
|
|
808
807
|
};
|
|
809
808
|
}
|
|
810
809
|
|
|
@@ -822,7 +821,7 @@ export async function resolveRecoveryTransaction({
|
|
|
822
821
|
transactionId,
|
|
823
822
|
publicationVersion,
|
|
824
823
|
}) {
|
|
825
|
-
let transaction = publicationVersion
|
|
824
|
+
let transaction = transactionId && publicationVersion
|
|
826
825
|
? await readExistingTransaction({
|
|
827
826
|
repoInfo,
|
|
828
827
|
apiUrl,
|
|
@@ -878,7 +877,7 @@ export async function resumeFromCandidateRun({
|
|
|
878
877
|
try {
|
|
879
878
|
const {
|
|
880
879
|
run, workflow, selected, resolvedOutput, bundleRoot, initialDownloads,
|
|
881
|
-
passport, buildSummary, stageCapsuleSidecar, chosen, downloads, publicationNames,
|
|
880
|
+
passport, buildSummary, stageCapsuleSidecar, stageCapsuleFile, publicationQualificationFile, chosen, downloads, publicationNames,
|
|
882
881
|
} = await recoverCandidateEvidence({
|
|
883
882
|
repoInfo, runId, artifactName, artifactPatterns, requiredArtifactCount,
|
|
884
883
|
outputDir, apiUrl, token, fetchImpl, archiveDir,
|
|
@@ -1023,9 +1022,9 @@ export async function resumeFromCandidateRun({
|
|
|
1023
1022
|
sealedBundleRoot: publication.manifest ? outputPath(bundleRoot) : "",
|
|
1024
1023
|
sealedBundleManifest: publication.manifest ? outputPath(sealedManifestPath) : "",
|
|
1025
1024
|
recoveryReceipt: outputPath(recoveryReceiptPath),
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1025
|
+
stageCapsules: stageCapsuleFile ? outputPath(stageCapsuleFile.absolutePath) : "",
|
|
1026
|
+
publicationQualification: publicationQualificationFile ? outputPath(publicationQualificationFile.absolutePath) : "",
|
|
1027
|
+
runtimeResumeEvidence: runtimeResumeEvidencePath ? outputPath(runtimeResumeEvidencePath) : "",
|
|
1029
1028
|
},
|
|
1030
1029
|
};
|
|
1031
1030
|
} finally {
|
|
@@ -1077,10 +1076,10 @@ export async function resumeFromCandidateRunCli() {
|
|
|
1077
1076
|
"release-candidate-run-url": result.run.url,
|
|
1078
1077
|
"release-candidate-recovery-receipt-path": result.paths.recoveryReceipt,
|
|
1079
1078
|
"release-candidate-recovery-root": result.receipt.root,
|
|
1079
|
+
"release-candidate-stage-capsules-path": result.paths.stageCapsules,
|
|
1080
|
+
"release-candidate-publication-qualification-path": result.paths.publicationQualification,
|
|
1080
1081
|
"v4-runtime-resume-evidence-path": result.paths.runtimeResumeEvidence,
|
|
1081
|
-
"v4-runtime-resume-finalize-command": result.paths.runtimeResumeEvidence
|
|
1082
|
-
? "node .buildchain/runtime/promotion-shell/scripts/resume-from-candidate-run.mjs finalize"
|
|
1083
|
-
: "",
|
|
1082
|
+
"v4-runtime-resume-finalize-command": result.paths.runtimeResumeEvidence ? "node .buildchain/runtime/promotion-shell/scripts/resume-from-candidate-run.mjs finalize" : "",
|
|
1084
1083
|
"release-candidate-root": result.candidateRoot,
|
|
1085
1084
|
"release-candidate-artifact-root": result.artifactRoot,
|
|
1086
1085
|
"publish-sealed-bundle-root": result.paths.sealedBundleRoot,
|
|
@@ -23,6 +23,9 @@ const PLATFORM_MARKERS = Object.freeze({
|
|
|
23
23
|
"self-hosted": /\bself-hosted\b/iu,
|
|
24
24
|
windows: /\bwindows\b/iu,
|
|
25
25
|
});
|
|
26
|
+
const LIVE_V4_PROTECTED_BRANCH = "refs/heads/dev/v4/v4.0";
|
|
27
|
+
const LIVE_V4_PROTECTED_LINEAGE_REF = "refs/remotes/origin/dev/v4/v4.0";
|
|
28
|
+
const PROTECTED_LINEAGE_DEPTH = 256;
|
|
26
29
|
|
|
27
30
|
export function sha256(value) {
|
|
28
31
|
return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
@@ -47,6 +50,110 @@ export function git(root, args, { trim = true } = {}) {
|
|
|
47
50
|
return trim ? output.trim() : output;
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
export function assertCapabilityCutAncestor({
|
|
54
|
+
root = process.cwd(),
|
|
55
|
+
revision,
|
|
56
|
+
descendant = "HEAD",
|
|
57
|
+
label = "capability cut",
|
|
58
|
+
} = {}) {
|
|
59
|
+
try {
|
|
60
|
+
execFileSync("git", ["merge-base", "--is-ancestor", revision, descendant], {
|
|
61
|
+
cwd: root,
|
|
62
|
+
stdio: "ignore",
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`${label} ${revision} must be an ancestor of ${descendant}; regenerate the cut after rebasing instead of relying on a retained local object`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function ensureCapabilityCutAncestor({
|
|
72
|
+
root = process.cwd(),
|
|
73
|
+
revision,
|
|
74
|
+
descendant = "HEAD",
|
|
75
|
+
label = "capability cut",
|
|
76
|
+
} = {}) {
|
|
77
|
+
try {
|
|
78
|
+
assertCapabilityCutAncestor({ root, revision, descendant, label });
|
|
79
|
+
return;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (git(root, ["rev-parse", "--is-shallow-repository"]) !== "true")
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
const descendantCommit = git(root, ["rev-parse", `${descendant}^{commit}`]);
|
|
85
|
+
try {
|
|
86
|
+
execFileSync(
|
|
87
|
+
"git",
|
|
88
|
+
["fetch", "--no-tags", "--depth=128", "origin", descendantCommit],
|
|
89
|
+
{ cwd: root, stdio: "ignore" },
|
|
90
|
+
);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`${label} ${revision} ancestry could not be hydrated from ${descendantCommit} through a bounded origin fetch`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
assertCapabilityCutAncestor({ root, revision, descendant, label });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function protectedV4TreeWitness(root, descendant) {
|
|
100
|
+
try {
|
|
101
|
+
execFileSync(
|
|
102
|
+
"git",
|
|
103
|
+
[
|
|
104
|
+
"fetch",
|
|
105
|
+
"--no-tags",
|
|
106
|
+
`--depth=${PROTECTED_LINEAGE_DEPTH}`,
|
|
107
|
+
"origin",
|
|
108
|
+
`${LIVE_V4_PROTECTED_BRANCH}:${LIVE_V4_PROTECTED_LINEAGE_REF}`,
|
|
109
|
+
],
|
|
110
|
+
{ cwd: root, stdio: "ignore" },
|
|
111
|
+
);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`protected v4 lineage ${LIVE_V4_PROTECTED_BRANCH} could not be hydrated through a bounded origin fetch`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const tree = git(root, ["rev-parse", `${descendant}^{tree}`]);
|
|
118
|
+
return (
|
|
119
|
+
git(root, [
|
|
120
|
+
"log",
|
|
121
|
+
`--max-count=${PROTECTED_LINEAGE_DEPTH}`,
|
|
122
|
+
"--format=%H %T",
|
|
123
|
+
LIVE_V4_PROTECTED_LINEAGE_REF,
|
|
124
|
+
])
|
|
125
|
+
.split("\n")
|
|
126
|
+
.map((row) => row.split(" "))
|
|
127
|
+
.find(([, candidateTree]) => candidateTree === tree)?.[0] || ""
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function ensureCapabilityCutLineage({
|
|
132
|
+
root = process.cwd(),
|
|
133
|
+
revision,
|
|
134
|
+
descendant = "HEAD",
|
|
135
|
+
label = "capability cut",
|
|
136
|
+
} = {}) {
|
|
137
|
+
try {
|
|
138
|
+
ensureCapabilityCutAncestor({ root, revision, descendant, label });
|
|
139
|
+
return { mode: "direct-ancestry", witness: descendant };
|
|
140
|
+
} catch (directError) {
|
|
141
|
+
const witness = protectedV4TreeWitness(root, descendant);
|
|
142
|
+
if (!witness)
|
|
143
|
+
throw new Error(
|
|
144
|
+
`${label} ${revision} is not an ancestor of ${descendant}, and ${descendant} has no tree-equivalent commit in the bounded protected v4 lineage`,
|
|
145
|
+
{ cause: directError },
|
|
146
|
+
);
|
|
147
|
+
ensureCapabilityCutAncestor({
|
|
148
|
+
root,
|
|
149
|
+
revision,
|
|
150
|
+
descendant: witness,
|
|
151
|
+
label: `${label} protected-lineage witness`,
|
|
152
|
+
});
|
|
153
|
+
return { mode: "protected-tree-equivalent", witness };
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
50
157
|
function gitJson(root, revision, relPath) {
|
|
51
158
|
return JSON.parse(
|
|
52
159
|
git(root, ["show", `${revision}:${relPath}`], { trim: false }),
|
|
@@ -8,7 +8,10 @@ export function admitV4DeclarativePromotion({
|
|
|
8
8
|
runtimeRef,
|
|
9
9
|
declarative,
|
|
10
10
|
}) {
|
|
11
|
-
if (
|
|
11
|
+
if (
|
|
12
|
+
declarative !== true &&
|
|
13
|
+
!/^v4(?:$|[-./])/u.test(String(runtimeRef || ""))
|
|
14
|
+
) {
|
|
12
15
|
return Object.freeze({ mode: "legacy", admitted: true });
|
|
13
16
|
}
|
|
14
17
|
if (declarative !== true) {
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { spawnSyncCommand } from "../packages/core/spawn-command.js";
|
|
8
|
+
import { selectV4ProductPublicationIntent } from "../packages/core/v4-product-publication.js";
|
|
9
|
+
import { v4ContentRoot } from "../packages/core/v4-canonical-contracts.js";
|
|
10
|
+
|
|
11
|
+
function env(name, required = false) {
|
|
12
|
+
const value = String(process.env[name] || "").trim();
|
|
13
|
+
if (required && !value) throw new Error(`${name} is required`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function readJson(file) {
|
|
18
|
+
return JSON.parse(fs.readFileSync(path.resolve(file), "utf8"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function packageName() {
|
|
22
|
+
const declared = env("BUILDCHAIN_PUBLISH_PACKAGE_MAIN");
|
|
23
|
+
if (declared) return declared;
|
|
24
|
+
const manifestPath = env("BUILDCHAIN_SEALED_BUNDLE_MANIFEST", true);
|
|
25
|
+
const manifest = readJson(manifestPath);
|
|
26
|
+
const selected = String(manifest?.npm?.name || "").trim();
|
|
27
|
+
if (!selected)
|
|
28
|
+
throw new Error("sealed bundle manifest does not declare npm.name");
|
|
29
|
+
return selected;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function observedVersions(name) {
|
|
33
|
+
const result = spawnSyncCommand(
|
|
34
|
+
"npm",
|
|
35
|
+
[
|
|
36
|
+
"view",
|
|
37
|
+
name,
|
|
38
|
+
"versions",
|
|
39
|
+
"--json",
|
|
40
|
+
"--registry=https://registry.npmjs.org/",
|
|
41
|
+
],
|
|
42
|
+
{ encoding: "utf8" },
|
|
43
|
+
);
|
|
44
|
+
if (result.error) throw result.error;
|
|
45
|
+
if (result.status !== 0) {
|
|
46
|
+
const output = `${result.stdout || ""}\n${result.stderr || ""}`;
|
|
47
|
+
if (/\bE404\b|404 Not Found|is not in this registry/iu.test(output))
|
|
48
|
+
return [];
|
|
49
|
+
throw new Error(`npm version discovery failed: ${output.trim()}`);
|
|
50
|
+
}
|
|
51
|
+
const parsed = JSON.parse(String(result.stdout || "[]"));
|
|
52
|
+
return Array.isArray(parsed) ? parsed : [parsed].filter(Boolean);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function writeOutput(name, value) {
|
|
56
|
+
const output = env("GITHUB_OUTPUT");
|
|
57
|
+
if (!output) return;
|
|
58
|
+
fs.appendFileSync(output, `${name}=${String(value)}\n`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveV4ProductPublicationIntent() {
|
|
62
|
+
const name = packageName();
|
|
63
|
+
const sourceSha = env("BUILDCHAIN_SOURCE_SHA", true);
|
|
64
|
+
const manifest = readJson(env("BUILDCHAIN_SEALED_BUNDLE_MANIFEST", true));
|
|
65
|
+
const requiredArtifacts = readJson(
|
|
66
|
+
env("BUILDCHAIN_REQUIRED_ARTIFACTS_PATH", true),
|
|
67
|
+
);
|
|
68
|
+
const channel = env("BUILDCHAIN_CHANNEL", true);
|
|
69
|
+
const intent = selectV4ProductPublicationIntent({
|
|
70
|
+
channel,
|
|
71
|
+
targetRef: env("BUILDCHAIN_TARGET_REF", true),
|
|
72
|
+
sourceSha,
|
|
73
|
+
sourceTimestamp: env("BUILDCHAIN_SOURCE_TIMESTAMP", true),
|
|
74
|
+
repository: env("BUILDCHAIN_REPOSITORY", true),
|
|
75
|
+
packageName: name,
|
|
76
|
+
distTag:
|
|
77
|
+
env("BUILDCHAIN_PUBLISH_DIST_TAG") ||
|
|
78
|
+
(channel === "alpha" ? "alpha" : "latest"),
|
|
79
|
+
sealedBundleRoot: manifest.root,
|
|
80
|
+
requiredArtifactsRoot: v4ContentRoot(
|
|
81
|
+
"v4-product-required-artifacts",
|
|
82
|
+
requiredArtifacts,
|
|
83
|
+
),
|
|
84
|
+
candidateVersion: env("BUILDCHAIN_CANDIDATE_VERSION", true),
|
|
85
|
+
recoveredVersion: env("BUILDCHAIN_RECOVERED_PUBLICATION_VERSION"),
|
|
86
|
+
observedVersions: observedVersions(name),
|
|
87
|
+
});
|
|
88
|
+
const outputPath = path.resolve(
|
|
89
|
+
env("BUILDCHAIN_PRODUCT_PUBLICATION_INTENT_PATH") ||
|
|
90
|
+
".buildchain/release-candidate/v4-product-publication-intent.json",
|
|
91
|
+
);
|
|
92
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
93
|
+
fs.writeFileSync(outputPath, `${JSON.stringify(intent, null, 2)}\n`);
|
|
94
|
+
writeOutput("version", intent.version);
|
|
95
|
+
writeOutput("exact-tag", intent.exactTag);
|
|
96
|
+
writeOutput("intent-path", outputPath);
|
|
97
|
+
writeOutput("intent-root", intent.intentRoot);
|
|
98
|
+
return { name, intent, outputPath };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (
|
|
102
|
+
process.argv[1] &&
|
|
103
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
104
|
+
) {
|
|
105
|
+
try {
|
|
106
|
+
const result = resolveV4ProductPublicationIntent();
|
|
107
|
+
process.stdout.write(
|
|
108
|
+
`v4 product publication intent: ${result.name}@${result.intent.version} (${result.intent.mode})\n`,
|
|
109
|
+
);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(`v4-product-publication-intent: ${error.message}`);
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
|
|
9
|
+
export function resolveV4ReleaseCandidateAdapter({
|
|
10
|
+
resumeCandidateRunId = "",
|
|
11
|
+
} = {}) {
|
|
12
|
+
const mode = String(resumeCandidateRunId || "").trim() ? "recovery" : "fresh";
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
mode,
|
|
15
|
+
script:
|
|
16
|
+
mode === "recovery"
|
|
17
|
+
? "scripts/resume-from-candidate-run.mjs"
|
|
18
|
+
: "scripts/release-candidate-resolver.mjs",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function runV4ReleaseCandidateAdapter({
|
|
23
|
+
env = process.env,
|
|
24
|
+
exec = execFileSync,
|
|
25
|
+
} = {}) {
|
|
26
|
+
const route = resolveV4ReleaseCandidateAdapter({
|
|
27
|
+
resumeCandidateRunId: env.BUILDCHAIN_RESUME_CANDIDATE_RUN_ID,
|
|
28
|
+
});
|
|
29
|
+
exec(process.execPath, [path.join(root, route.script)], {
|
|
30
|
+
env,
|
|
31
|
+
stdio: "inherit",
|
|
32
|
+
});
|
|
33
|
+
return route;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
process.argv[1] &&
|
|
38
|
+
import.meta.url === pathToFileURL(process.argv[1]).href
|
|
39
|
+
) {
|
|
40
|
+
runV4ReleaseCandidateAdapter();
|
|
41
|
+
}
|